From ee1ca2f653b8fef6b9c460825ed702aac2390fb2 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Mon, 25 Apr 2022 14:49:42 -0500 Subject: [PATCH] moved chess logic to class --- plugin/chess/__init__.py | 176 +---------------------------------- plugin/chess/game.py | 194 +++++++++++++++++++++++++++++++++++++++ plugin/chess/util.py | 41 +++++++++ 3 files changed, 238 insertions(+), 173 deletions(-) create mode 100644 plugin/chess/game.py create mode 100644 plugin/chess/util.py diff --git a/plugin/chess/__init__.py b/plugin/chess/__init__.py index 73a485e..920cae0 100644 --- a/plugin/chess/__init__.py +++ b/plugin/chess/__init__.py @@ -1,176 +1,6 @@ -from os import path -from enum import Enum, auto -import time -import re -import itertools -from icecream import ic -from cinput import ControlInput, Button +from cinput import ControlInput from graphics import Graphics -from .draw import Draw -from . import sunfish - -class GameState(Enum): - MAIN_MENU = auto() - THINKING = auto() - CHOOSE_SRC = auto() - CHOOSE_DST = auto() - GAME_OVER = auto() - -SAVE_FILE = path.join("data", "chess_state.fen") -MAX_THINKING_SECONDS = 3 +from .game import ChessGame def execute(cinput: ControlInput, graphics: Graphics, _): - graphics.clear() - draw = Draw(graphics) - hist: list[sunfish.Position] - searcher = sunfish.Searcher() - - if path.exists(SAVE_FILE): - with open(SAVE_FILE, "r") as fen: - fenstr = fen.read() - hist = [parseFEN(fenstr)] - else: - hist = [sunfish.Position( - sunfish.initial, 0, (True,True), (True,True), 0, 0)] - - player_color = 'wb'[get_color(hist[-1])] - draw.draw_board(hist[-1], player_color) - - state = GameState.MAIN_MENU - menu_index = 0 - move = None - score = None - last_move = "Begin!" - all_moves = dict() - src_idx = 0 - dst_idx = 0 - src = "" - - while True: - key = None - if state == GameState.MAIN_MENU: - draw.draw_menu(menu_index) - key = cinput.get_one_shot() - elif state == GameState.THINKING: - draw.draw_thinking(last_move) - start = time.time() - for _, move, score in searcher.search(hist[-1], hist): - if time.time() - start > MAX_THINKING_SECONDS: - break - hist.append(hist[-1].move(move)) - draw.draw_board(hist[-1], player_color) - last_move = move_str(move, player_color == "w") - all_moves = dict() - state = GameState.CHOOSE_SRC - elif state == GameState.CHOOSE_SRC: - if len(list(all_moves)) == 0: - all_moves = get_all_moves(hist[-1]) - src = list(all_moves)[src_idx] - draw.draw_select(last_move, list(all_moves)[src_idx]) - key = cinput.get_one_shot() - elif state == GameState.CHOOSE_DST: - src = list(all_moves)[src_idx] - draw.draw_select(last_move, src, all_moves[src][dst_idx]) - key = cinput.get_one_shot() - elif state == GameState.GAME_OVER: - key = cinput.get_one_shot() - - if state == GameState.MAIN_MENU: - if key == Button.DIR_U: - menu_index -= 1 - if menu_index < 0: - menu_index = 0 - elif key == Button.DIR_D: - menu_index += 1 - if menu_index > 3: - menu_index = 3 - elif key == Button.BTN_B: - if menu_index == 0: - state = GameState.CHOOSE_SRC - elif menu_index == 1: - with open(SAVE_FILE, "w") as fen: - fen.write(renderFEN(hist[-1])) - return - elif menu_index == 2 or menu_index == 3: - pos = sunfish.Position( - sunfish.initial, 0, (True,True), (True,True), 0, 0) - if menu_index == 3: - player_color = "b" - state = GameState.THINKING - else: - player_color = "w" - state = GameState.CHOOSE_SRC - hist = [pos] - draw.draw_board(hist[-1] if player_color == "w" else hist[-1].rotate(), player_color) - elif state == GameState.CHOOSE_SRC: - if key == Button.DIR_D or key == Button.DIR_R: - src_idx += 1 - if src_idx >= len(all_moves.keys()): - src_idx = 0 - elif key == Button.DIR_U or key == Button.DIR_L: - src_idx -= 1 - if src_idx < 0: - src_idx = len(list(all_moves)) - 1 - elif key == Button.BTN_A: - state = GameState.MAIN_MENU - elif key == Button.BTN_B: - state = GameState.CHOOSE_DST - elif state == GameState.CHOOSE_DST: - if key == Button.DIR_D or key == Button.DIR_R: - dst_idx += 1 - if dst_idx >= len(all_moves[src]): - dst_idx = 0 - elif key == Button.DIR_U or key == Button.DIR_L: - dst_idx -= 1 - if dst_idx < 0: - dst_idx = len(all_moves[src]) - 1 - elif key == Button.BTN_A: - state = GameState.CHOOSE_SRC - elif key == Button.BTN_B: - dst = all_moves[src][dst_idx] - last_move = src + " - " + dst - sfmove = sunfish.parse(src), sunfish.parse(dst) - hist.append(hist[-1].move(sfmove)) - draw.draw_board(hist[-1].rotate(), player_color) - state = GameState.THINKING - else: - if key == Button.BTN_A or key == Button.BTN_B: - state = GameState.MAIN_MENU - -def get_all_moves(pos: sunfish.Position): - all_moves = dict() - for src, dst in pos.gen_moves(): - all_moves.setdefault(sunfish.render(src), []).append(sunfish.render(dst)) - return all_moves - -def move_str(move, shift = False): - s = -119 if shift else 0 - return sunfish.render(s + move[0]) + " - " + sunfish.render(s + move[1]) - -def get_color(pos): - return 1 if pos.board.startswith('\n') else 0 - -def parseFEN(fen): - board, color, castling, enpas, _, _ = fen.split() - board = re.sub(r'\d', (lambda m: '.'*int(m.group(0))), board) - board = list(21*' ' + ' '.join(board.split('/')) + 21*' ') - board[9::10] = ['\n']*12 - board = ''.join(board) - wc = ('Q' in castling, 'K' in castling) - bc = ('k' in castling, 'q' in castling) - ep = sunfish.parse(enpas) if enpas != '-' else 0 - score = sum(sunfish.pst[p][i] for i,p in enumerate(board) if p.isupper()) - score -= sum(sunfish.pst[p.upper()][119-i] for i,p in enumerate(board) if p.islower()) - pos = sunfish.Position(board, score, wc, bc, ep, 0) - return pos if color == 'w' else pos.rotate() - -def renderFEN(pos, half_move_clock=0, full_move_clock=1): - color = 'wb'[get_color(pos)] - if color == 'b': - pos = pos.rotate() - board = '/'.join(pos.board.split()) - board = re.sub(r'\.+', (lambda m: str(len(m.group(0)))), board) - castling = ''.join(itertools.compress('KQkq', pos.wc[::-1]+pos.bc)) or '-' - ep = sunfish.render(pos.ep) if not pos.board[pos.ep].isspace() else '-' - clock = '{} {}'.format(half_move_clock, full_move_clock) - return ' '.join((board, color, castling, ep, clock)) + ChessGame(cinput, graphics).run() diff --git a/plugin/chess/game.py b/plugin/chess/game.py new file mode 100644 index 0000000..404745d --- /dev/null +++ b/plugin/chess/game.py @@ -0,0 +1,194 @@ +from os import path +from enum import Enum, auto +import time +from cinput import ControlInput, Button +from graphics import Graphics +from .draw import Draw +from . import sunfish +from . import util + +class GameState(Enum): + MAIN_MENU = auto() + THINKING = auto() + CHOOSE_SRC = auto() + CHOOSE_DST = auto() + GAME_OVER = auto() + +class ChessGame: + SAVE_FILE = path.join("data", "chess_state.fen") + MAX_THINKING_SECONDS = 3 + + def __init__(self, cinput: ControlInput, graphics: Graphics): + self._cinput = cinput + self._draw = Draw(graphics) + self._state = GameState.MAIN_MENU + self._menu_index = 0 + self._searcher = sunfish.Searcher() + + def _init_new_game(self, pos: sunfish.Position): + self._hist = [pos] + self._last_move = "Begin!" + self._all_moves = dict() + self._src_idx = 0 + self._dst_idx = 0 + self._draw.draw_board(self._hist[-1], self._player_color) + + def _load_saved_or_init(self): + if path.exists(self.SAVE_FILE): + with open(self.SAVE_FILE, "r") as fen: + fenstr = fen.read() + pos = util.parseFEN(fenstr) + self._player_color = util.get_color(pos) + self._init_new_game(pos) + else: + self._player_color = "w" + self._init_new_game(sunfish.Position( + sunfish.initial, 0, (True,True), (True,True), 0, 0)) + + def _save_game(self): + with open(self.SAVE_FILE, "w") as fen: + fen.write(util.renderFEN(self._hist[-1])) + + def run(self): + # either load the save game or start a new one + self._load_saved_or_init() + + while True: + key = None + move = None + src = "" + + ################ + # HANDLE STATE # + ################ + + # draw menu + if self._state == GameState.MAIN_MENU: + self._draw.draw_menu(self._menu_index) + key = self._cinput.get_one_shot() + + # computer makes a move + elif self._state == GameState.THINKING: + self._draw.draw_thinking(self._last_move) + start = time.time() + for _, self._move, self._score in self._searcher.search( + self._hist[-1], self._hist): + if time.time() - start > self.MAX_THINKING_SECONDS: + break + self._hist.append(self._hist[-1].move(self._move)) + self._draw.draw_board(self._hist[-1], self._player_color) + self._last_move = util.move_str(move, self._player_color == "w") + # reset user moves and move state to user's turn + self._all_moves = dict() + self._state = GameState.CHOOSE_SRC + + # user picks source piece + elif self._state == GameState.CHOOSE_SRC: + if len(list(self._all_moves)) == 0: + self._all_moves = util.get_all_moves(self._hist[-1]) + src = list(self._all_moves)[self._src_idx] + self._draw.draw_select(self._last_move, src) + key = self._cinput.get_one_shot() + + # user picks dest square + elif self._state == GameState.CHOOSE_DST: + src = list(self._all_moves)[self._src_idx] + dst = self._all_moves[src][self._dst_idx] + self._draw.draw_select(self._last_move, src, dst) + key = self._cinput.get_one_shot() + + # game has ended + elif self._state == GameState.GAME_OVER: + key = self._cinput.get_one_shot() + + ################ + # HANDLE INPUT # + ################ + + # handle user input on main menu + if self._state == GameState.MAIN_MENU: + + # menu cursor up and down + if key == Button.DIR_U: + self._menu_index -= 1 + if self._menu_index < 0: + self._menu_index = 0 + elif key == Button.DIR_D: + self._menu_index += 1 + if self._menu_index > 3: + self._menu_index = 3 + + # select current menu item + elif key == Button.BTN_B: + if self._menu_index == 0: # play current game + self._state = GameState.CHOOSE_SRC + elif self._menu_index == 1: # save and quit + self._save_game() + return + elif self._menu_index == 2 or self._menu_index == 3: # new game + pos = sunfish.Position( + sunfish.initial, 0, (True,True), (True,True), 0, 0) + if self._menu_index == 3: + self._player_color = "b" + self._state = GameState.THINKING + else: + self._player_color = "w" + self._state = GameState.CHOOSE_SRC + self._init_new_game(pos) + # TODO: figure out why I need to do this + self._draw.draw_board( + pos if self._player_color == "w" else pos.rotate(), self._player_color) + + # handle user input when selecting source piece + # TODO: some kind of cursor indicator on board + elif self._state == GameState.CHOOSE_SRC: + # move between source pieces + if key == Button.DIR_D or key == Button.DIR_R: + self._src_idx += 1 + if self._src_idx >= len(self._all_moves.keys()): + self._src_idx = 0 + elif key == Button.DIR_U or key == Button.DIR_L: + self._src_idx -= 1 + if self._src_idx < 0: + self._src_idx = len(list(self._all_moves)) - 1 + + # back out to main menu + elif key == Button.BTN_A: + self._state = GameState.MAIN_MENU + + # move to destination picker + elif key == Button.BTN_B: + self._state = GameState.CHOOSE_DST + + # handle user input when selecting dest piece + elif self._state == GameState.CHOOSE_DST: + # move between dest squares for the given source + if key == Button.DIR_D or key == Button.DIR_R: + self._dst_idx += 1 + if self._dst_idx >= len(self._all_moves[src]): + self._dst_idx = 0 + elif key == Button.DIR_U or key == Button.DIR_L: + self._dst_idx -= 1 + if self._dst_idx < 0: + self._dst_idx = len(self._all_moves[src]) - 1 + + # back out to choose a different source piece + elif key == Button.BTN_A: + self._dst_idx = 0 + self._state = GameState.CHOOSE_SRC + + # make the move + elif key == Button.BTN_B: + dst = self._all_moves[src][self._dst_idx] + # TODO: fix rotation depending on player color + self._last_move = src + " - " + dst + sfmove = sunfish.parse(src), sunfish.parse(dst) + self._hist.append(self._hist[-1].move(sfmove)) + self._draw.draw_board(self._hist[-1].rotate(), self._player_color) + self._state = GameState.THINKING + + # handle user input on game over + else: + if key == Button.BTN_A or key == Button.BTN_B: + self._state = GameState.MAIN_MENU + diff --git a/plugin/chess/util.py b/plugin/chess/util.py new file mode 100644 index 0000000..c6bc831 --- /dev/null +++ b/plugin/chess/util.py @@ -0,0 +1,41 @@ +import re +import itertools +from . import sunfish + +def get_all_moves(pos: sunfish.Position): + all_moves = dict() + for src, dst in pos.gen_moves(): + all_moves.setdefault(sunfish.render(src), []).append(sunfish.render(dst)) + return all_moves + +def move_str(move, shift = False): + s = -119 if shift else 0 + return sunfish.render(s + move[0]) + " - " + sunfish.render(s + move[1]) + +def get_color(pos): + return "wb"[1 if pos.board.startswith("\n") else 0] + +def parseFEN(fen): + board, color, castling, enpas, _, _ = fen.split() + board = re.sub(r"\d", (lambda m: "."*int(m.group(0))), board) + board = list(21*" " + " ".join(board.split("/")) + 21*" ") + board[9::10] = ["\n"]*12 + board = "".join(board) + wc = ("Q" in castling, "K" in castling) + bc = ("k" in castling, "q" in castling) + ep = sunfish.parse(enpas) if enpas != "-" else 0 + score = sum(sunfish.pst[p][i] for i,p in enumerate(board) if p.isupper()) + score -= sum(sunfish.pst[p.upper()][119-i] for i,p in enumerate(board) if p.islower()) + pos = sunfish.Position(board, score, wc, bc, ep, 0) + return pos if color == "w" else pos.rotate() + +def renderFEN(pos, half_move_clock=0, full_move_clock=1): + color = get_color(pos) + if color == "b": + pos = pos.rotate() + board = "/".join(pos.board.split()) + board = re.sub(r"\.+", (lambda m: str(len(m.group(0)))), board) + castling = "".join(itertools.compress("KQkq", pos.wc[::-1]+pos.bc)) or "-" + ep = sunfish.render(pos.ep) if not pos.board[pos.ep].isspace() else "-" + clock = "{} {}".format(half_move_clock, full_move_clock) + return " ".join((board, color, castling, ep, clock))