diff --git a/.gitignore b/.gitignore index ce7b164..03e76dc 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ l10n/ launch.json venv *.db -Client/Data/profiles.json +Client/Data/profiles.json \ No newline at end of file diff --git a/Client/client.plantuml b/Client/client.plantuml deleted file mode 100644 index f7fe1b4..0000000 --- a/Client/client.plantuml +++ /dev/null @@ -1,48 +0,0 @@ -@startuml Client -!pragma useIntermediatePackages false - -class Client.client.GameClient { - - _ip: str - - _port: int - - _player: Player - - _player_number: int - - _symbol: str - - _opponent: Player - - _opponent_number: int - - _current_player: Player - - _lobby_status: list[str] - - _playfield: list[list[int]] - - _statistics: Statistics - - _chat_history: list[tuple[Player, str]] - - _winner: Player - - _error_history: list[str] - - _json_schema: Any - + __init__(ip:str, port:int, player:Player): None - - connect(): None - + create_game(player: Player, port:int = 8765): tuple[GameClient, asyncio.Task, Thread] - + join_game(player: Player, ip:str, port:int = 8765): tuple[GameClient, asyncio.Task] - + listen(): None - + get_player_by_uuid(uuid:str): Player - - _preprocess_message(message:str): str - - _message_handler(message_type: str): None - + join_lobby(): None - + lobby_ready(ready:bool = True): None - + lobby_kick(player_to_kick_index:int): None - + game_make_move(x:int, y:int): None - + chat_message(message:str): None - + close(): None -} - -class Client.ui_client.GameClientUI { - - _tk_root: tk.Tk - - _in_queue: queue.Queue - - _out_queue: queue.Queue - + join_game(player: Player, ip: str, tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, port: int = 8765): tuple[GameClientUI, asyncio.Task, asyncio.Task] - - listen(): None - - _message_handler(message_type: str): None - - await_commands(): None -} - -Client.client.GameClient <|-- Client.ui_client.GameClientUI -@enduml - diff --git a/README.md b/README.md index 47b0c98..eb89867 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# TicTacToeKojote \ No newline at end of file +# TicTacToeKojote + +This is a simple TicTacToe game written in Python. IT can be played by two players in a GUI. However, it only works on Windows. + +## Installation +- Install Python 3.11 or higher from [python.org](https://www.python.org/downloads/) +- Install the required packages by running `pip install -r requirements.txt` from the root directory of the project +- Run the game by executing `python main.py` from the root directory of the project + +## Features +- Play TicTacToe with a friend online +- Play TicTacToe with a friend offline +- Play TicTacToe against an AI (weak or strong) +- Chat with your opponent during the game +- Create Profiles +- View your game statistics whenever you are connected to a server + +## How to play +- **Manage your player profiles:** View Use Case 1: Manage Player Profiles +- **Play against AI:** View the corresponding [sequence diagram](docs/sequence_diagrams/play_vs_ai.png) +- **Play against another player locally:** View the corresponding [sequence diagram](docs/sequence_diagrams/play_locally.png) +- **Host an online game:** View Use Case 3.1: Host Game +- **Join an online game:** View Use Case 3.2: Join Game +- **Leave an online game:** View Use Case 3.3: Leave Game +- **Send Chat messages:** View Use Case 4: Chat +- **Display the statistics:** View Use Case 5: Display Statistics \ No newline at end of file diff --git a/docs/class_diagrams/AI.png b/docs/class_diagrams/AI.png new file mode 100644 index 0000000..eb3dcfa Binary files /dev/null and b/docs/class_diagrams/AI.png differ diff --git a/docs/class_diagrams/AI.puml b/docs/class_diagrams/AI.puml new file mode 100644 index 0000000..b26c31d --- /dev/null +++ b/docs/class_diagrams/AI.puml @@ -0,0 +1,75 @@ +@startuml AI +!pragma useIntermediatePackages false + +class AI.ai_context.AIContext { + - _strategy: AIStrategy + + + AIContext(strategy: ai_strategy.AIStrategy) + + None set_strategy(strategy: ai_strategy.AIStrategy) + + None run_strategy() +} +class AI.ai_rulebase.AIRulebase { + + check_win(state: Server.game_state.GameState) +} +abstract class AI.ai_strategy.AIStrategy { + - _strength: str + - _good_luck_message: str + - _good_game_message_lost: str + - _good_game_message_won: str + - _good_game_message_draw: str + - _current_uuid: str + - _rulebase: AI.ai_rulebase.AIRulebase + - _ip: str + - _port: int + + + AIStrategy() + - post_init() + + None thread_entry() + + None run() + + None join_game() + - _message_handler(message_type: str) + - wish_good_luck() + - say_good_game() + - list[list[int]] get_empty_cells(game_status: list[list[int]]) + + None do_turn() +} + +class AI.ai_strategy.AdvancedAIStrategy { + - _current_uuid: str + - _strength: str + - _good_luck_message: str + - _good_game_message_lost: str + - _good_game_message_won: str + - _good_game_message_draw: str + - _player: Server.player.Player + + + AdvancedAIStrategy(uuid: str = 'd90397a5-a255-4101-9864-694b43ce8a6c') + + None do_turn() +} + +class AI.ai_strategy.WeakAIStrategy { + - _current_uuid: str + - _strength: str + - _good_luck_message: str + - _good_game_message_lost: str + - _good_game_message_won: str + - _good_game_message_draw: str + - _player: Server.player.Player + + + WeakAIStrategy(uuid: str = '108eaa05-2b0e-4e00-a190-8856edcd56a5') + - check_winning_move(empty_cells: list[list[int]], player:int) + + None do_turn() +} + +AI.ai_context.AIContext *-- AI.ai_strategy.AIStrategy +AI.ai_strategy.AIStrategy <|-- AI.ai_strategy.AdvancedAIStrategy +AI.ai_strategy.AIStrategy <|-- AI.ai_strategy.WeakAIStrategy +AI.ai_strategy.AIStrategy *-- AI.ai_rulebase.AIRulebase +AI.ai_strategy.AIStrategy --|> Client.client.GameClient +AI.ai_rulebase.AIRulebase --|> Server.rulebase.Rulebase + +AI.ai_strategy.AdvancedAIStrategy *-- Server.player.Player +AI.ai_strategy.WeakAIStrategy *-- Server.player.Player + +@enduml + diff --git a/docs/class_diagrams/Client.png b/docs/class_diagrams/Client.png new file mode 100644 index 0000000..cf9e467 Binary files /dev/null and b/docs/class_diagrams/Client.png differ diff --git a/docs/class_diagrams/Server.png b/docs/class_diagrams/Server.png new file mode 100644 index 0000000..a84f5f4 Binary files /dev/null and b/docs/class_diagrams/Server.png differ diff --git a/docs/class_diagrams/client.puml b/docs/class_diagrams/client.puml new file mode 100644 index 0000000..1d732cf --- /dev/null +++ b/docs/class_diagrams/client.puml @@ -0,0 +1,64 @@ +@startuml Client +!pragma useIntermediatePackages false + +class Client.client.GameClient { + - _ip: str + - _port: int + - _player: Player + - _player_number: int + - _symbol: str + - _opponent: Player + - _opponent_number: int + - _starting_player: Player + - _current_player: Player + - _lobby_status: list[str] + - _playfield: list[list[int]] + - _statistics: Server.statistics.Statistics + - _chat_history: list[tuple[Player, str]] + - _winner: Player + - _error_history: list[str] + - _json_schema: Any + + + None GameClient(ip:str, port:int, player:Player) + - None connect() + + tuple[GameClient, asyncio.Task, Thread] create_game(player: Player, port:int = 8765) + + tuple[GameClient, asyncio.Task] join_game(player: Player, ip:str, port:int = 8765) + + None listen() + + Player get_player_by_uuid(uuid:str) + - None _preprocess_message(message:str) + - None _message_handler(message_type: str) + + None join_lobby() + + None lobby_ready(ready:bool = True) + + None lobby_kick(player_to_kick_index:int) + + None game_make_move(x:int, y:int) + + None chat_message(message:str) + + None close() + + None terminate() +} + +class Client.ui_client.GameClientUI { + - _tk_root: tk.Tk + - _in_queue: queue.Queue + - _out_queue: queue.Queue + + + None GameClientUI(ip:str, port:int, player:Player, tk_root:tk.Tk, out_queue:Queue, in_queue:Queue) + + tuple[GameClientUI, asyncio.Task, asyncio.Task] join_game(player: Player, ip: str, tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, port: int = 8765) + - None listen() + - None _message_handler(message_type: str) + - None await_commands() + - None send_gamestate_to_ui() +} + +class Client.profile_save.Profile { + + list[Player] get_profiles() + + None set_profiles(players:list[Player], selected:int) + + None delete_all_profiles() +} + +class Server.statistics.Statistics { +} + +Client.client.GameClient <|-- Client.ui_client.GameClientUI +Client.client.GameClient *-- Server.statistics.Statistics +@enduml + diff --git a/docs/class_diagrams/server.puml b/docs/class_diagrams/server.puml new file mode 100644 index 0000000..ba41fef --- /dev/null +++ b/docs/class_diagrams/server.puml @@ -0,0 +1,90 @@ +@startuml Server +!pragma useIntermediatePackages false + +class Server.game.Game { + - _uuid: UUID + - _id: int + + state: Server.gamestate.GameState + + players: list[Server.player.Player] + + rule_base: RuleBase + + + Game(player1: Player, player2: Player, rule_base: RuleBase = RuleBase()) + + None move(player: int, new_position: tuple[int, int]) + + str current_player_uuid() + + Server.player.Player winner() +} +class Server.gamestate.GameState { + - _playfield: list[list[int]] + - _finished: bool + - _winner: int + - _current_player: int + + + GameState(playfield_dimensions: tuple[int, int] = (3,3)) + + None set_player_position(player: int, new_position: tuple[int, int]) + + None set_winner(winner: int) + + int winner() + + bool finished() + + list[list[int]] playfield() + + int playfield_value(position: tuple[int, int]) + + tuple[int, int] playfield_dimensions() + + int current_player() +} +class Server.player.Player { + + uuid: UUID + + display_name: str + + color: int + + ready: bool + + + Player(display_name: str, color: int, uuid: UUID = uuid4(), ready:bool = False) + + dict[str, Any] as_dict() + + Player from_dict(data: dict[str, Any]) +} +class Server.rulebase.RuleBase { + - _playfield_dimensions: tuple[int, int] + + + RuleBase(playfield_dimensions: tuple[int, int] = (3,3)) + + bool is_move_valid(state: GameState, new_position: tuple[int, int]) + + None check_win(state: GameState) + + bool is_game_state_valid(state: GameState) + + list[list[int]] transpose(matrix: list[list[int]]) +} +class Server.websocket_server.Lobby { + - _players: dict[str, Server.player.Player] + - _game: Server.game.Game + - _inprogress: bool + - _port: int + - _connections: list[websockets.server.WebSocketServerProtocol] + - _stats: Server.statistics.Statistics + - _json_schema: dict[str, Any] + + + Lobby(admin:Player, port: int = 8765) + + None handler(websocket: websockets.server.WebSocketServerProtocol) + - None _end_game() + + None start_server() + + None run() +} +class Server.statistics.Statistics { + - path: str + - conn: sqlite3.Connection + - cursor: sqlite3.Cursor + + + Statistics(path: str = os.path.abspath('Server/Data/statistics.db')) + + list[tuple[str, int]] get_statistics() + + None increment_emojis(player: Player, message: str) + + None increment_moves(player: Player) + + None increment_games(player_list: list[Player], winner: int) + - None _increment_win(player: Player) + - None _increment_loss(player: Player) + - None _increment_draws(player: Player) + - None _check_add_profile(player: Player) + - bool _check_profile(uuid_str: str) + - None _add_profile(player: Player) +} +Server.game.Game *-- Server.rulebase.RuleBase +Server.game.Game *-- Server.gamestate.GameState +Server.game.Game *-- Server.player.Player +Server.websocket_server.Lobby *-- Server.game.Game +Server.websocket_server.Lobby *-- Server.statistics.Statistics +Server.websocket_server.Lobby *-- Server.player.Player +@enduml + diff --git a/docs/patterns/observer.png b/docs/patterns/observer.png new file mode 100644 index 0000000..80896d2 Binary files /dev/null and b/docs/patterns/observer.png differ diff --git a/docs/patterns/observer.puml b/docs/patterns/observer.puml new file mode 100644 index 0000000..808543d --- /dev/null +++ b/docs/patterns/observer.puml @@ -0,0 +1,31 @@ +@startuml Server +!pragma useIntermediatePackages false + +class Server.websocket_server.Lobby { + - _connections: set[WebSocketServerProtocol] + + handler(websocket: WebSocketServerProtocol) +} + +class websockets.WebSocketServerProtocol { + + send(message: str) + + str recv() +} + +note left of Server.websocket_server.Lobby::handler + Whenever a new connection is made, the handler is called. + The handler adds the connection to the set of connections + (_connections) and removes it when the connection is closed. + Whenever a message is received, the handler uses + websockets.broadcast() to send the updated game state to all + connections in _connections. +end note + +note bottom of websockets.WebSocketServerProtocol + The WebSocketServerProtocol is a protocol that is used to + handle the WebSocket connection. It has methods to send and + receive messages, and it also has methods to handle the + opening and closing of the connection. +end note + +Server.websocket_server.Lobby o-- websockets.WebSocketServerProtocol +@enduml \ No newline at end of file diff --git a/docs/sequence_diagrams/create_profile.png b/docs/sequence_diagrams/create_profile.png new file mode 100644 index 0000000..7b21052 Binary files /dev/null and b/docs/sequence_diagrams/create_profile.png differ diff --git a/docs/sequence_diagrams/create_profile.puml b/docs/sequence_diagrams/create_profile.puml new file mode 100644 index 0000000..6cd1756 --- /dev/null +++ b/docs/sequence_diagrams/create_profile.puml @@ -0,0 +1,15 @@ +@startuml +title Create Player Profile + +actor Player +database Client + +Player->Client: Click Profile Button +Client->Player: Show Profile Creation Prompt +Player->Client: Set Name +Player->Client: Set Color +Player->Client: Click Create Profile Button +Client->Client: Save Profile on Disk +Client->Player: Show Profile + +@enduml \ No newline at end of file diff --git a/docs/sequence_diagrams/delete_profile.png b/docs/sequence_diagrams/delete_profile.png new file mode 100644 index 0000000..2d7a199 Binary files /dev/null and b/docs/sequence_diagrams/delete_profile.png differ diff --git a/docs/sequence_diagrams/delete_profile.puml b/docs/sequence_diagrams/delete_profile.puml new file mode 100644 index 0000000..c31db42 --- /dev/null +++ b/docs/sequence_diagrams/delete_profile.puml @@ -0,0 +1,13 @@ +@startuml +title Delete Player Profile + +actor Player +database Client + +Player->Client: Click Profile Button +Client->Player: Show Profile +Player->Client: Click Delete Profile Button +Client->Client: Delete Profile from Disk +Client->Player: Show Profile Creation Prompt + +@enduml \ No newline at end of file diff --git a/docs/sequence_diagrams/edit_profile.png b/docs/sequence_diagrams/edit_profile.png new file mode 100644 index 0000000..7c9b615 Binary files /dev/null and b/docs/sequence_diagrams/edit_profile.png differ diff --git a/docs/sequence_diagrams/edit_profile.puml b/docs/sequence_diagrams/edit_profile.puml new file mode 100644 index 0000000..f21d2d6 --- /dev/null +++ b/docs/sequence_diagrams/edit_profile.puml @@ -0,0 +1,16 @@ +@startuml +title Edit Player Profile + +actor Player +database Client + +Player->Client: Click Profile Button +Client->Player: Show Profile +Player->Client: Click Edit Profile Button +Player->Client: Change Name +Player->Client: Change Color +Player->Client: Click Edit Profile Button +Client->Client: Save Profile on Disk +Client->Player: Show Profile + +@enduml \ No newline at end of file diff --git a/docs/sequence_diagrams/host_game.png b/docs/sequence_diagrams/host_game.png new file mode 100644 index 0000000..5500aa6 Binary files /dev/null and b/docs/sequence_diagrams/host_game.png differ diff --git a/docs/sequence_diagrams/host_game.puml b/docs/sequence_diagrams/host_game.puml new file mode 100644 index 0000000..0479241 --- /dev/null +++ b/docs/sequence_diagrams/host_game.puml @@ -0,0 +1,15 @@ +@startuml +title Host Game + +actor Player as p +database Client + +p->Client: Click Multiplayer Button +Client->p: Display Multiplayer Menu +p->Client: Click Create a new online game Button +Client->Client: Host Server locally +Client->p: Display Multiplayer Lobby +Client->Network: Send advertisements +Client->Client: Wait for other players to join + +@enduml diff --git a/docs/sequence_diagrams/join_game.png b/docs/sequence_diagrams/join_game.png new file mode 100644 index 0000000..3a61c71 Binary files /dev/null and b/docs/sequence_diagrams/join_game.png differ diff --git a/docs/sequence_diagrams/join_game.puml b/docs/sequence_diagrams/join_game.puml new file mode 100644 index 0000000..eb84088 --- /dev/null +++ b/docs/sequence_diagrams/join_game.puml @@ -0,0 +1,21 @@ +@startuml +title Join Game + +actor Player as p +database Client +database Server as s + +p->Client: Click Multiplayer Button +Client->p: Display Multiplayer Menu +p->Client: Click Join by address Button +Client->p: Show IP-Address Input field +p->Client: Enter IP-Address +p->Client: Click Connect Button +Client->s: Join Lobby +s->Client: Send Server information +Client->p: Display Game Lobby +p->Client: Click Ready Button +Client->s: Get Ready +s->s: Wait for other Player to get ready + +@enduml diff --git a/docs/sequence_diagrams/leave_game.png b/docs/sequence_diagrams/leave_game.png new file mode 100644 index 0000000..afd9cd5 Binary files /dev/null and b/docs/sequence_diagrams/leave_game.png differ diff --git a/docs/sequence_diagrams/leave_game.puml b/docs/sequence_diagrams/leave_game.puml new file mode 100644 index 0000000..638a71b --- /dev/null +++ b/docs/sequence_diagrams/leave_game.puml @@ -0,0 +1,17 @@ +@startuml +title Leave Game + +actor Player as p +database Client +database Server as s +actor Player2 as p2 + +p->Client: Click Menu Button +Client->s: Player disconnected +Client->s: Close connection +Client->p: Show Main Menu +s->p2: You have won +s->p2: Close Connection +s->s: Terminate + +@enduml diff --git a/docs/sequence_diagrams/play_locally.png b/docs/sequence_diagrams/play_locally.png new file mode 100644 index 0000000..ee1f1dc Binary files /dev/null and b/docs/sequence_diagrams/play_locally.png differ diff --git a/docs/sequence_diagrams/play_locally.puml b/docs/sequence_diagrams/play_locally.puml new file mode 100644 index 0000000..2c657b8 --- /dev/null +++ b/docs/sequence_diagrams/play_locally.puml @@ -0,0 +1,15 @@ +@startuml +title Play locally + +actor Player as p +database Client + +p->Client: Click Multiplayer Button +Client->p: Display Multiplayer Menu +p->Client: Click Create local Game Button +Client->p: Display Profile selection screen +p->Client: Select Profiles for both players +p->Client: Click Start Game Button +Client->p: Display Playfield + +@enduml diff --git a/docs/sequence_diagrams/play_vs_ai.png b/docs/sequence_diagrams/play_vs_ai.png new file mode 100644 index 0000000..e20f61e Binary files /dev/null and b/docs/sequence_diagrams/play_vs_ai.png differ diff --git a/docs/sequence_diagrams/play_vs_ai.puml b/docs/sequence_diagrams/play_vs_ai.puml new file mode 100644 index 0000000..1111255 --- /dev/null +++ b/docs/sequence_diagrams/play_vs_ai.puml @@ -0,0 +1,21 @@ +@startuml +title Play versus AI + +actor Player as p +database Client +actor AI as a + +p->Client: Click Singleplayer Button +Client->p: Display AI Difficulty Selection +p->Client: Select AI Difficulty by clicking Strong or Weak AI Button +Client->Client: Host Server locally +Client->p: Display Game Lobby +Client->a: Create AI +a->Client: Connect to Server +a->Client: Get Ready +Client->p: Show AI is ready +p->Client: Click Ready Button +p->Client: Click Start Game Button +Client->p: Show Playfield + +@enduml diff --git a/docs/sequence_diagrams/send_chat.png b/docs/sequence_diagrams/send_chat.png new file mode 100644 index 0000000..729e678 Binary files /dev/null and b/docs/sequence_diagrams/send_chat.png differ diff --git a/docs/sequence_diagrams/send_chat.puml b/docs/sequence_diagrams/send_chat.puml new file mode 100644 index 0000000..f4d5de4 --- /dev/null +++ b/docs/sequence_diagrams/send_chat.puml @@ -0,0 +1,17 @@ +@startuml +title Send Chat Message + +actor Player as p +database Client +database Server as s +actor Player2 as p2 + +p->Client: Click on the message entry box +p->p: Enter message +p->Client: Click Send Button +Client->s: Send chat message +s->p2: Send chat message +s-> Client: Send chat message +Client->p: Display chat message + +@enduml diff --git a/docs/sequence_diagrams/view_profile.png b/docs/sequence_diagrams/view_profile.png new file mode 100644 index 0000000..b555904 Binary files /dev/null and b/docs/sequence_diagrams/view_profile.png differ diff --git a/docs/sequence_diagrams/view_profile.puml b/docs/sequence_diagrams/view_profile.puml new file mode 100644 index 0000000..304efd7 --- /dev/null +++ b/docs/sequence_diagrams/view_profile.puml @@ -0,0 +1,10 @@ +@startuml +title View Player Profile + +actor Player +database Client + +Player->Client: Click Profile Button +Client->Player: Show Profile + +@enduml \ No newline at end of file