Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sequence diagrams #18

Closed
wants to merge 11 commits into from
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
48 changes: 42 additions & 6 deletions AI/ai_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import logging
import copy

logging.basicConfig(level=logging.DEBUG)
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 @@ -30,19 +33,26 @@ def post_init(self):
super().__init__(self._ip, self._port, self._player)

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 @@ -52,6 +62,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 @@ -84,9 +97,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 @@ -110,7 +129,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 @@ -123,12 +148,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
66 changes: 66 additions & 0 deletions AI/class_diagram.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@startuml
title AI Strategy Pattern
skinparam classAttributeIconSize 0

class AIContext {
-_strategy: AIStrategy
+setStrategy(strategy: AIStrategy): None
+runStrategy(): Thread

}

abstract class 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: AIRulebase
-_ip: str
-_port: int
+post_init(): None
+thread_entry(): None
+run(): None
+join_game(): None
+message_handler(message_type: str): None
+wish_good_luck(): None
+say_good_game(): None
+get_empty_cells(game_status: list): list
+do_turn(): None
}

class WeakAIStrategy {
+do_turn(): None
}

class AdvancedAIStrategy {
+do_turn(): None
+check_winning_move(empty_cells: list, player: int): list|None
}

class AIRuleBase{

+check_win(game_state: GameState): None

}

class ABC{
--Python's Abstract Base Class--
}
class GameClient{
--The Client's GameClient--
}
class RuleBase{
--The Server's RuleBase--
}

AIContext --* AIStrategy
AIStrategy --|> ABC
AIStrategy --|> GameClient
AIStrategy --* AIRuleBase
AIRuleBase --|> RuleBase
AIStrategy <|-- WeakAIStrategy
AIStrategy <|-- AdvancedAIStrategy

@enduml
2 changes: 1 addition & 1 deletion Client/ui_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ async def await_commands(self):
case "game/make_move":
await self.game_make_move(**message["args"])
case "chat/message":
pass
await self.chat_message(**message["args"])
case "server/terminate":
await self.terminate()
case "game/gamestate":
Expand Down
38 changes: 38 additions & 0 deletions UI/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from .lib import tttk_tk as tk

class Chat(tk.Frame):
def __init__(self, master, root, chat='', *args, **kwargs):
super().__init__(master)
self.root = root
self._create_widgets(chat)
self._display_widgets()
self.root.network_events['chat/receive'] = self._chat_receive

def _create_widgets(self, chat):
#self.txtChat = tk.Text(self.widget, state=tk.DISABLED)
self.txtChat = tk.Text(self.widget, width=0)
self.txtScroll = tk.Scrollbar(self.widget, command=self.txtChat.yview)
self.txtChat.config(yscrollcommand=self.txtScroll.set)
self.txtChat.insert(tk.END, chat)
self.txtChat.config(state=tk.DISABLED)
self.etrMessage = tk.Entry(self.widget)
self.btnSend = tk.Button(self.widget, text="Send", command=lambda *args: self._send())

def _display_widgets(self):
self.widget.columnconfigure([0], weight=5)
self.widget.columnconfigure([1], weight=1)
self.widget.rowconfigure([0], weight=1)
self.widget.rowconfigure([1,2], weight=0)
self.txtChat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=0, columnspan=2)
self.txtScroll.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=0)
self.etrMessage.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=1)
self.btnSend.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=1, columnspan=2)

def _send(self):
self.root.out_queue.put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}})
self.etrMessage.val = ""

def _chat_receive(self, queue):
self.txtChat.config(state=tk.NORMAL)
self.txtChat.insert(tk.END, f"{queue['sender'].display_name}: {queue['message']}\n")
self.txtChat.config(state=tk.DISABLED)
2 changes: 1 addition & 1 deletion UI/endscreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def _create_widgets(self, win:bool):
message = "You won the game!" if win else "You lost the game!"
self.lblWinner = tk.Label(self, width=20, height=5, bg="white", text=message)
#self.btnPlayAgain = tk.Button(self, width=20, height=5, text="Play Again", command=lambda: self.master.show(Field))
self.btnMainMenu = tk.Button(self, text="Main Menu", width=20, height=5, command=lambda: self.master.show_menu)
self.btnMainMenu = tk.Button(self, text="Main Menu", width=20, height=5, command=lambda: self.master.show_menu())

def _display_widgets(self):
self.lblWinner.pack()
Expand Down
11 changes: 7 additions & 4 deletions UI/field_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .gamefield import gamefield, gamefield_controller
from .endscreen import EndScreen
from .messages import messages
from .chat import Chat

class player_type(Enum):
local = auto()
Expand Down Expand Up @@ -71,9 +72,9 @@ def error(self, queue, *args):
msg.display()

class Field(base_frame):
def __init__(self, master, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs):
def __init__(self, master, chat, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs):
super().__init__(master)
self._create_widgets()
self._create_widgets(chat)
self.controller = field_controller(self, [starting_player, opponent])
self._display_widgets()
#self.bind("<<game/turn>>", self.controller.sub_controller.turn)
Expand All @@ -83,22 +84,24 @@ def __init__(self, master, *args, starting_player, starting_player_symbol, oppon
self.master.network_events['game/end'] = self.controller.end
self.master.network_events['game/error'] = self.controller.error

def _create_widgets(self):
def _create_widgets(self, chat):
self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font)
self.player = []
self.player.append(player(self, 1))
self.player.append(player(self, 2))
self.gamefield = gamefield(self)
self.chat = Chat(self, self.master, chat)
self.close = tk.Button(self, text="close")

def _display_widgets(self):
self.columnconfigure(1, weight=1)
self.columnconfigure([1,3], weight=1)
self.rowconfigure(2, weight=1)

self.heading.grid(row=0, column=0, columnspan=3)
self.player[0].grid(row=1, column=0)
self.player[1].grid(row=1, column=2)
self.gamefield.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=2, column=1)
self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3)
self.close.grid(row=3, column=2)

def on_destroy(self):
Expand Down
3 changes: 2 additions & 1 deletion UI/lib/tttk_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def __init__(self, Widget: tk.Widget, master: tk.Misc, *args, **kwargs):
kwargs['defaultValues'] = {
'font': ("Arial bold", 12),
'margin': 5,
'bg': '#FFFFFF',} | defaultValues
'bg': '#FFFFFF',
'width': 0,} | defaultValues
if(Widget == tk.Frame):
del kwargs['defaultValues']['font']
super().__init__(Widget, master, *args, **kwargs)
Expand Down
14 changes: 9 additions & 5 deletions UI/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from Server.main import server_thread
from AI.ai_context import AIContext
from AI.ai_strategy import AIStrategy, WeakAIStrategy, AdvancedAIStrategy
from .chat import Chat

class Join(base_frame):
def __init__(self, master, *args, opponent=player_type.unknown, **kwargs):
Expand All @@ -36,8 +37,9 @@ def _create_widgets(self, opponent):
if opponent == player_type.local:
self.btnRdy2 = tk.Button(self, text='Start', command=lambda *args: self.master.out_queue.put({'message_type': 'lobby/ready', 'args' : {'ready': True}}))
self.btnExit = tk.Button(self, text='Menu', command=lambda: self.master.show_menu())
self.chat = Chat(self, self.master)

def _display_widgets(self,):
def _display_widgets(self):
self.columnconfigure([0, 6], weight=1)
self.columnconfigure([1, 5], weight=2)
self.columnconfigure([2, 4], weight=4)
Expand All @@ -46,11 +48,13 @@ def _display_widgets(self,):
self.rowconfigure([2], weight=2)
self.rowconfigure([4, 6, 8, 10], weight=4)
self.rowconfigure([3, 5, 7, 9, 11], weight=2)
self.grid_configure()
# display the buttons created in the _create_widgets method
self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3)
self.btnRdy.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=10)
self.btnRdy.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10)
if hasattr(self, 'btnRdy2'):
self.btnRdy2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10)
self.chat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=2, rowspan=7)
self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1)

def _update_lobby(self, queue):
Expand All @@ -65,13 +69,13 @@ def _update_lobby(self, queue):
else:
self.btnRdy.config(text="Start")
for i, player in enumerate(self.playerlist):
player[0].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4+i, columnspan=2)
player[1].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4+i)
player[0].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4+i)
player[1].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4+i)


def _start_game(self, queue):
print(queue)
self.master.show(Field, **queue)
self.master.show(Field, self.chat.txtChat.get("1.0", tk.END+"-1c")+"Game starting\n", **queue)

def on_destroy(self):
del self.master.network_events['lobby/status']
Expand Down
8 changes: 6 additions & 2 deletions UI/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _create_widgets(self):
self.lblUUDI = tk.Label(self, text='User ID')
self.lblUUIDValue = tk.Label(self, text=self.master.player.uuid)
self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, 'edit'))
self.btnDelete = tk.Button(self, text='Delete profile', command=lambda*args : self.master.show(NewProfile, 'delete'))
self.btnDelete = tk.Button(self, text='Delete profile', command=lambda *args: self._delete())
self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu())

def _display_widgets(self):
Expand All @@ -87,4 +87,8 @@ def _display_widgets(self):
self.lblUUIDValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=6, columnspan=5)
self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=3)
self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=6, row=8, columnspan=3)
self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1)
self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1)

def _delete(self):
self.master.player = None
self.master.show(NewProfile, 'delete')
15 changes: 15 additions & 0 deletions docs/sequence_diagrams/create_profile.puml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading