from __future__ import annotations import random import typing as tp from abc import ABC, abstractmethod from operator import itemgetter class QuitException(Exception): pass class GameState(tp.Protocol): player: tp.Any class Move(tp.Protocol): pass class Player(tp.Protocol): pass Score = int class GameModule(tp.Protocol): @staticmethod def get_valid_moves(state: GameState) -> tp.Iterable[Move]: ... @staticmethod def apply_move(state: GameState, move: Move) -> GameState: ... @staticmethod def get_winner(state: GameState) -> Player | tp.Literal["draw"] | None: ... @staticmethod def get_next_states( state: GameState, ) -> tuple[tuple[Move, GameState], ...]: ... @staticmethod def get_score(target: Player, state: GameState) -> Player: ... @staticmethod def get_scored_moves( target: Player, state: GameState ) -> tuple[tuple[Score, Move], ...]: ... @staticmethod def get_human_move(state: GameState) -> Move: ... def _get_ai_move(mod: GameModule, state: GameState) -> Move: scored_moves = list(mod.get_scored_moves(state.player, state)) random.shuffle(scored_moves) _, best_move = max(scored_moves, key=itemgetter(0)) return best_move def play_human(mod: GameModule, human_player: Player, state: GameState) -> None: try: while not (winner := mod.get_winner(state)): print(state) if state.player == human_player: move = mod.get_human_move(state) else: move = _get_ai_move(mod, state) print(f"{state.player} plays {move}") print() state = mod.apply_move(state, move) print(state) if winner == "draw": print("draw") else: print(f"{winner} wins") except QuitException: print("quit") if __name__ == "__main__": import tictactoe as ttt play_human(ttt, "X", ttt.REAL)