Skip to content

Commit

Permalink
Merge branch 'main' into UI-UX
Browse files Browse the repository at this point in the history
  • Loading branch information
HOOK-Hawkins committed Mar 17, 2024
2 parents cd650e4 + 9d220b5 commit 48ff2b9
Show file tree
Hide file tree
Showing 16 changed files with 462 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ package.nls.*.json
l10n/
launch.json
venv
*.db
Client/Data/profiles.json
3 changes: 3 additions & 0 deletions AI/ai_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
class AIContext():

"""
This is the context of the AI strategy pattern.
It holds the strategy and runs the strategy as a new thread.
It uses either the WeakAIStrategy or the AdvancedAIStrategy.
Use: AIContext(strategy: ai_strategy.AIStrategy)
-> Pass the strategy(WeakAIStrategy or StrongAIStrategy) to the AIContext, then call run_strategy() to run the strategy as a new thread.
It will then connect to localhost and play the game using the strategy.
Expand Down
49 changes: 44 additions & 5 deletions AI/ai_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
import copy
from uuid import UUID

logging.basicConfig(level=logging.DEBUG)
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class AIStrategy(ABC, GameClient):

"""
This is the abstract strategy class for the AI.
It implements as many methods as possible and leaves the actual logic to the inheriting classes.
This avoids code duplication and makes it easier to add new strategies.
"""

def __init__(self):

self._strength = "Placeholder"
Expand All @@ -34,19 +40,26 @@ def get_uuid(self):
return UUID(self._current_uuid)

def thread_entry(self):
"""
Entry point for the AI thread. Run the AI in asyncio.
"""
asyncio.run(self.run())

async def run(self):
"""
Start running the AI by joining a game and getting ready for the game.
"""

# The AI-UUID is hardcoded so that it can be excluded from statistics
await self.join_game()
asyncio.timeout(1)
await self.lobby_ready()
logger.info("test")

await self._listening_task

async def join_game(self):
"""
Join a game.
"""

await self.connect()
self._listening_task = asyncio.create_task(self.listen())
Expand All @@ -56,6 +69,9 @@ async def join_game(self):


async def _message_handler(self, message_type: str):
"""
Handle the incoming messages from the server.
"""

match message_type:
case "lobby/status":
Expand Down Expand Up @@ -88,9 +104,15 @@ async def _message_handler(self, message_type: str):
return

async def wish_good_luck(self):
"""
Send a good luck message to the chat on game start.
"""
await self.chat_message(self._good_luck_message)

async def say_good_game(self):
"""
Send a good game message to the chat, depending on the outcome of the game.
"""
if self._winner.uuid == self._current_uuid:
await self.chat_message(self._good_game_message_won)
elif self._winner.uuid == None:
Expand All @@ -114,7 +136,13 @@ async def do_turn(self):
pass

class WeakAIStrategy(AIStrategy):

"""
Weak AI Strategy:
The weak AI strategy is a simple AI that makes random moves.
It is one strategy in the strategy pattern for the AI.
"""


def __init__(self, uuid: str = '108eaa05-2b0e-4e00-a190-8856edcd56a5'):
super().__init__()
self._current_uuid = uuid
Expand All @@ -127,12 +155,23 @@ def __init__(self, uuid: str = '108eaa05-2b0e-4e00-a190-8856edcd56a5'):
self.post_init()

async def do_turn(self):
"""
Do one turn in the game.
Make the move on a random empty cell.
"""

empty_cells = self.get_empty_cells(self._playfield)
move = random.randint(0, len(empty_cells) - 1)
await self.game_make_move(empty_cells[move][0], empty_cells[move][1])

class AdvancedAIStrategy(AIStrategy):

"""
Advanced AI Strategy:
The advanced AI strategy is a more complex AI that tries to win the game.
It is one strategy in the strategy pattern for the AI.
"""

def __init__(self, uuid: str = 'd90397a5-a255-4101-9864-694b43ce8a6c'):
super().__init__()
self._current_uuid = uuid
Expand Down
20 changes: 12 additions & 8 deletions Client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class GameClient:
_current_player (Player): The player that is currently allowed to make a move.
_lobby_status (list[str]): The status of the lobby. Contains all players in the lobby.
_playfield (list[list[int]]): The status of the game. Contains the current playfield.
_statistics: The statistics of the game. TODO
_statistics (dict[Player:dict[str:int]] ): The statistics of the game.
_chat_history (list[tuple[Player, str]]): The chat history of the game. Contains all messages sent in the game.
_winner (Player): The winner of the game. None if the game is not finished yet or it is a draw.
_error_history (list[str]): The error history of the game. Contains all errors that occurred for this client.
Expand Down Expand Up @@ -71,7 +71,7 @@ def __init__(self, ip:str, port:int, player:Player) -> None:
self._current_player: Player = None
self._lobby_status: list[str] = []
self._playfield: list[list[int]] = [[0,0,0],[0,0,0],[0,0,0]]
self._statistics = None # TODO
self._statistics = {}
self._chat_history: list[tuple[Player, str]] = []
self._winner: Player = None
self._error_history: list[str] = []
Expand Down Expand Up @@ -101,7 +101,7 @@ async def create_game(cls, player: Player, port:int = 8765) -> tuple[GameClient,
"""

lobby = Lobby(port = port, admin = player)
server_thread = Thread(target=lobby.run, daemon=True) # TODO: Maybe remove daemon=True and add a proper shutdown function for database etc.
server_thread = Thread(target=lobby.run, daemon=True)
server_thread.start()

client = cls("localhost", port, player)
Expand Down Expand Up @@ -150,6 +150,9 @@ async def listen(self):

await self._message_handler(message_type)

if message_type == "game/end":
await self.terminate()

def get_player_by_uuid(self, uuid:str) -> Player:
for player in self._lobby_status:
if str(player.uuid) == uuid:
Expand Down Expand Up @@ -200,8 +203,9 @@ async def _preprocess_message(self, message:str) -> str:
self._playfield = message_json["updated_playfield"]
self._current_player = self.get_player_by_uuid(message_json["next_player_uuid"])
case "statistics/statistics":
# TODO: Add statistics handling
pass
sorted_statistics = sorted(message_json["server_statistics"], key=lambda x: x["player"]["display_name"])
for entry in sorted_statistics:
self._statistics[Player(**entry["player"])] = entry["statistics"]
case "game/error":
self._error_history.append(message_json["error_message"])
logger.error(f"Game error: {message_json['error_message']}")
Expand Down Expand Up @@ -304,10 +308,10 @@ async def close(self):

async def terminate(self):
msg = {
"message_type": "server/terminate"
"message_type": "server/terminate",
"player_uuid": str(self._player.uuid)
}
await self._websocket.send(json.dumps(msg))
await asyncio.sleep(0.1)
if self._websocket.open:
await self.close()
return
await self.close()
6 changes: 5 additions & 1 deletion Client/ui_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ async def _message_handler(self, message_type: str):
"message": self._chat_history[-1][1]
})
self._tk_root.event_generate("<<queue_input>>", when="tail")

case "lobby/kick":
self._out_queue.put({
"message_type": "lobby/kick",
})
self._tk_root.event_generate("<<queue_input>>", when="tail")
return

def send_gamestate_to_ui(self):
Expand Down
Empty file added Server/Data/.gitkeep
Empty file.
3 changes: 3 additions & 0 deletions Server/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def __eq__(self, other) -> bool:
return False
return str(self.uuid) == str(other.uuid)

def __hash__(self) -> int:
return hash(self.uuid)

@classmethod
def from_dict(cls, player_dict: dict):
return cls(player_dict["display_name"], player_dict["color"], UUID(player_dict["uuid"]), player_dict["ready"])
Loading

0 comments on commit 48ff2b9

Please sign in to comment.