├── .gitignore ├── LICENSE ├── README.md ├── autoload └── lichess │ ├── board_setup.vim │ ├── play.vim │ └── util.vim ├── plugin └── lichess.vim └── python ├── play_game.py ├── server.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | .jukit/ 2 | mylichesstoken.py 3 | fen_test_cases.py 4 | log/ 5 | __pycache__ 6 | .debug_level 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lukas Pichlmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-lichess 2 | 3 | Play online chess in (Neo)Vim! 4 | 5 | ![vimlichessdemo](https://user-images.githubusercontent.com/57172028/190704946-4708be17-83c0-4652-ae3e-9cb958faa557.gif) 6 | 7 | ### Why? 8 | 9 | Because why not. Not having to leave (Neo)Vim to play online chess should be a basic human right. 10 | 11 | ### Honestly, why? 12 | 13 | Honestly, because why not. 14 | 15 | ## Requirements 16 | 17 | * (Neo)Vim with python3 support 18 | * `berserk` package for python3 (install via `pip install berserk`) - note in case you have the package simplejson installed: berserk seems to be incompatible with simplejson, see [this comment for a possible workaround](https://github.com/luk400/vim-lichess/issues/2#issuecomment-1250392384) 19 | * A lichess account 20 | 21 | ## Basic setup and how to play 22 | 23 | Install the plugin using e.g. `Plug 'luk400/vim-lichess'` in case you're using vim-plug. 24 | 25 | Open (Neo)Vim and run `:LichessFindGame`. You'll be prompted with instructions on how to create and specify your lichess API token in your vim config if you haven't yet (see first variable in section [Other parameters](#other-parameters)). If you've already set your API Token, a new buffer will open and a new game will be started. 26 | 27 | ***You can then play by simply left-clicking on a piece and then right-clicking on its destination square***, or alternatively by typing the move in UCI format ([see this link for examples](https://en.wikipedia.org/wiki/Universal_Chess_Interface#Design)) after using the `LichessMakeMoveUCI` command. 28 | 29 | For other actions, such as resigning, offering draws, takebacks, chatting, etc. see section [Commands and mappings](#commands-and-mappings) 30 | 31 | When the game is over, you can either start a new game again using `:LichessFindGame` or delete the buffer using e.g. `:bd` and get back to whatever you've been doing before. 32 | 33 | ## Commands and mappings 34 | 35 | #### Commands 36 | * `:LichessFindGame`: Find a new game using [the parameters specified in your vim config](#game-parameters) 37 | * `:LichessResign`: Resign a game 38 | * `:LichessAbort`: Abort a game 39 | * `:LichessClaimVictory`: Claim victory if opponent has abandoned the game (unfortunately there's no way to determine whether a game is "claimable" through lichess API, thus you'll just have to try by running the command when you think the opponent might've abandoned the game) 40 | * `:LichessDrawOfferAccept`: Create or accept a draw offer 41 | * `:LichessDrawDecline`: Decline a draw offer 42 | * `:LichessTakebackOfferAccept`: Create or accept a takeback offer 43 | * `:LichessTakebackOfferDecline`: Decline a takeback offer 44 | * `:LichessMakeMoveUCI`: type a move to make in UCI format ([see this link for examples](https://en.wikipedia.org/wiki/Universal_Chess_Interface#Design)) 45 | * `:LichessChat`: write a message in chat (note that your messages won't register if you're shadowbanned) 46 | 47 | #### Mappings 48 | ```vim 49 | nnoremap lm :LichessMakeMoveUCI 50 | nnoremap lc :LichessChat 51 | nnoremap la :LichessAbort 52 | nnoremap lr :LichessResign 53 | nnoremap ldo :LichessOfferDraw 54 | nnoremap lda :LichessAcceptDraw 55 | nnoremap ldd :LichessDeclineDraw 56 | nnoremap ch :call lichess#play#find_game() 57 | ``` 58 | 59 | ## Global variables 60 | 61 | #### Game parameters 62 | ```vim 63 | let g:lichess_autoqueen = 1 64 | " whether to automatically promote to queen or not 65 | let g:lichess_time = 10 66 | " game time in minutes - must be >= 8, since lichess API only allows rapid or classical games 67 | let g:lichess_increment = 0 68 | " increment in seconds 69 | let g:lichess_rated = 1 70 | " whether to play rated games (1) or unrated games (0) 71 | let g:lichess_variant = "standard" 72 | " lichess variant to play -> this plugin has currently only been tested with 'standard'! possible values: ['standard', 'chess960', 'crazyhouse', 'antichess', 'atomic', 'horde', 'kingOfTheHill', 'racingKings', 'threeCheck'] 73 | let g:lichess_color = "random" 74 | " which color you want to play as. possible values: ['white', 'black', 'random'] 75 | let g:lichess_rating_range = [] 76 | " rating range of your opponents, can be an empty list to use the default (recommended) or a list like `[low,high]`, where `low` and `high` are integers. 77 | ``` 78 | 79 | #### Other parameters 80 | ```vim 81 | let g:lichess_api_token = '' 82 | " your required lichess API token. you can easily easily create one which you can put in your config using this link: https://lichess.org/account/oauth/token/create?scopes[]=challenge:write&scopes[]=board:play&description=vim+lichess 83 | let g:python_cmd = 'python3' 84 | " python command to run server in background - this should be the python executable for which berserk is installed (can also be a full path) 85 | let g:lichess_debug_level = -1 86 | " set debugging level. -1 means nothing is logged and no log files are created, 0 -> all info is logged, 1 -> only warnings and 'worse' are logged, 2 -> only errors and 'worse' are logged, 3 -> only crashes are logged 87 | ``` 88 | 89 | #### Highlighting 90 | 91 | In case you want change the board colors or other highlighting options, you can modify any of the following highlights and put them in your vim config (AFTER the plugin is loaded - e.g. after `call plug#end()` in case you're using vim-plug) to overwrite them: 92 | 93 | ```vim 94 | highlight lichess_black_squares guibg=#B58863 ctermbg=94 95 | " highlighting of black squares 96 | highlight lichess_white_squares guibg=#F0D9B5 ctermbg=7 97 | " highlighting of white squares 98 | highlight lichess_black_pieces guifg=#000000 guibg=#000000 ctermbg=0 ctermfg=0 99 | " highlighting of black pieces 100 | highlight lichess_white_pieces guifg=#ffffff guibg=#ffffff ctermbg=15 ctermfg=15 101 | " highlighting of white pieces 102 | highlight lichess_from_square_dark guifg=#AAA23A guibg=#AAA23A ctermbg=172 ctermfg=172 103 | " highlighting of the previous and new square of the latest moved piece if it's a dark square 104 | highlight lichess_from_square_light guifg=#CDD26A guibg=#CDD26A ctermbg=178 ctermfg=178 105 | " highlighting of the previous and new square of the latest moved piece if it's a light square 106 | highlight lichess_cell_delimiters guifg=#000000 guibg=#000000 ctermbg=0 ctermfg=0 107 | " highlighting of vertical cell delimiters between squares 108 | highlight lichess_user_turn guibg=#3eed6c guifg=#000000 ctermbg=2 ctermfg=0 cterm=bold gui=bold 109 | " highlighting of the name of the user whose turn it currently is 110 | highlight lichess_user_noturn guifg=#ffffff ctermfg=15 cterm=bold gui=bold 111 | " highlighting of the name of the user whose turn it's currently not 112 | highlight lichess_searching_game guifg=#42d7f5 guibg=#000000 ctermfg=14 ctermbg=0 cterm=bold gui=bold 113 | " highlighting of 'searching game...' prompt 114 | highlight lichess_game_ended guibg=#e63c30 guifg=#ffffff ctermbg=1 ctermfg=15 cterm=bold gui=bold 115 | " highlighting of last game status (e.g. 'MATE' or 'RESIGN') 116 | highlight lichess_chat guibg=#e3f27e guifg=#000000 ctermbg=191 ctermfg=0 117 | " highlighting of of opponent chat messages 118 | highlight lichess_chat_system guibg=#ed8787 guifg=#000000 ctermbg=178 ctermfg=0 119 | " highlighting of of lichess chat messages 120 | highlight lichess_chat_you guibg=#b4e364 guifg=#000000 ctermbg=190 ctermfg=0 121 | " highlighting of your chat messages 122 | highlight lichess_chat_bold guibg=#e3f27e guifg=#000000 ctermbg=191 ctermfg=0 cterm=bold gui=bold 123 | " highlighting of of 'CHAT:' prompt 124 | highlight lichess_move_info guibg=#9ea832 guifg=#000000 ctermbg=3 ctermfg=0 cterm=bold gui=bold 125 | " echohl highlighting of echoed move-message 126 | highlight lichess_too_many_requests guibg=#c20202 guifg=#ffffff ctermbg=15 ctermfg=9 cterm=bold gui=bold 127 | " echohl highlighting of too_many_requests error 128 | ``` 129 | 130 | #### Piece representation 131 | 132 | In case you don't like my piece design, you can design your own as shown below. 133 | You can also change their width/height (number of characters in strings/number of strings in list) to make them bigger if you want more detail, as long as you follow the following restrictions: 134 | * all pieces must have the same height (number of strings in piece list) 135 | * all pieces must have the same width (number of characters in the strings) 136 | * there must be exactly one unique non-whitespace character for all black and one unique non-whitespace character for all white pieces. This can not be the same for the white and black pieces and it must have a length of 1 (there are certain characters which have a different length in vim - e.g.: `echo len('║')` will print `3` even though it's a single character). Easiest way is just to use `,` for black and `;` for the white pieces, as shown below 137 | 138 | 139 | ```vim 140 | " black pieces 141 | let g:lichess_piece_p = 142 | \ [" ", 143 | \ " ,, ", 144 | \ " ,,,, ", 145 | \ " ,,,, ", 146 | \ " ,,,, ", 147 | \ " "] " black pawn 148 | let g:lichess_piece_r = 149 | \ [" ", 150 | \ " , ,, , ", 151 | \ " ,,,,,, ", 152 | \ " ,,,,,, ", 153 | \ " ,,,,,,,, ", 154 | \ " "] " black rook 155 | let g:lichess_piece_k = 156 | \ [" ", 157 | \ " ,, ", 158 | \ " ,,,,,,,, ", 159 | \ " ,, ", 160 | \ " ,, ", 161 | \ " "] " black king 162 | let g:lichess_piece_q = 163 | \ [" ", 164 | \ " , ,, , ", 165 | \ " ,,,, ", 166 | \ " ,, ", 167 | \ " ,,,,,, ", 168 | \ " "] " black queen 169 | let g:lichess_piece_b = 170 | \ [" ", 171 | \ " ,,,, ", 172 | \ " ,,,, ", 173 | \ " ,, ", 174 | \ " ,,,,,, ", 175 | \ " "] " black bishop 176 | let g:lichess_piece_n = 177 | \ [" ", 178 | \ " ,,, ", 179 | \ " ,,, ,, ", 180 | \ " ,,, ", 181 | \ " ,,,,,, ", 182 | \ " "] " black knight 183 | 184 | " white pieces 185 | let g:lichess_piece_P = 186 | \ [" ", 187 | \ " ;; ", 188 | \ " ;;;; ", 189 | \ " ;;;; ", 190 | \ " ;;;; ", 191 | \ " "] " white pawn 192 | let g:lichess_piece_R = 193 | \ [" ", 194 | \ " ; ;; ; ", 195 | \ " ;;;;;; ", 196 | \ " ;;;;;; ", 197 | \ " ;;;;;;;; ", 198 | \ " "] " white rook 199 | let g:lichess_piece_K = 200 | \ [" ", 201 | \ " ;; ", 202 | \ " ;;;;;;;; ", 203 | \ " ;; ", 204 | \ " ;; ", 205 | \ " "] " white king 206 | let g:lichess_piece_Q = 207 | \ [" ", 208 | \ " ; ;; ; ", 209 | \ " ;;;; ", 210 | \ " ;; ", 211 | \ " ;;;;;; ", 212 | \ " "] " white queen 213 | let g:lichess_piece_B = 214 | \ [" ", 215 | \ " ;;;; ", 216 | \ " ;;;; ", 217 | \ " ;; ", 218 | \ " ;;;;;; ", 219 | \ " "] " white bishop 220 | let g:lichess_piece_N = 221 | \ [" ", 222 | \ " ;;; ", 223 | \ " ;;; ;; ", 224 | \ " ;;; ", 225 | \ " ;;;;;; ", 226 | \ " "] " white knight 227 | ``` 228 | 229 | # Credit 230 | 231 | All credit goes to my huge procrastination issues 232 | -------------------------------------------------------------------------------- /autoload/lichess/board_setup.vim: -------------------------------------------------------------------------------- 1 | """""""""""""""""""" 2 | " highlighting setup 3 | """""""""""""""""""" 4 | let params = lichess#play#get_square_dim_and_piece_chars() 5 | let s:square_width = params[0] 6 | let s:square_height = params[1] 7 | let s:white_piece_char = params[2] 8 | let s:black_piece_char = params[3] 9 | let s:start_wcell = '`' 10 | let s:start_bcell = "'" 11 | let s:move_cell_dark = '-' 12 | let s:move_cell_light = '_' 13 | 14 | if !hlexists('lichess_cell_delimiters') 15 | highlight lichess_cell_delimiters guifg=#000000 guibg=#000000 ctermbg=0 ctermfg=0 16 | endif 17 | if !hlexists('lichess_black_squares') 18 | highlight lichess_black_squares guibg=#B58863 ctermbg=94 19 | endif 20 | if !hlexists('lichess_white_squares') 21 | highlight lichess_white_squares guibg=#F0D9B5 ctermbg=7 22 | endif 23 | if !hlexists('lichess_black_pieces') 24 | highlight lichess_black_pieces guifg=#000000 guibg=#000000 ctermbg=0 ctermfg=0 25 | endif 26 | if !hlexists('lichess_white_pieces') 27 | highlight lichess_white_pieces guifg=#ffffff guibg=#ffffff ctermbg=15 ctermfg=15 28 | endif 29 | if !hlexists('lichess_from_square_dark') 30 | highlight lichess_from_square_dark guifg=#AAA23A guibg=#AAA23A ctermbg=172 ctermfg=172 31 | endif 32 | if !hlexists('lichess_from_square_light') 33 | highlight lichess_from_square_light guifg=#CDD26A guibg=#CDD26A ctermbg=178 ctermfg=178 34 | endif 35 | 36 | let s:empty_line = repeat(' ', s:square_width) 37 | let s:empty_line_move_dark = repeat(s:move_cell_dark, s:square_width) 38 | let s:empty_line_move_light = repeat(s:move_cell_light, s:square_width) 39 | 40 | fun! lichess#board_setup#syntax_matching() abort 41 | syn clear 42 | 43 | exe 'syn match lichess_cell_delimiters /' . s:start_wcell . '/ contains=ALL containedin=ALL' 44 | exe 'syn match lichess_cell_delimiters /' . s:start_bcell . '/ contains=ALL containedin=ALL' 45 | 46 | exe 'syn match lichess_black_squares /' . s:start_bcell . '.\{-}' . s:start_wcell . '/ containedin=ALL' 47 | exe 'syn match lichess_white_squares /' . s:start_wcell . '.\{-}' . s:start_bcell . '/ containedin=ALL' 48 | 49 | exe 'syn match lichess_black_pieces /' . s:black_piece_char . '/ containedin=lichess_black_squares,lichess_white_squares' 50 | exe 'syn match lichess_white_pieces /' . s:white_piece_char . '/ containedin=lichess_black_squares,lichess_white_squares' 51 | 52 | exe 'syn match lichess_from_square_dark /' . s:move_cell_dark . '/ containedin=lichess_black_squares,lichess_white_squares' 53 | exe 'syn match lichess_from_square_light /' . s:move_cell_light . '/ containedin=lichess_black_squares,lichess_white_squares' 54 | endfun 55 | 56 | 57 | """""""""""""""" 58 | " board creation 59 | """""""""""""""" 60 | let s:piece_symbols = { 61 | \ 'p': p, 62 | \ 'r': r, 63 | \ 'k': k, 64 | \ 'q': q, 65 | \ 'b': b, 66 | \ 'n': n, 67 | \ 'P': P, 68 | \ 'R': R, 69 | \ 'K': K, 70 | \ 'Q': Q, 71 | \ 'B': B, 72 | \ 'N': N, 73 | \ } 74 | 75 | function! s:create_board(fen, latest_move) abort 76 | " example FEN: 77 | " rn2k1r1/ppp1pp1p/3p2p1/5bn1/P7/2N2B2/1PPPPP2/2BNK1RR 78 | 79 | if a:latest_move != "None" 80 | let from_row = a:latest_move[0] - 1 81 | let from_column = a:latest_move[1] - 1 82 | let to_row = a:latest_move[2] - 1 83 | let to_column = a:latest_move[3] - 1 84 | else 85 | let from_row = -1 86 | let from_column = -1 87 | let to_row = -1 88 | let to_column = -1 89 | endif 90 | 91 | let board = repeat(s:start_wcell, 9 + s:square_width * 8) . "\n" 92 | let i = 0 93 | for str in split(a:fen, '/') 94 | " rn2k1r1 95 | for j in range(s:square_height) 96 | let n = 0 97 | for char in split(str, '\zs') 98 | " r 99 | let next_cell_black = fmod(i + n, 2) == 0.0 100 | let is_move_cell = (i == from_row) && (n == from_column) || (i == to_row) && (n == to_column) 101 | if next_cell_black 102 | let board = board . s:start_wcell 103 | else 104 | let board = board . s:start_bcell 105 | endif 106 | 107 | if str2float(char) > 0 108 | for k in range(char) 109 | if is_move_cell && !next_cell_black 110 | let board = board . s:empty_line_move_dark 111 | elseif is_move_cell 112 | let board = board . s:empty_line_move_light 113 | else 114 | let board = board . s:empty_line 115 | endif 116 | 117 | if next_cell_black && (k < char - 1) 118 | let board = board . s:start_bcell 119 | elseif (k < char - 1) 120 | let board = board . s:start_wcell 121 | endif 122 | let n += 1 123 | let next_cell_black = fmod(i + n, 2) == 0.0 124 | let is_move_cell = (i == from_row) && (n == from_column) || (i == to_row) && (n == to_column) 125 | endfor 126 | else 127 | if is_move_cell && !next_cell_black 128 | let board = board . substitute(s:piece_symbols[char][j], ' ', s:move_cell_dark, 'g') 129 | elseif is_move_cell 130 | let board = board . substitute(s:piece_symbols[char][j], ' ', s:move_cell_light, 'g') 131 | else 132 | let board = board . s:piece_symbols[char][j] 133 | endif 134 | let n += 1 135 | endif 136 | endfor 137 | 138 | if fmod(i, 2) == 0.0 139 | let board = board . s:start_wcell . "\n" 140 | else 141 | let board = board . s:start_bcell . "\n" 142 | endif 143 | endfor 144 | let i += 1 145 | endfor 146 | let board = board . repeat(s:start_wcell, 9 + s:square_width * 8) 147 | 148 | return board 149 | endfunction 150 | 151 | 152 | function! lichess#board_setup#display_board(fen, latest_move) abort 153 | let board = s:create_board(a:fen, a:latest_move) 154 | call append(0, split(board, '\n')) 155 | endfunction 156 | -------------------------------------------------------------------------------- /autoload/lichess/play.vim: -------------------------------------------------------------------------------- 1 | """"""""""""""""""" 2 | " get piece symbols 3 | """"""""""""""""""" 4 | let p = get(g:, 'lichess_piece_p', 5 | \ [" ", 6 | \ " ,, ", 7 | \ " ,,,, ", 8 | \ " ,,,, ", 9 | \ " ,,,, ", 10 | \ " "]) 11 | let r = get(g:, 'lichess_piece_r', 12 | \ [" ", 13 | \ " , ,, , ", 14 | \ " ,,,,,, ", 15 | \ " ,,,,,, ", 16 | \ " ,,,,,,,, ", 17 | \ " "]) 18 | let k = get(g:, 'lichess_piece_k', 19 | \ [" ", 20 | \ " ,, ", 21 | \ " ,,,,,,,, ", 22 | \ " ,, ", 23 | \ " ,, ", 24 | \ " "]) 25 | let q = get(g:, 'lichess_piece_q', 26 | \ [" ", 27 | \ " , ,, , ", 28 | \ " ,,,, ", 29 | \ " ,, ", 30 | \ " ,,,,,, ", 31 | \ " "]) 32 | let b = get(g:, 'lichess_piece_b', 33 | \ [" ", 34 | \ " ,,,, ", 35 | \ " ,,,, ", 36 | \ " ,, ", 37 | \ " ,,,,,, ", 38 | \ " "]) 39 | let n = get(g:, 'lichess_piece_n', 40 | \ [" ", 41 | \ " ,,, ", 42 | \ " ,,, ,, ", 43 | \ " ,,, ", 44 | \ " ,,,,,, ", 45 | \ " "]) 46 | 47 | let P = get(g:, 'lichess_piece_P', 48 | \ [" ", 49 | \ " ;; ", 50 | \ " ;;;; ", 51 | \ " ;;;; ", 52 | \ " ;;;; ", 53 | \ " "]) 54 | let R = get(g:, 'lichess_piece_R', 55 | \ [" ", 56 | \ " ; ;; ; ", 57 | \ " ;;;;;; ", 58 | \ " ;;;;;; ", 59 | \ " ;;;;;;;; ", 60 | \ " "]) 61 | let K = get(g:, 'lichess_piece_K', 62 | \ [" ", 63 | \ " ;; ", 64 | \ " ;;;;;;;; ", 65 | \ " ;; ", 66 | \ " ;; ", 67 | \ " "]) 68 | let Q = get(g:, 'lichess_piece_Q', 69 | \ [" ", 70 | \ " ; ;; ; ", 71 | \ " ;;;; ", 72 | \ " ;; ", 73 | \ " ;;;;;; ", 74 | \ " "]) 75 | let B = get(g:, 'lichess_piece_B', 76 | \ [" ", 77 | \ " ;;;; ", 78 | \ " ;;;; ", 79 | \ " ;; ", 80 | \ " ;;;;;; ", 81 | \ " "]) 82 | let N = get(g:, 'lichess_piece_N', 83 | \ [" ", 84 | \ " ;;; ", 85 | \ " ;;; ;; ", 86 | \ " ;;; ", 87 | \ " ;;;;;; ", 88 | \ " "] 89 | \) 90 | 91 | 92 | """"""""""""""""""""""""""""""""" 93 | " check validity of piece symbols 94 | """"""""""""""""""""""""""""""""" 95 | let black_pieces = [p, r, k, q, b, n] 96 | let white_pieces = [P, R, K, Q, B, N] 97 | let i = 0 98 | for piece_set in [black_pieces, white_pieces] 99 | for piece in piece_set 100 | let str_concat = join(split(join(piece)), '') 101 | let all_chars = split(str_concat, '\zs') 102 | let unique_chars = filter(copy(all_chars), 'index(all_chars, v:val, v:key+1)==-1') 103 | let err_msg = 'Error: all piece representations must contain exactly one ' 104 | \ . 'unique character (ignoring whitespaces), and there must be exactly ' 105 | \ . 'one such character for the white and one for the black pieces!' 106 | if !(len(unique_chars) == 1) 107 | echohl ErrorMsg | echom err_msg | echohl None 108 | finish 109 | endif 110 | 111 | if i==0 && !exists('s:black_piece_char') 112 | let s:black_piece_char = unique_chars[0] 113 | elseif i==0 && unique_chars[0] != s:black_piece_char 114 | echohl ErrorMsg | echom err_msg | echohl None 115 | finish 116 | elseif i==1 && !exists('s:white_piece_char') 117 | let s:white_piece_char = unique_chars[0] 118 | elseif i==1 && unique_chars[0] != s:white_piece_char 119 | echohl ErrorMsg | echom err_msg | echohl None 120 | finish 121 | endif 122 | 123 | let h = len(piece) 124 | 125 | let err_msg = 'Error: all piece representations must have the same height!' 126 | if !exists('s:square_height') 127 | let s:square_height = h 128 | elseif h != s:square_height 129 | echohl ErrorMsg | echom err_msg | echohl None 130 | finish 131 | endif 132 | 133 | for l in piece 134 | let w = len(l) 135 | if !exists('s:square_width') 136 | let s:square_width = w 137 | elseif w != s:square_width 138 | echohl ErrorMsg | echom err_msg | echohl None 139 | finish 140 | endif 141 | endfor 142 | endfor 143 | let i += 1 144 | endfor 145 | 146 | if s:black_piece_char == s:white_piece_char 147 | let err_msg = "Error: white piece character and black piece character can't be the same!" 148 | echohl ErrorMsg | echom err_msg | echohl None 149 | finish 150 | elseif len(s:black_piece_char) != 1 151 | let err_msg = "Error: Only piece characters with length 1 allowed (check via `len`" 152 | \ . " in vim), otherwise the cursor position can't be determined correctly! " 153 | \ . "Current character for black pieces (" . s:black_piece_char . "): " 154 | \ . "len(" . s:black_piece_char . ")=" . len(s:black_piece_char) 155 | echohl ErrorMsg | echom err_msg | echohl None 156 | finish 157 | elseif len(s:white_piece_char) != 1 158 | let err_msg = "Error: Only piece characters with length 1 allowed (check via `len`" 159 | \ . " in vim), otherwise the cursor position can't be determined correctly! " 160 | \ . "Current character for white pieces (" . s:white_piece_char . "): " 161 | \ . "len(" . s:white_piece_char . ")=" . len(s:white_piece_char) 162 | echohl ErrorMsg | echom err_msg | echohl None 163 | finish 164 | endif 165 | 166 | fun! lichess#play#get_square_dim_and_piece_chars() abort 167 | return [s:square_width, s:square_height, s:white_piece_char, s:black_piece_char] 168 | endfun 169 | 170 | 171 | """"""""""""""""""""""""""""""""""""""""" 172 | " other needed variables and highlighting 173 | """"""""""""""""""""""""""""""""""""""""" 174 | let s:lichess_fen = "None" 175 | let s:hl_was_set = 0 176 | let yoffset = 2 177 | let xoffset = 0 178 | 179 | if !hlexists('lichess_move_info') 180 | highlight lichess_move_info guibg=#9ea832 guifg=#000000 ctermbg=3 ctermfg=0 cterm=bold gui=bold 181 | endif 182 | if !hlexists('lichess_too_many_requests') 183 | highlight lichess_too_many_requests guibg=#c20202 guifg=#ffffff ctermbg=15 ctermfg=9 cterm=bold gui=bold 184 | endif 185 | if !hlexists('lichess_user_turn') 186 | highlight lichess_user_turn guibg=#3eed6c guifg=#000000 ctermbg=2 ctermfg=0 cterm=bold gui=bold 187 | endif 188 | if !hlexists('lichess_user_noturn') 189 | highlight lichess_user_noturn guifg=#ffffff ctermfg=15 cterm=bold gui=bold 190 | endif 191 | if !hlexists('lichess_searching_game') 192 | highlight lichess_searching_game guifg=#42d7f5 guibg=#000000 ctermfg=14 ctermbg=0 cterm=bold gui=bold 193 | endif 194 | if !hlexists('lichess_game_ended') 195 | highlight lichess_game_ended guibg=#e63c30 guifg=#ffffff ctermbg=1 ctermfg=15 cterm=bold gui=bold 196 | endif 197 | if !hlexists('lichess_chat') 198 | highlight lichess_chat guibg=#e3f27e guifg=#000000 ctermbg=191 ctermfg=0 199 | endif 200 | if !hlexists('lichess_chat_system') 201 | highlight lichess_chat_system guibg=#ed8787 guifg=#000000 ctermbg=178 ctermfg=0 202 | endif 203 | if !hlexists('lichess_chat_bold') 204 | highlight lichess_chat_bold guibg=#e3f27e guifg=#000000 ctermbg=191 ctermfg=0 cterm=bold gui=bold 205 | endif 206 | if !hlexists('lichess_chat_you') 207 | highlight lichess_chat_you guibg=#b4e364 guifg=#000000 ctermbg=190 ctermfg=0 208 | endif 209 | 210 | 211 | """""""""""""""""""""""""""""""""""""""""""""""""""""" 212 | " dictionaries to map cursor position to board squares 213 | """""""""""""""""""""""""""""""""""""""""""""""""""""" 214 | let s:line_squareline_map_black = {} 215 | for idx in range(1, s:square_height * 8) 216 | let s:line_squareline_map_black[yoffset + 1 + idx] = float2nr(ceil(str2float(idx) / s:square_height)) 217 | endfor 218 | let s:line_squareline_map_black[yoffset + 1] = 8 219 | let s:line_squareline_map_black[s:square_height * 8 + yoffset + 2] = 1 220 | 221 | let s:line_squareline_map_white = {} 222 | for idx in range(1, s:square_height * 8) 223 | let s:line_squareline_map_white[yoffset + 1 + idx] = 9 - float2nr(ceil(str2float(idx) / s:square_height)) 224 | endfor 225 | let s:line_squareline_map_white[yoffset + 1] = 1 226 | let s:line_squareline_map_white[s:square_height * 8 + yoffset + 2] = 8 227 | 228 | 229 | let s:col_squarecol_map_white = {} 230 | for idx in range(1, 8 + s:square_width * 8) 231 | let s:col_squarecol_map_white[xoffset + idx] = float2nr(ceil(str2float(idx) / (s:square_width + 1))) 232 | endfor 233 | let s:col_squarecol_map_white[8 + s:square_width * 8 + 1] = 1 234 | 235 | let s:col_squarecol_map_black = {} 236 | for idx in range(1, 8 + s:square_width * 8) 237 | let s:col_squarecol_map_black[xoffset + idx] = 9 - float2nr(ceil(str2float(idx) / (s:square_width + 1))) 238 | endfor 239 | let s:col_squarecol_map_black[8 + s:square_width * 8 + 1] = 8 240 | 241 | 242 | let s:col_idx_to_letter = {1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f', 7: 'g', 8: 'h'} 243 | 244 | 245 | """""""""""""""""""" 246 | " gameplay functions 247 | """""""""""""""""""" 248 | fun! lichess#play#write_msg(msg_txt) abort 249 | if a:msg_txt == '' 250 | return 251 | endif 252 | let msg = '' . a:msg_txt 253 | call s:query_server(msg) 254 | endfun 255 | 256 | 257 | fun! lichess#play#abort_game() abort 258 | let response = s:query_server("abort_game") 259 | call s:check_for_query_error(response) 260 | call lichess#util#log_msg('Game aborted. Response: ' . response, 0) 261 | endfun 262 | 263 | 264 | fun! lichess#play#resign_game() abort 265 | let response = s:query_server("resign_game") 266 | call s:check_for_query_error(response) 267 | call lichess#util#log_msg('Game resigned. Response: ' . response, 0) 268 | endfun 269 | 270 | 271 | fun! lichess#play#claim_victory() abort 272 | let response = s:query_server("claim_victory") 273 | call s:check_for_query_error(response) 274 | call lichess#util#log_msg('Tried to claim victory. Response: ' . response, 0) 275 | endfun 276 | 277 | 278 | fun! lichess#play#draw_offer(accept) abort 279 | let response = s:query_server("" . a:accept) 280 | call s:check_for_query_error(response) 281 | call lichess#util#log_msg('Handle draw offer - accept=' . a:accept . ' - response: ' . response, 0) 282 | endfun 283 | 284 | 285 | fun! lichess#play#takeback_offer(accept) abort 286 | let response = s:query_server("" . a:accept) 287 | call s:check_for_query_error(response) 288 | call lichess#util#log_msg('Handle takeback offer - accept=' . a:accept . ' - response: ' . response, 0) 289 | endfun 290 | 291 | 292 | fun! lichess#play#make_move(pos_end) abort 293 | if !exists('b:lichess_pos_init') || !exists('g:lichess_current_color') 294 | return 295 | endif 296 | 297 | let pos_init_idx = s:cursor_pos_to_square_pos(b:lichess_pos_init, g:lichess_current_color) 298 | if type(pos_init_idx) != 3 299 | return 300 | endif 301 | let pos_init = s:col_idx_to_letter[pos_init_idx[1]] . pos_init_idx[0] 302 | unlet b:lichess_pos_init 303 | 304 | let pos_end_idx = s:cursor_pos_to_square_pos(a:pos_end, g:lichess_current_color) 305 | if type(pos_end_idx) != 3 306 | return 307 | endif 308 | let pos_end = s:col_idx_to_letter[pos_end_idx[1]] . pos_end_idx[0] 309 | 310 | let move_uci = pos_init . pos_end 311 | if !g:lichess_autoqueen && s:is_promotion(g:lichess_current_color, pos_init_idx, pos_end_idx) 312 | call lichess#util#log_msg('function lichess#play#make_move: pawn is promoting!', 1) 313 | let promotion_pieces = ['q', 'r', 'n', 'b'] 314 | let new_piece = confirm("Choose promotion piece!", "&queen\n&rook\nk&night\n&bishop", 1) 315 | let move_uci = pos_init . pos_end . promotion_pieces[new_piece - 1] 316 | endif 317 | 318 | echo 319 | call s:query_server('' . move_uci, 'lichess_move_info') 320 | endfun 321 | 322 | 323 | fun! lichess#play#make_move_keyboard() abort 324 | let move_uci = input('Enter move (UCI notation): ') 325 | echo 326 | call s:query_server('' . move_uci, 'lichess_move_info') 327 | endfun 328 | 329 | 330 | fun! lichess#play#find_game(...) abort 331 | if exists('g:_lichess_start_tries') && g:_lichess_start_tries >= 5 332 | call lichess#play#update_board() 333 | return 334 | endif 335 | 336 | let g:lichess_api_token = get(g:, 'lichess_api_token', "") 337 | if !len(g:lichess_api_token) 338 | echohl ErrorMsg | echom "No API Token found! You need to add `let g:lichess_api_token = YOUR_API_TOKEN` to your vim config using your generated Token!" | 339 | \ echom "You can easily create this API Token by logging in on lichess.org and then simply following the following link:" | 340 | \ echom "https://lichess.org/account/oauth/token/create?scopes[]=challenge:write&scopes[]=board:play&description=vim+lichess" | 341 | \ echohl None 342 | return 343 | endif 344 | 345 | let berserk_not_installed = stridx(system(g:python_cmd . ' -c "import berserk"'), 'ModuleNotFoundError') >= 0 346 | if berserk_not_installed 347 | let choice = confirm("Berserk is not installed and needed for vim-lichess. Install it now?", "&yes\n&no", 1) 348 | if choice == 1 349 | exe "!" . g:python_cmd . " -m pip install berserk" 350 | else 351 | echohl ErrorMsg | echom 'Berserk needs to be installed to use vim-lichess!' | echohl None 352 | finish 353 | endif 354 | endif 355 | 356 | if g:lichess_debug_level != -1 357 | let plugin_path = lichess#util#plugin_path() 358 | call writefile([g:lichess_debug_level], plugin_path . '/.debug_level') 359 | endif 360 | 361 | if !(expand('%') == 'newgame.chess') 362 | let shortmess_val = &shortmess 363 | setlocal shortmess+=A 364 | edit newgame.chess | setlocal buftype=nofile 365 | exe 'setlocal shortmess=' . shortmess_val 366 | 367 | if hlexists('Visual') 368 | let vis_setting = substitute(trim(execute('hi Visual'), "\n"), '\s*xxx\s*', ' ', '') 369 | 370 | if stridx(vis_setting, 'Visual cleared') == -1 371 | hi clear Visual 372 | exe "au BufLeave :exe 'hi ' . '" . vis_setting . "'" 373 | exe "au BufEnter :hi clear Visual" 374 | endif 375 | endif 376 | endif 377 | 378 | let rated = g:lichess_rated ? "True" : "False" 379 | let rating_range = len(g:lichess_rating_range) ? '[' . join(g:lichess_rating_range, ',') . ']' : "None" 380 | let query = 'True/' . g:lichess_time . '-' . g:lichess_increment 381 | \ . '-' . rated . '-' . g:lichess_variant . '-' . g:lichess_color . '-' . rating_range 382 | 383 | if !exists('g:_lichess_server_started') || exists('g:_lichess_start_tries') && g:_lichess_start_tries > 0 384 | call s:start_game_loop() 385 | sleep 500m 386 | let response = s:query_server(query) 387 | else 388 | let response = s:query_server(query) 389 | endif 390 | call s:check_for_query_error(response) 391 | 392 | if !a:0 393 | call lichess#setup_mappings() 394 | syn clear lichess_searching_game 395 | syn match lichess_searching_game /Searching for game...\|Retrying.../ 396 | call append(0, ["Searching for game..."]) 397 | else 398 | call append(1, ["Retrying..."]) 399 | endif 400 | 401 | call lichess#play#update_board() 402 | endfun 403 | 404 | 405 | fun! lichess#play#update_board(...) abort 406 | let all_info = s:query_server('get_all_info') 407 | 408 | if s:check_for_query_error(all_info) 409 | if exists('g:_lichess_timer_id') 410 | call timer_stop(g:_lichess_timer_id) 411 | let s:timer_started = 0 412 | endif 413 | 414 | if !exists('g:_lichess_start_tries') 415 | let g:_lichess_start_tries = 1 416 | elseif g:_lichess_start_tries > 5 417 | echohl ErrorMsg | echom "Could not start/continue game! Try again in a minute. Make sure you have a working internet connection, that you correctly specified your API token, and that your account has not been suspended/timed out on lichess.org! If the issue persists, please specify `let g:lichess_debug_level = 0` in your vim config, then try starting a new game again, and after it fails again attach the created log-file (located in " . lichess#util#plugin_path() . "/log/ to a new issue which you can create at https://github.com/luk400/vim-lichess/issues" | echohl None 418 | return 419 | else 420 | let g:_lichess_start_tries += 1 421 | endif 422 | 423 | call lichess#util#log_msg('function lichess#play#find_game had to be restarted', 1) 424 | sleep 250m 425 | call lichess#play#find_game(1) 426 | return 427 | endif 428 | 429 | if exists('g:_lichess_start_tries') && g:_lichess_start_tries > 0 430 | let g:_lichess_start_tries = 0 431 | endif 432 | 433 | if !s:timer_started 434 | let updatetime = 0.5 435 | let g:_lichess_timer_id = timer_start(str2nr(string(1000 * updatetime)), 436 | \ function('lichess#play#update_board'), {'repeat': -1}) 437 | 438 | exe "au BufLeave call timer_pause(" . g:_lichess_timer_id . ", 1)" 439 | exe "au BufEnter call timer_pause(" . g:_lichess_timer_id . ", 0)" 440 | exe "au BufDelete call timer_stop(" . g:_lichess_timer_id . ")" 441 | au BufDelete call s:kill_server() 442 | 443 | let s:timer_started = 1 444 | endif 445 | 446 | 447 | if all_info == 'None' 448 | return 449 | endif 450 | 451 | let all_info = substitute(all_info, ": False", ": 0", "g") 452 | let all_info = substitute(all_info, ": True", ": 1", "g") 453 | let all_info = substitute(all_info, ": None", ': "None"', "g") 454 | let all_info = json_decode(all_info) 455 | 456 | if all_info['last_err'] != "None" 457 | echohl ErrorMsg | echom all_info['last_err'] | echohl None 458 | endif 459 | 460 | let my_color = all_info['color'] 461 | let searching_game = all_info['searching_game'] 462 | 463 | if my_color == 'None' 464 | call lichess#util#log_msg('function lichess#play#update_board(): my_color is None', 1) 465 | return 466 | endif 467 | 468 | let g:lichess_current_color = my_color 469 | 470 | if searching_game != 0 " could also be 'None' 471 | let searching_game = 1 472 | endif 473 | 474 | let player_info = all_info['player_info'] 475 | let player_times = all_info['player_times'] 476 | let username = all_info['username'] 477 | let s:lichess_fen = split(all_info['fen'], ' ')[0] 478 | let is_my_turn = all_info['is_my_turn'] 479 | let status = all_info['status'] 480 | let latest_move = all_info['latest_move'] 481 | let messages = all_info['messages'] 482 | let msg_sep = all_info['msg_sep'] 483 | 484 | let opp_color = my_color == 'white' ? 'black' : 'white' 485 | if s:lichess_fen == 'None' 486 | call lichess#util#log_msg('function lichess#play#update_board(): fen is None', 1) 487 | return 488 | endif 489 | let curpos = getpos('.') 490 | silent! exe '%delete_' 491 | 492 | if latest_move != 'None' && my_color != 'None' 493 | let latest_move = s:get_row_col_move(latest_move, my_color) 494 | endif 495 | 496 | call lichess#board_setup#display_board(s:lichess_fen, latest_move) " DISPLAY BOARD 497 | 498 | if player_info != "None" && player_times != "None" 499 | let player_info = split(player_info, msg_sep) 500 | 501 | let my_rating = player_info[0] 502 | let opp_rating = player_info[1] 503 | let my_name = player_info[2] 504 | let opp_name = player_info[3] 505 | let my_title = player_info[4] == 'None' ? '' : player_info[4] . ' ' 506 | let opp_title = player_info[5] == 'None' ? '' : player_info[5] . ' ' 507 | 508 | let td_since_last = str2float(split(player_times, '/')[0]) 509 | let my_time = str2float(split(split(player_times, '/')[1], '-')[0]) 510 | let opp_time = str2float(split(split(player_times, '/')[1], '-')[1]) 511 | 512 | if is_my_turn 513 | let my_time = float2nr(my_time - td_since_last) 514 | let opp_time = float2nr(opp_time) 515 | else 516 | let opp_time = float2nr(opp_time - td_since_last) 517 | let my_time = float2nr(my_time) 518 | endif 519 | 520 | if opp_time >= 0 && my_time >=0 && status == 'started' 521 | let g:lichess_opp_time = printf("%02d", opp_time / 3600) . ':' . printf("%02d", (opp_time % 3600) / 60) . ':' . printf("%02d", opp_time % 3600 % 60) 522 | let g:lichess_my_time = printf("%02d", my_time / 3600) . ':' . printf("%02d", (my_time % 3600) / 60) . ':' . printf("%02d", my_time % 3600 % 60) 523 | elseif !exists('g:lichess_opp_time') || !exists('g:lichess_my_time') 524 | let g:lichess_opp_time = '--:--:--' 525 | let g:lichess_my_time = '--:--:--' 526 | endif 527 | 528 | let opp_info = opp_title . opp_name . ' [' . opp_rating . '] - ' . g:lichess_opp_time 529 | let my_info = my_title . my_name . ' [' . my_rating . '] - ' . g:lichess_my_time 530 | syn clear lichess_user_turn 531 | syn clear lichess_user_noturn 532 | if is_my_turn 533 | exe 'syn match lichess_user_turn /^' . my_title . my_name . ' .*' . '/ containedin=ALL' 534 | exe 'syn match lichess_user_noturn /^' . opp_title . opp_name . ' .*' . '/ containedin=ALL' 535 | else 536 | exe 'syn match lichess_user_turn /^' . opp_title . opp_name . ' .*' . '/ containedin=ALL' 537 | exe 'syn match lichess_user_noturn /^' . my_title . my_name . ' .*' . '/ containedin=ALL' 538 | endif 539 | 540 | call append(0, [opp_info, '']) 541 | call append('$', my_info) 542 | 543 | if status != 'started' 544 | let pattern = '- ' . toupper(status) . ' -' 545 | syn clear lichess_game_ended 546 | exe 'syn match lichess_game_ended /' . pattern . '/ containedin=ALL' 547 | call append(0, [pattern, '']) 548 | endif 549 | 550 | if messages != 'None' 551 | if !s:hl_was_set 552 | syn match lichess_chat_bold /CHAT:/ containedin=ALL 553 | syn match lichess_chat /CHAT:\_.*/ containedin=ALL 554 | syn match lichess_chat_system /^> lichess:.*/ containedin=ALL 555 | exe 'syn match lichess_chat_you /^> ' . username . ':.*/ containedin=ALL' 556 | let s:hl_was_set = 1 557 | endif 558 | let msg_lines = split(messages, msg_sep) 559 | call map(msg_lines, {key, val -> '> ' . val}) 560 | call append('$', ['', '', 'CHAT:'] + msg_lines) 561 | endif 562 | else 563 | call append(0, ['', '']) 564 | call append('$', '') 565 | endif 566 | 567 | if searching_game && status != 'started' 568 | syn clear lichess_searching_game 569 | syn match lichess_searching_game /Searching for game...\|Retrying.../ 570 | call append(0, ["Searching for game...", "", ""]) 571 | endif 572 | 573 | call cursor(curpos[1], curpos[2]) 574 | endfun 575 | 576 | 577 | """""""""""""""""""""""" 578 | " script local functions 579 | """""""""""""""""""""""" 580 | fun! s:check_for_query_error(response, ...) abort 581 | if a:0 > 0 582 | let hlgroup = a:1 583 | else 584 | let hlgroup = -1 585 | endif 586 | 587 | let had_querry_error = a:response[:len('')-1] == '' 588 | if had_querry_error && hlgroup != -1 589 | exe "echohl " . hlgroup . " | " . "echom substitute(a:response, '', '', '')" . " | echohl None" 590 | return 1 591 | elseif had_querry_error 592 | if stridx(a:response, 'is not in use anymore') < 0 593 | echom substitute(a:response, '', '', '') 594 | endif 595 | return 1 596 | endif 597 | return 0 598 | endfun 599 | 600 | 601 | fun! s:query_server(query, ...) abort 602 | let plugin_path = lichess#util#plugin_path() 603 | python3 << EOF 604 | import os, sys, vim 605 | plugin_path = vim.eval('plugin_path') 606 | sys.path.append(os.path.join(plugin_path, 'python')) 607 | 608 | from util import log_message, query_server 609 | 610 | query = vim.eval('a:query') 611 | port = int(vim.eval('g:_lichess_server_port')) 612 | 613 | log_message(f'function s:query_server(): query - {query}') 614 | try: 615 | response = query_server(query, port) 616 | if isinstance(response, bytes): 617 | response = response.decode('utf-8') 618 | 619 | if response is not None: 620 | response = response.replace("'", '"') 621 | vim.command(f"let response = '{response}'") 622 | except Exception as e: 623 | log_message(f"function s:query_server(): {str(e)}", 2) 624 | vim.command(f"let response = '{str(e)}'") 625 | EOF 626 | 627 | if a:0 > 0 628 | let hlgroup = a:1 629 | else 630 | let hlgroup = -1 631 | endif 632 | 633 | call s:check_for_query_error(response, hlgroup) 634 | return response 635 | endfun 636 | 637 | function! s:get_row_col_move(move, color) abort 638 | let from_letter = matchstr(a:move, '^[a-h]') 639 | let from_number = matchstr(a:move, '^[a-h]\zs[1-8]') 640 | let to_letter = matchstr(a:move, '[a-h][1-8]\zs[a-h]') 641 | let to_number = matchstr(a:move, '[a-h][1-8][a-h]\zs[1-8]') 642 | 643 | if a:color == 'white' 644 | let letter_to_col = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8} 645 | let from_row = 9 - from_number 646 | let to_row = 9 - to_number 647 | let from_col = letter_to_col[from_letter] 648 | let to_col = letter_to_col[to_letter] 649 | else 650 | let letter_to_col = {'a': 8, 'b': 7, 'c': 6, 'd': 5, 'e': 4, 'f': 3, 'g': 2, 'h': 1} 651 | let from_row = from_number 652 | let to_row = to_number 653 | let from_col = letter_to_col[from_letter] 654 | let to_col = letter_to_col[to_letter] 655 | endif 656 | return from_row . from_col . to_row . to_col 657 | endfunction 658 | 659 | 660 | fun! s:cursor_pos_to_square_pos(cursor_pos, color) abort 661 | let lnum = a:cursor_pos[0] 662 | let cnum = a:cursor_pos[1] 663 | 664 | if a:color == 'white' 665 | if !has_key(s:line_squareline_map_white, lnum) || !has_key(s:col_squarecol_map_white, cnum) 666 | echohl ErrorMsg | echom 'Invalid square' | echohl None 667 | return -1 668 | endif 669 | let square_row = s:line_squareline_map_white[lnum] 670 | let square_col = s:col_squarecol_map_white[cnum] 671 | else 672 | if !has_key(s:line_squareline_map_black, lnum) || !has_key(s:col_squarecol_map_black, cnum) 673 | echohl ErrorMsg | echom 'Invalid square' | echohl None 674 | return -1 675 | endif 676 | let square_row = s:line_squareline_map_black[lnum] 677 | let square_col = s:col_squarecol_map_black[cnum] 678 | endif 679 | 680 | return [square_row, square_col] 681 | endfun 682 | 683 | 684 | fun! s:is_promotion(color, pos_init_idx, pos_end_idx) abort 685 | if !(a:color == 'black' && a:pos_end_idx[0] == 1 || a:color == 'white' && a:pos_end_idx[0] == 8) 686 | return 0 687 | endif 688 | 689 | if s:lichess_fen == -1 690 | s:lichess_fen = s:query_server('get_fen') 691 | if s:lichess_fen == 'None' 692 | return 0 693 | endif 694 | endif 695 | 696 | let plugin_path = lichess#util#plugin_path() 697 | python3 << EOF 698 | import os, sys, vim 699 | plugin_path = vim.eval('plugin_path') 700 | sys.path.append(os.path.join(plugin_path, 'python')) 701 | 702 | from util import fen_to_board 703 | fen = vim.eval('s:lichess_fen') 704 | color = vim.eval('a:color') 705 | pos_init = [int(el) for el in vim.eval('a:pos_init_idx')] 706 | board = fen_to_board(fen) 707 | if color == 'white': 708 | idx_row = 8 - pos_init[0] 709 | idx_col = pos_init[1] - 1 710 | else: 711 | idx_row = pos_init[0] - 1 712 | idx_col = 8 - pos_init[1] 713 | is_pawn = int(board[idx_row][idx_col] in ['p', 'P']) 714 | vim.command(f"let is_pawn = {is_pawn}") 715 | EOF 716 | 717 | if !is_pawn 718 | return 0 719 | endif 720 | 721 | return 1 722 | endfun 723 | 724 | 725 | fun! s:kill_server() abort 726 | call lichess#util#log_msg('function s:send_kill_signal: sending kill signal', 0) 727 | call s:query_server('') 728 | if exists('g:_lichess_server_started') 729 | unlet g:_lichess_server_started 730 | endif 731 | endfun 732 | 733 | 734 | fun! s:set_port() abort 735 | python3 << EOF 736 | import socket 737 | from contextlib import closing 738 | 739 | def find_free_port(): 740 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 741 | s.bind(('', 0)) 742 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 743 | return s.getsockname()[1] 744 | 745 | port = find_free_port() 746 | vim.command(f"let g:_lichess_server_port = {port}") 747 | EOF 748 | endfun 749 | 750 | 751 | fun! s:start_game_loop() abort 752 | let g:_lichess_server_started = 1 753 | call s:set_port() 754 | let cmd = g:python_cmd . ' ' . lichess#util#plugin_path() . '/python/play_game.py ' 755 | \ . g:_lichess_server_port . ' ' . g:lichess_api_token 756 | call lichess#util#log_msg('starting game loop', 0) 757 | if has('nvim') 758 | noautocmd exec "vsp|term " . cmd | let g:lichess_server_bufnr = bufnr() | setlocal nobl | hide 759 | else 760 | noautocmd exec "vsp|term ++curwin " . cmd | let g:lichess_server_bufnr = bufnr() | setlocal nobl | hide 761 | endif 762 | exe "autocmd QuitPre,BufDelete :silent! bd! " . g:lichess_server_bufnr 763 | 764 | let s:hl_was_set = 0 765 | let s:timer_started = 0 766 | call lichess#board_setup#syntax_matching() 767 | endfun 768 | 769 | 770 | fun! OnExit(job_id, code, event) dict 771 | if a:code == 0 772 | exec 'bdelete ' . self.bufnr 773 | endif 774 | endfun 775 | -------------------------------------------------------------------------------- /autoload/lichess/util.vim: -------------------------------------------------------------------------------- 1 | let s:script_path = expand(":p:h") 2 | 3 | fun! lichess#util#plugin_path() abort 4 | let plugin_path = split(s:script_path, '/')[:-3] 5 | return '/' . join(plugin_path, '/') 6 | endfun 7 | 8 | fun! lichess#util#log_msg(msg, level) 9 | let plugin_path = lichess#util#plugin_path() 10 | python3 << EOF 11 | import os, sys, vim 12 | plugin_path = vim.eval('plugin_path') 13 | sys.path.append(os.path.join(plugin_path, 'python')) 14 | 15 | from util import log_message 16 | log_message(vim.eval('a:msg'), int(vim.eval('a:level'))) 17 | EOF 18 | endfun 19 | -------------------------------------------------------------------------------- /plugin/lichess.vim: -------------------------------------------------------------------------------- 1 | fun! lichess#setup_mappings() abort 2 | setlocal colorcolumn= 3 | setlocal mouse=a 4 | nnoremap :silent let b:lichess_pos_init = getpos(".")[1:2] 5 | nnoremap :call lichess#play#make_move(getpos(".")[1:2]) 6 | 7 | if !hasmapto('LichessMakeMoveUCI', 'n') 8 | nnoremap lm :LichessMakeMoveUCI 9 | endif 10 | if !hasmapto('LichessChat', 'n') 11 | nnoremap lc :LichessChat 12 | endif 13 | if !hasmapto('LichessAbort', 'n') 14 | nnoremap la :LichessAbort 15 | endif 16 | if !hasmapto('LichessResign', 'n') 17 | nnoremap lr :LichessResign 18 | endif 19 | if !hasmapto('LichessOfferDraw', 'n') 20 | nnoremap ldo :LichessOfferDraw 21 | endif 22 | if !hasmapto('LichessAcceptDraw', 'n') 23 | nnoremap lda :LichessAcceptDraw 24 | endif 25 | if !hasmapto('LichessDeclineDraw', 'n') 26 | nnoremap ldd :LichessDeclineDraw 27 | endif 28 | endfun 29 | 30 | 31 | if !hasmapto('lichess#play#find_game()', 'n') 32 | nnoremap ch :call lichess#play#find_game() 33 | endif 34 | 35 | 36 | command! -nargs=1 LichessChat :call lichess#play#write_msg() 37 | command! LichessFindGame :call lichess#play#find_game() 38 | command! LichessResign :call lichess#play#resign_game() 39 | command! LichessAbort :call lichess#play#abort_game() 40 | command! LichessClaimVictory :call lichess#play#claim_victory() 41 | command! LichessDrawDecline :call lichess#play#draw_offer('no') 42 | command! LichessDrawOfferAccept :call lichess#play#draw_offer('yes') 43 | command! LichessTakebackOfferAccept :call lichess#play#takeback_offer('yes') 44 | command! LichessTakebackOfferDecline :call lichess#play#takeback_offer('no') 45 | command! LichessMakeMoveUCI :call lichess#play#make_move_keyboard() 46 | 47 | 48 | " time (integer in minutes) - must be >= 8, since lichess API only allows rapid or classical games 49 | let g:lichess_time = get(g:, 'lichess_time', 10) 50 | " increment (integer in seconds) 51 | let g:lichess_increment = get(g:, 'lichess_increment', 0) 52 | " rated = False 53 | let g:lichess_rated = get(g:, 'lichess_rated', 1) 54 | " variant = "standard" 55 | let g:lichess_variant = get(g:, 'lichess_variant', "standard") 56 | " color = "random" 57 | let g:lichess_color = get(g:, 'lichess_color', "random") 58 | " rating_range = None (can be passed as [low,high]) 59 | let g:lichess_rating_range = get(g:, 'lichess_rating_range', []) 60 | " set debug level 61 | let g:lichess_debug_level = get(g:, 'lichess_debug_level', -1) 62 | " whether to automatically promote to queen or not 63 | let g:lichess_autoqueen = get(g:, 'lichess_autoqueen', 1) 64 | " command for python executable to run server in background (can also be full path) 65 | let g:python_cmd = get(g:, 'python_cmd', 'python3') 66 | -------------------------------------------------------------------------------- /python/play_game.py: -------------------------------------------------------------------------------- 1 | import berserk 2 | import time 3 | import argparse 4 | from datetime import datetime 5 | from threading import Thread 6 | 7 | import util 8 | from server import Server 9 | 10 | 11 | def seek_game(client, port, *args, **kwargs): 12 | try: 13 | util.log_message( 14 | f"function seek_game: seeking game with args: {args}, kwargs: {kwargs}" 15 | ) 16 | client.board.seek(*args, **kwargs) 17 | except Exception as e: 18 | util.log_message(f"function seek_game: {type(e)}: {str(e)}", 2) 19 | if "HTTP 429" in str(e): 20 | msg = "Too many HTTP requests at once, please wait one minute before trying again!" 21 | util.query_server(f"{msg}", port) 22 | else: 23 | util.query_server(f"{str(e)}", port) 24 | 25 | 26 | def game_loop(client, all_games, port): 27 | util.query_server(f"False/None", port) 28 | state = None 29 | game = all_games[0] 30 | color = game["color"] 31 | 32 | util.query_server(f"{color}", port) 33 | util.query_server(f"{game['fen']}", port) 34 | 35 | game_id = game["gameId"] 36 | util.query_server(f"{game_id}", port) 37 | util.log_message(f"function game_loop: you're {color}! (game id: {game_id})") 38 | fen, latest_move, last_move = None, None, None 39 | 40 | for state in client.board.stream_game_state(game_id): 41 | last_game = game 42 | game = util.get_current_game(client) 43 | 44 | util.log_message(f"state dict: {state} ---- game dict: {game}") 45 | 46 | if game is None: 47 | game = last_game 48 | 49 | if state["type"] == "gameFull": 50 | game_info = state 51 | state = state["state"] 52 | 53 | my_info = game_info[color] 54 | opp_info = game_info["white" if color == "black" else "black"] 55 | util.query_server( 56 | "" 57 | + util.MSG_SEP.join( 58 | [ 59 | str(el) 60 | for el in [ 61 | my_info["rating"], 62 | opp_info["rating"], 63 | my_info["name"], 64 | opp_info["name"], 65 | my_info["title"], 66 | opp_info["title"], 67 | ] 68 | ] 69 | ), 70 | port, 71 | ) 72 | elif state["type"] == "chatLine": 73 | continue 74 | 75 | fen = game["fen"] 76 | 77 | curtime = datetime.now().strftime("%Y-%m-%d-%H:%M:%S.%f") 78 | my_time = state["wtime" if color == "white" else "btime"] 79 | opp_time = state["wtime" if color == "black" else "btime"] 80 | if isinstance(my_time, int): 81 | my_time_seconds = my_time / 1000 82 | opp_time_seconds = opp_time / 1000 83 | else: 84 | my_time_seconds = my_time.second + my_time.minute * 60 + my_time.hour * 3600 85 | opp_time_seconds = ( 86 | opp_time.second + opp_time.minute * 60 + opp_time.hour * 3600 87 | ) 88 | 89 | latest_move = game["lastMove"] 90 | util.query_server( 91 | "" 92 | + util.MSG_SEP.join( 93 | [ 94 | f"{state['status']}", 95 | f"{game['isMyTurn']}", 96 | f"{fen}", 97 | f"{curtime}/{my_time_seconds}-{opp_time_seconds}", 98 | f"{latest_move}", 99 | ] 100 | ), 101 | port, 102 | ) 103 | 104 | util.log_message( 105 | f"function game_loop: game loop over. last_move={last_move}, latest_move={latest_move}, fen={fen}, last state={state}" 106 | ) 107 | 108 | latest_move = state["moves"].split(" ")[-1] if state is not None else None 109 | if ( 110 | not any([el is None for el in [fen, latest_move, state]]) 111 | and len(latest_move) 112 | and state["status"] == "mate" 113 | ): 114 | fen = util.change_fen_last_move(fen, latest_move) 115 | util.log_message( 116 | f"function game_loop: fen after change: {fen} (latest_move:{latest_move})" 117 | ) 118 | util.query_server(f"{fen}", port) 119 | util.query_server(f"{latest_move}", port) 120 | 121 | 122 | def wait_until_start_signal(port): 123 | start_new_game = False 124 | params = "" 125 | while not start_new_game: 126 | response = util.query_server(f"get_start_new_game", port) 127 | if response is None: 128 | time.sleep(1) 129 | util.log_message( 130 | "function wait_until_start_signal: server not responding ", 2 131 | ) 132 | continue 133 | 134 | start_new_game, params = response.decode("utf-8").split("/") 135 | start_new_game = start_new_game == "True" 136 | 137 | if not start_new_game: 138 | time.sleep(1) 139 | 140 | # params in order: 141 | # time (integer in minutes) 142 | # increment (integer in seconds) 143 | # rated = False 144 | # variant = "standard" 145 | # color = "random" 146 | # rating_range = None (can be passed as [low,high]) 147 | t, inc, rated, variant, color, rating_range = params.split("-") 148 | t = int(t) 149 | inc = int(inc) 150 | rated = rated == "True" 151 | if rating_range == "None": 152 | rating_range = None 153 | else: 154 | l, h = rating_range.replace("[", "").replace("]", "").split(",") 155 | rating_range = [int(l), int(h)] 156 | 157 | kwargs = { 158 | "rated": rated, 159 | "variant": variant, 160 | "color": color, 161 | "rating_range": rating_range, 162 | } 163 | 164 | return (t, inc), kwargs 165 | 166 | 167 | def start_server(port, token): 168 | util.log_message("function start_server: starting lichess session") 169 | session = berserk.TokenSession(token) 170 | client = berserk.Client(session) 171 | 172 | try: 173 | client.account.get() 174 | except Exception as e: 175 | util.log_message( 176 | "function start_server: could not get account! " 177 | f"possibly invalid api token ({str(e)})", 178 | 3, 179 | ) 180 | raise e 181 | 182 | server = Server(port, client) 183 | 184 | if not util.is_port_in_use(port): 185 | server.start() 186 | else: 187 | raise Exception("port already in use") 188 | 189 | while True: 190 | all_games = client.games.get_ongoing() 191 | seek_params = wait_until_start_signal(port) 192 | if not len(all_games): 193 | t2 = Thread( 194 | target=seek_game, 195 | args=(client, port, *seek_params[0]), 196 | kwargs=seek_params[1], 197 | ) 198 | t2.start() 199 | all_games = client.games.get_ongoing() 200 | no_game = True 201 | while no_game: 202 | all_games = client.games.get_ongoing() 203 | no_game = not len(all_games) 204 | time.sleep(1) 205 | elif len(all_games) > 1: 206 | raise Exception("more than one game found!") 207 | 208 | util.log_message("function start_server: game found!") 209 | game_loop(client, all_games, port) 210 | util.log_message("function start_server: game ended!") 211 | 212 | 213 | def parse_cmd_args(): 214 | parser = argparse.ArgumentParser(description="vim-lichess server") 215 | parser.add_argument("port", type=int, help="port to listen on") 216 | parser.add_argument("token", type=str, help="lichess api token") 217 | args = parser.parse_args() 218 | return args 219 | 220 | 221 | if __name__ == "__main__": 222 | args = parse_cmd_args() 223 | util.log_message(f"starting server on port: {args.port}", 0) 224 | try: 225 | start_server(args.port, args.token) 226 | except Exception as e: 227 | util.log_message(f"function start_server: {str(e)}", 3) 228 | if "HTTP 429" in str(e): 229 | msg = "Too many HTTP requests at once, please wait one minute before trying again!" 230 | util.query_server(f"{msg}", args.port) 231 | else: 232 | util.query_server(f"{str(e)}", args.port) 233 | 234 | raise e 235 | -------------------------------------------------------------------------------- /python/server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | import re 4 | import berserk 5 | import time 6 | from datetime import datetime 7 | from threading import Thread 8 | 9 | import util 10 | 11 | HOST = socket.gethostname() 12 | 13 | 14 | def _parse_make_move_exception(e): 15 | json_str = re.sub(r'HTTP 400.*?{', '{', str(e)).replace('\'', '"') 16 | error = json.loads(json_str)["error"] 17 | return f"{error}".encode() 18 | 19 | 20 | class Server: 21 | def __init__(self, port, client): 22 | self.port = port 23 | self.client = client 24 | self.reset_parameters() 25 | self.username = client.account.get()["username"] 26 | util.log_message(f"Server started on port {self.port}") 27 | self._update_dict = { 28 | "chat": {"freq": 1, "last": time.time()}, 29 | } 30 | 31 | def reset_parameters(self): 32 | self.start_new_game = False 33 | self.chat_messages = None 34 | self.player_times = None 35 | self.player_info = None 36 | self.latest_move = None 37 | self.game_params = None 38 | self.last_game = None 39 | self.next_move = None 40 | self.last_err = None 41 | self.premove = None 42 | self.my_turn = None 43 | self.game_id = None 44 | self.status = None 45 | self.color = None 46 | self.fen = None 47 | 48 | @util.log_func_failure("couldn't start self.handle_client in new thread") 49 | def start(self): 50 | t = Thread(target=self.handle_client) 51 | t.start() 52 | 53 | @util.log_func_failure("socket error occured") 54 | def handle_client(self): 55 | exit_ = False 56 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 57 | s.bind((HOST, self.port)) 58 | while not exit_: 59 | s.listen() 60 | conn, _ = s.accept() 61 | with conn: 62 | while True: 63 | data = conn.recv(1024) 64 | if not data: 65 | break 66 | 67 | data = data.decode("utf-8") 68 | 69 | if data == "": 70 | util.log_message(f"function handle_client: received kill signal") 71 | exit_ = True 72 | break 73 | 74 | response = self.parse_data(data) 75 | conn.sendall(response) 76 | 77 | self.post_response_update(data) 78 | 79 | util.log_message(f"function handle_client: socket closed") 80 | 81 | @util.log_func_failure("could not update attributes") 82 | def post_response_update(self, data): 83 | tdiff = time.time() - self._update_dict['chat']['last'] 84 | if data == "get_all_info" and tdiff > self._update_dict['chat']['freq']: 85 | self._update_dict['chat']['last'] = time.time() 86 | if self.game_id is not None: 87 | self._update_chat(self.client, self.game_id) 88 | else: 89 | self.chat_messages = None 90 | 91 | @util.log_func_failure("could not parse data") 92 | def parse_data(self, data): 93 | response = b"success" 94 | 95 | # new move 96 | if data.startswith(""): 97 | response = self.make_move(data) 98 | elif data == "get_move": 99 | response = f"{self.next_move}".encode() 100 | # game_id 101 | elif data.startswith(""): 102 | self.game_id = data.replace("", "") 103 | elif data == "get_game_id": 104 | response = f"{self.game_id}".encode() 105 | # is it my turn? 106 | elif data.startswith(""): 107 | self.my_turn = data.replace("", "") == "True" 108 | elif data == "get_my_turn": 109 | response = f"{self.my_turn}".encode() 110 | # current fen 111 | elif data.startswith(""): 112 | response = self.set_fen(data) 113 | elif data == "get_fen": 114 | response = f"{self.fen}".encode() 115 | # latest move 116 | elif data.startswith(""): 117 | response = self.set_latest_move(data) 118 | elif data == "get_latest_move": 119 | response = f"{self.latest_move}".encode() 120 | # current game status 121 | elif data.startswith(""): 122 | self.status = data.replace("", "") 123 | elif data == "get_status": 124 | response = f"{self.status}".encode() 125 | # what's my color 126 | elif data.startswith(""): 127 | self.color = data.replace("", "") 128 | elif data == "get_color": 129 | response = f"{self.color}".encode() 130 | # player info: ----- 131 | elif data.startswith(""): 132 | self.player_info = data.replace("", "") 133 | elif data == "get_player_info": 134 | response = f"{self.player_info}".encode() 135 | # current chat messages 136 | elif data == "get_chat_messages": 137 | response = f"{self.chat_messages}".encode() 138 | # time left of each player 139 | elif data.startswith(""): 140 | self.player_times = data.replace("", "") 141 | elif data == "get_player_times": 142 | response = self.get_player_times() 143 | # fen of last move in game which can't be obtained using game dict 144 | elif data.startswith(""): 145 | response = self.process_last_fen(data) 146 | # write a chat message 147 | elif data.startswith(""): 148 | response = self.write_chat_message(data) 149 | # signal that new game should be started 150 | elif data.startswith(""): 151 | response = self.handle_start_new_game(data) 152 | elif data == "get_start_new_game": 153 | response = f"{self.start_new_game}/{self.game_params}".encode() 154 | # abort game 155 | elif data == "abort_game": 156 | response = self.abort_game() 157 | # resign game 158 | elif data == "resign_game": 159 | response = self.resign_game() 160 | # claim a victory if opponent left game 161 | elif data == "claim_victory": 162 | response = self.claim_victory() 163 | # make/accept/decline draw offer 164 | elif data.startswith(""): 165 | response = self.draw_offer(data) 166 | # make/accept/decline takeback offer 167 | elif data.startswith(""): 168 | response = self.takeback_offer(data) 169 | # get current game dict 170 | elif data == "get_game_dict": 171 | response = self.get_game_dict() 172 | # occured errors 173 | elif data.startswith(""): 174 | self.last_err = data.replace("", "") 175 | # all needed info to display board and player info in vim 176 | elif data.startswith(""): 177 | response = self.parse_all_info(data) 178 | elif data == "get_all_info": 179 | response = self.get_all_info() 180 | # unknown request 181 | else: 182 | response = b"unknown request" 183 | util.log_message(f"function parse_data: unknown request: {data}", 2) 184 | 185 | util.log_message( 186 | f"function parse_data: new request: {data} - RESPONSE: {response}" 187 | ) 188 | 189 | return response 190 | 191 | @util.log_func_failure(_parse_make_move_exception) 192 | def make_move(self, data): 193 | move = data.replace("", "") 194 | 195 | try: 196 | self.client.board.make_move(self.game_id, move=move) 197 | except Exception as e: 198 | if "not your turn" in str(e).lower() and not self.my_turn: 199 | self.premove = move 200 | return f"premoving {move}".encode() 201 | else: 202 | raise e 203 | 204 | return b"success" 205 | 206 | @util.log_func_failure("could not parse all info") 207 | def parse_all_info(self, data): 208 | data = data.replace("", "") 209 | self.status, my_turn, fen, self.player_times, latest_move = data.split( 210 | util.MSG_SEP 211 | ) 212 | self.my_turn = my_turn == "True" 213 | self.latest_move = latest_move if len(latest_move) > 0 else None 214 | 215 | if self.premove is not None and my_turn: 216 | self.make_move(f"{self.premove}") 217 | self.premove = None 218 | 219 | return self.set_fen(fen) 220 | 221 | @util.log_func_failure("could not set fen") 222 | def set_fen(self, data): 223 | if self.color == 'white': 224 | self.fen = data.replace("", "") 225 | elif self.color == 'black': 226 | self.fen = util.flip_fen(data.replace("", "")) 227 | else: 228 | return b"unknown color" 229 | 230 | return b"success" 231 | 232 | @util.log_func_failure("could not set latest move") 233 | def set_latest_move(self, data): 234 | move = data.replace("", "") 235 | self.latest_move = move if len(move) > 0 else None 236 | return b"success" 237 | 238 | @util.log_func_failure("could not process last fen") 239 | def process_last_fen(self, data): 240 | self.fen = data.replace("", "") 241 | if self.color == "black": 242 | self.fen = util.flip_fen(self.fen) 243 | 244 | return b"success" 245 | 246 | @util.log_func_failure("could not write chat message") 247 | def write_chat_message(self, data): 248 | msg = data.replace("", "") 249 | if self.game_id is not None: 250 | self.client.board.post_message(self.game_id, msg) 251 | return b"success" 252 | 253 | return b"could not write text message" 254 | 255 | @util.log_func_failure("could not get game dict") 256 | def get_game_dict(self): 257 | game = util.get_current_game(self.client) 258 | if game is not None: 259 | if self.color == "black": 260 | game["fen"] = util.flip_fen(game["fen"]) 261 | 262 | response = f"{game}".encode() 263 | self.last_game = game 264 | elif self.last_game is not None: 265 | response = f"{self.last_game}".encode() 266 | else: 267 | response = b"could not get game dict - no game found" 268 | 269 | return response 270 | 271 | @util.log_func_failure("could not get player times") 272 | def get_player_times(self): 273 | if self.player_times is not None: 274 | thentime = datetime.strptime( 275 | self.player_times.split("/")[0], "%Y-%m-%d-%H:%M:%S.%f" 276 | ) 277 | td = (datetime.now() - thentime).total_seconds() 278 | my_time = float(self.player_times.split("/")[1].split("-")[0]) 279 | opp_time = float(self.player_times.split("/")[1].split("-")[1]) 280 | return f"{td}/{my_time}-{opp_time}".encode() 281 | 282 | return f"{self.player_times}".encode() 283 | 284 | @util.log_func_failure("could not get all info") 285 | def get_all_info(self): 286 | if self.player_times is not None: 287 | thentime = datetime.strptime( 288 | self.player_times.split("/")[0], "%Y-%m-%d-%H:%M:%S.%f" 289 | ) 290 | td = (datetime.now() - thentime).total_seconds() 291 | my_time = float(self.player_times.split("/")[1].split("-")[0]) 292 | opp_time = float(self.player_times.split("/")[1].split("-")[1]) 293 | times_parsed = f"{td}/{my_time}-{opp_time}" 294 | else: 295 | times_parsed = None 296 | 297 | all_info = { 298 | "color": self.color, 299 | "messages": self.chat_messages, 300 | "player_info": self.player_info, 301 | "player_times": times_parsed, 302 | "is_my_turn": self.my_turn, 303 | "latest_move": self.latest_move, 304 | "status": self.status, 305 | "username": self.username, 306 | "fen": self.fen, 307 | "msg_sep": util.MSG_SEP, 308 | "last_err": self.last_err, 309 | "searching_game": self.start_new_game, 310 | } 311 | 312 | self.last_err = None 313 | 314 | return f"{all_info}".encode() 315 | 316 | @util.log_func_failure("could not resign game") 317 | def resign_game(self): 318 | if self.game_id is not None: 319 | self.client.board.resign_game(self.game_id) 320 | return b"success" 321 | else: 322 | util.log_message(f"function resign_game: no game id found!", 1) 323 | return b"could not resign game - no game id found" 324 | 325 | @util.log_func_failure("could not abort game") 326 | def abort_game(self): 327 | if self.game_id is not None: 328 | self.client.board.abort_game(self.game_id) 329 | return b"success" 330 | else: 331 | util.log_message(f"function abort_game: no game id found!", 1) 332 | return b"could not abort game - no game id found" 333 | 334 | @util.log_func_failure("could not set start new game signal") 335 | def handle_start_new_game(self, data): 336 | data = data.replace("", "") 337 | # parameter order: 338 | # time (integer in minutes) 339 | # increment (integer in seconds) 340 | # rated = False 341 | # variant = "standard" 342 | # color = "random" 343 | # rating_range = None (can be passed as [low, high]) 344 | start_new_game, game_params = data.split("/") 345 | start_new_game = start_new_game == "True" 346 | 347 | if game_params != "None": 348 | self.game_params = game_params 349 | 350 | self.start_new_game = start_new_game 351 | 352 | util.log_message( 353 | "function handle_start_new_game: start_new_game: " 354 | f"{start_new_game} (parsed: {self.start_new_game})," 355 | f" params: {self.game_params}", 356 | 0, 357 | ) 358 | 359 | return b"success" 360 | 361 | @util.log_func_failure("could not claim victory") 362 | def claim_victory(self): 363 | path = f"/api/board/game/{self.game_id}/claim-victory" 364 | response = self.client.board._r.post( 365 | path, data=None, fmt=berserk.formats.TEXT, stream=False 366 | ) 367 | response = json.loads(response) 368 | 369 | if "ok" in response.keys(): 370 | util.log_message("function claim_victory: ok", 0) 371 | return b"success" 372 | elif "error" in response.keys(): 373 | util.log_message(f"function claim_victory error: {response['error']}", 2) 374 | return f"{response['error']}".encode() 375 | 376 | @util.log_func_failure("could not handle draw offer") 377 | def draw_offer(self, data): 378 | accept = data.replace("", "") 379 | path = f"/api/board/game/{self.game_id}/draw/{accept}" 380 | response = self.client.board._r.post( 381 | path, data=None, fmt=berserk.formats.TEXT, stream=False 382 | ) 383 | response = json.loads(response) 384 | 385 | if "ok" in response.keys(): 386 | util.log_message("function draw_offer: ok", 0) 387 | return b"success" 388 | elif "error" in response.keys(): 389 | util.log_message(f"function draw_offer error: {response['error']}", 2) 390 | return f"{response['error']}".encode() 391 | 392 | @util.log_func_failure("could not handle takeback offer") 393 | def takeback_offer(self, data): 394 | accept = data.replace("", "") 395 | path = f"/api/board/game/{self.game_id}/takeback/{accept}" 396 | response = self.client.board._r.post( 397 | path, data=None, fmt=berserk.formats.TEXT, stream=False 398 | ) 399 | response = json.loads(response) 400 | 401 | if "ok" in response.keys(): 402 | util.log_message("function takeback_offer: ok", 0) 403 | return b"success" 404 | elif "error" in response.keys(): 405 | util.log_message(f"function takeback_offer error: {response['error']}", 2) 406 | return f"{response['error']}".encode() 407 | 408 | @util.log_func_failure("could not fetch game chat") 409 | def _update_chat(self, client, game_id): 410 | path = f"api/board/game/{game_id}/chat" 411 | response = client.board._r.get( 412 | path, data=None, fmt=berserk.formats.TEXT, stream=False 413 | ) 414 | response = json.loads(response) 415 | chat = util.MSG_SEP.join([f'{msg["user"]}: {msg["text"]}' for msg in response]) 416 | self.chat_messages = chat 417 | 418 | @util.log_func_failure("could not fetch game chat") 419 | def _update_fen(self, client): 420 | game = util.get_current_game(client) 421 | if game is not None: 422 | self.set_fen(game["fen"]) 423 | -------------------------------------------------------------------------------- /python/util.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | import os 4 | import re 5 | 6 | from datetime import datetime 7 | 8 | 9 | PLUGIN_PATH = os.path.normpath(os.path.split(os.path.realpath(__file__))[0] + os.sep + os.pardir) 10 | HOST = socket.gethostname() 11 | MSG_SEP = "-,-/ß/-,-" 12 | 13 | 14 | _debug_file = os.path.join(PLUGIN_PATH, ".debug_level") 15 | if not os.path.isfile(_debug_file): 16 | DEBUG_LEVEL = -1 17 | else: 18 | try: 19 | with open(_debug_file, "r") as f: 20 | DEBUG_LEVEL = int(f.read()) 21 | assert DEBUG_LEVEL in [0, 1, 2, 3], "debug level must be 0, 1, 2 or 3" 22 | except Exception as e: 23 | DEBUG_LEVEL = -1 24 | 25 | 26 | def log_message(message, level=0): 27 | if level < DEBUG_LEVEL or DEBUG_LEVEL == -1: 28 | return 29 | elif level == 0: 30 | prefix = "[INFO]" 31 | elif level == 1: 32 | prefix = "[WARNING]" 33 | elif level == 2: 34 | prefix = "[ERROR]" 35 | elif level == 3: 36 | prefix = "[CRASH]" 37 | else: 38 | raise ValueError("level must be 0, 1, 2 or 3") 39 | 40 | date = datetime.now().strftime("%Y_%m_%d") 41 | current_time = datetime.now().strftime("%H:%M:%S.%f") 42 | 43 | log_dir = os.path.join(PLUGIN_PATH, "log") 44 | if not os.path.isdir(log_dir): 45 | os.mkdir(log_dir) 46 | 47 | with open(os.path.join(log_dir, date + ".log"), "a+") as f: 48 | f.write(f"{prefix} {current_time}: {message}\n") 49 | 50 | 51 | def log_func_failure(handle_failure): 52 | """ decorator which logs occuring errors using the `log_message` function 53 | with the function name, the given error_message and the error string """ 54 | 55 | def log_decorator(func): 56 | def wrapper(*args, **kwargs): 57 | try: 58 | return func(*args, **kwargs) 59 | except Exception as e: 60 | if isinstance(handle_failure, str): 61 | log_message(f"{func.__name__}: {handle_failure}: {str(e)}", 2) 62 | return f"{handle_failure}".encode() 63 | elif callable(handle_failure): 64 | return handle_failure(e) 65 | else: 66 | raise TypeError('Unexpected argument type for `handle_failure`') 67 | 68 | return wrapper 69 | 70 | return log_decorator 71 | 72 | 73 | def is_port_in_use(port): 74 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 75 | return s.connect_ex((HOST, port)) == 0 76 | 77 | 78 | def query_server(query, port, max_tries=10): 79 | for _ in range(max_tries): 80 | try: 81 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 82 | s.connect((HOST, port)) 83 | s.sendall(query.encode()) 84 | response = s.recv(1024) 85 | 86 | return response 87 | except ConnectionRefusedError as e: 88 | log_message(f"connection refused (query: {query}): {str(e)}", 2) 89 | time.sleep(0.1) 90 | 91 | if not is_port_in_use(port): 92 | raise ConnectionRefusedError(f"port {port} is not in use anymore") 93 | 94 | 95 | def flip_fen(fen): 96 | fen_split = fen.split(" ") 97 | fen_pos, fen_rest = fen_split[0], fen_split[1:] 98 | pos_flipped = "/".join([el[::-1] for el in fen_pos.split("/")[::-1]]) 99 | return f"{pos_flipped} {' '.join(fen_rest)}" 100 | 101 | 102 | def get_current_game(client): 103 | all_games = client.games.get_ongoing() 104 | if not len(all_games) == 1: 105 | return None 106 | 107 | return all_games[0] 108 | 109 | 110 | def fen_to_board(fen): 111 | rows = fen.split(" ")[0].split("/") 112 | num = [str(i) for i in range(1, 9)] 113 | board = [ 114 | list("".join([("0" * int(p) if p in num else p) for p in row])) for row in rows 115 | ] 116 | 117 | return board 118 | 119 | 120 | def board_to_fen(board): 121 | new_fen = [] 122 | for row in board: 123 | num_before = False 124 | new_row = "" 125 | n = 0 126 | for el in row: 127 | if el == "0": 128 | n += 1 129 | num_before = True 130 | elif num_before: 131 | new_row += str(n) + el 132 | n = 0 133 | num_before = False 134 | else: 135 | new_row += el 136 | 137 | if num_before: 138 | new_row += str(n) 139 | 140 | new_fen.append(new_row) 141 | 142 | return "/".join(new_fen) 143 | 144 | 145 | def change_fen_last_move(fen, last_move): 146 | fen_split = fen.split(" ") 147 | fen_pos, fen_rest = fen_split[0], fen_split[1:] 148 | color = fen_rest[0][0] 149 | 150 | m_row = re.match(r"[a-z](\d)[a-z](\d)[a-z]*", last_move) 151 | m_idx = re.match(r"([a-z])\d([a-z])\d([a-z]*)", last_move) 152 | 153 | assert ( 154 | m_row is not None and m_idx is not None 155 | ), f"last move could not be parsed! (last_move: {last_move} - type:{type(last_move)})" 156 | 157 | letter_idx_map = {"a": 0, "b": 1, "c": 2, "d": 3, "e": 4, "f": 5, "g": 6, "h": 7} 158 | 159 | row_before_i, row_after_i = m_row.groups() 160 | row_before_i, row_after_i = 8 - int(row_before_i), 8 - int(row_after_i) 161 | idx_before_i, idx_after_i, new_piece = m_idx.groups() 162 | idx_before, idx_after = letter_idx_map[idx_before_i], letter_idx_map[idx_after_i] 163 | 164 | board = fen_to_board(fen) 165 | 166 | if len(new_piece): 167 | moved_piece = new_piece.upper() if color == "w" else new_piece.lower() 168 | else: 169 | moved_piece = board[row_before_i][idx_before] 170 | 171 | board[row_before_i][idx_before] = "0" 172 | board[row_after_i][idx_after] = moved_piece 173 | 174 | if last_move == "e1g1" or last_move == "e8g8": 175 | board[row_before_i][-1] = "0" 176 | board[row_before_i][5] = "R" if color == "w" else "r" 177 | elif last_move == "e1c1" or last_move == "e8c8": 178 | board[row_before_i][0] = "0" 179 | board[row_before_i][3] = "R" if color == "w" else "r" 180 | 181 | fen_pos = board_to_fen(board) 182 | return f"{fen_pos} {' '.join(fen_rest)}" 183 | --------------------------------------------------------------------------------