Skip to content

Commit

Permalink
Merge pull request #22 from jrsmth/feature/notifications/UMA-31
Browse files Browse the repository at this point in the history
Feature/notifications/uma 31
  • Loading branch information
jrsmth authored Nov 27, 2023
2 parents 83b6511 + fc98690 commit 7bf4307
Show file tree
Hide file tree
Showing 18 changed files with 732 additions and 682 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Releases
<!-- @LatestFirst -->

## [0.5.0] - ???
## [0.5.0] - 27/11/23
- UMA-30: Add the ability to display a message from the backend via message topic
- UMA-31: Implement websocket replacement throughout game flow for both game and player modes

## [0.4.1] - 18/11/23
- UMA-29: [BUG] Computer incorrectly places in already completed outer-squares (Ultimate)
Expand Down
421 changes: 211 additions & 210 deletions src/app/game/game.py

Large diffs are not rendered by default.

51 changes: 9 additions & 42 deletions src/app/login/login.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import shortuuid
from flask import redirect, url_for, render_template, Blueprint, request

from src.app.model.board.nineboard import NineBoard
from src.app.model.board.threeboard import ThreeBoard
from src.app.model.game import Game
from src.app.model.mode.gamemode import GameMode
from src.app.model.status import Status
from src.app.model.game import Game, generate_game_id, generate_board, has_valid_game_id
from src.app.model.mode.playermode import PlayerMode


# Login Logic
Expand All @@ -16,29 +11,23 @@ def construct_blueprint(messages, socket, redis):
def login():
error = None
if request.method == "POST":
print(request.form["name"]) # log me
print(request.form["gameId"])
print(request.form["gameMode"])
print(request.form["playerMode"])
print(bool(request.form["restart"]))
game_id = request.form["gameId"]
user_id = request.form["name"]
game_mode = request.form["gameMode"]
player_mode = request.form["playerMode"]
print(f"[login] Creating game for {user_id}, with game mode [{game_mode}] & player mode [{player_mode}]")

if bool(request.form["restart"]):
redis.set("whoseTurn", "player1")
return redirect(url_for("game_page.game", game_id=request.form["gameId"], user_id=request.form["name"]))

# Set up player one - Question :: extract?
if request.form["gameId"] == "":
if game_id == "":
game_id = generate_game_id()
game = Game()
game.game_id = game_id
game.game_mode = game_mode
game.player_mode = player_mode
game.player_one.name = request.form["name"]
game.player_two.name = ""
game.board = generate_board(game_mode)

if redis.get("playerMode") == "SINGLE":
if player_mode == PlayerMode.SINGLE.value:
game.player_two.name = "Computer"

print("[login] Setting game object: " + game.to_string())
Expand All @@ -64,27 +53,5 @@ def login():

return render_template("login.html", error=error)

# Closing return
# Blueprint return
return login_page


def has_valid_game_id(game_id):
if game_id != "-1": # Does game id already exist?
return True
else:
return False


def generate_game_id():
return shortuuid.uuid()[:12]
# return "ab12-3cd4-e5f6-78gh"


def generate_board(game_mode):
if game_mode == GameMode.STANDARD.value:
return ThreeBoard().list()
elif game_mode == GameMode.ULTIMATE.value:
return NineBoard().list()

else:
print("Game Mode already set [" + game_mode + "]") # TODO :: err handle
13 changes: 13 additions & 0 deletions src/app/model/board/board.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from abc import ABC, abstractmethod

from src.app.model.status import Status
from src.app.model.symbol import Symbol


class Board(ABC):

Expand Down Expand Up @@ -51,3 +54,13 @@ def bot_rhs(self): pass
@abstractmethod
def list(self): pass
# Returns list in the form [top_lhs, top_mid, ...]


# Maps the status of an outer square to the corresponding player symbol
def map_to_symbol(state):
if state == Status.IN_PROGRESS.value or state == Status.DRAW.value:
return Symbol.NEUTRAL.value
if state == Status.PLAYER_ONE_WINS.value:
return Symbol.CROSS.value
if state == Status.PLAYER_TWO_WINS.value:
return Symbol.CIRCLE.value
17 changes: 17 additions & 0 deletions src/app/model/combos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

# Winning combinations in Tic Tac Toe
def get_wins():
return [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]


class Combos:
pass
31 changes: 28 additions & 3 deletions src/app/model/game.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import shortuuid

from src.app.model.base import Base
from src.app.model.board.nineboard import NineBoard
from src.app.model.board.threeboard import ThreeBoard
from src.app.model.mode.gamemode import GameMode
from src.app.model.mode.playermode import PlayerMode
from src.app.model.notification import Notification
from src.app.model.player import Player
from src.app.model.status import Status
from src.app.model.symbol import Symbol
Expand All @@ -10,9 +15,9 @@
class Game(Base): # Question :: Rename? -> State?
complete = False
game_id = ''
game_mode = GameMode.STANDARD
player_one = Player('', Symbol.CROSS.value)
player_two = Player('', Symbol.CIRCLE.value)
game_mode = GameMode.STANDARD.value
player_one = Player('', Symbol.CROSS.value, Notification())
player_two = Player('', Symbol.CIRCLE.value, Notification())
player_turn = 1 # Tracks whose turn it is: player '1' or '2' -> # Question :: better way?
player_mode = PlayerMode.DOUBLE
playable_square = -1 # Tracks the next outer square that can be played (ultimate-mode); -1 represents any square
Expand All @@ -21,3 +26,23 @@ class Game(Base): # Question :: Rename? -> State?
Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, Status.IN_PROGRESS.value,
Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, Status.IN_PROGRESS.value
]


def has_valid_game_id(game_id):
if game_id != "-1": # Does game id already exist?
return True
else:
return False


def generate_game_id():
return shortuuid.uuid()[:12]


def generate_board(game_mode):
if game_mode == GameMode.STANDARD.value:
return ThreeBoard().list()
elif game_mode == GameMode.ULTIMATE.value:
return NineBoard().list()
else:
print("Game Mode already set [" + game_mode + "]") # TODO :: err handle
11 changes: 11 additions & 0 deletions src/app/model/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from src.app.model.base import Base
from src.app.model.mood import Mood


# Model object that represents a notification
class Notification(Base):
active = False
title = ''
content = ''
icon = ''
mood = Mood.NEUTRAL.value
3 changes: 2 additions & 1 deletion src/app/model/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Model object that holds the player information
class Player(Base):

def __init__(self, name, symbol):
def __init__(self, name, symbol, notification):
self.name = name
self.symbol = symbol
self.notification = notification
2 changes: 1 addition & 1 deletion src/app/socket/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ def my_event(message):
print(str(message))
emit('my_response', {'data': message['data'], 'count': session['receive_count']})

# Closing return
# Blueprint return
return socket_page
18 changes: 13 additions & 5 deletions src/app/util/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ def __init__(self, path):
self.bundle.load(open(path))

def load(self, key):
return self.bundle[key]
message = self.bundle[key]
if message is None:
print(f"[load] Unable to find message for key [{key}]")
return ''
else:
return message

def load_with_params(self, key, parameters: list):
message = self.bundle[key]
for index in range(len(parameters)):
message = message.replace('{'+str(index)+'}', parameters[index])

return message
if message is None:
print(f"[load_with_params] Unable to find message for key [{key}]")
return ''
else:
for index in range(len(parameters)):
message = message.replace('{' + str(index) + '}', parameters[index])
return message
32 changes: 25 additions & 7 deletions src/app/util/redis.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from json import JSONDecodeError
import jsons
from flask_redis import FlaskRedis

Expand All @@ -8,20 +9,37 @@ class Redis:
def __init__(self, app):
self.client = FlaskRedis(app)

# Get an element by its key and decode in utf-8 format
def get(self, key):
return self.client.get(key).decode('utf-8')
# Get an element by its key and decode in utf-8 format

# Set a key-value element
def set(self, key, value):
return self.client.set(key, value)
# Set a key-value element

# Set a complex key-value element by converting to json string
def set_complex(self, key, complex_value):
json_value = str(jsons.dump(complex_value))
json_value = standardise(jsons.dump(complex_value))
print("[set_complex] Successful conversion to JSON, setting value: " + json_value) # TODO :: TRACE
return self.client.set(key, json_value)
# Set a complex key-value element by converting to json string

# Get a complex key-value element by converting from json string
def get_complex(self, key):
json_value = self.client.get(key).decode('utf-8').replace("\'", "\"") # FixMe :: bit dodgy
return jsons.loads(json_value)
# Get a complex key-value element by converting from json string
json_value = self.client.get(key).decode('utf-8')
try:
return jsons.loads(standardise(json_value))
except JSONDecodeError:
raise Exception("[get_complex] Error parsing retrieved object: " + str(json_value))


# Standardises a JSON string for conversion into a python dict
def standardise(value): # FixMe :: bit dodgy
# Convert Python `False` to JSON-standard `true`
standardised = str(value).replace("False,", "false,").replace("True,", "true,")

# Convert single-speech (') marks to the JSON-standard double-speech marks (")
return standardised.replace("{\'", "{\"") \
.replace("\'}", "\"}") \
.replace("\':", "\":") \
.replace(" \'", " \"") \
.replace("\',", "\",")
18 changes: 9 additions & 9 deletions src/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ login.error.invalid-game-id=Invalid Game ID: Please try again or leave blank to
## Game
game.end.draw.header=Draw
game.end.draw.icon=fa-solid fa-face-surprise
game.end.draw.1.message=Phew, what a battle!
game.end.draw.2.message=Well that was good use of time...
game.end.draw.3.message=When you win, say nothing. When you lose, say less
game.end.draw.0.message=Phew, what a battle!
game.end.draw.1.message=Well that was good use of time...
game.end.draw.2.message=When you win, say nothing. When you lose, say less

game.end.win.header=Congratulations, {0}!
game.end.win.icon=fa-solid fa-face-smile
game.end.win.1.message=Winning isn't everything, it's the only thing
game.end.win.2.message=Life shrinks or expands in proportion to one's courage
game.end.win.3.message=Winning isn't everything, but wanting it is
game.end.win.0.message=Winning isn't everything, it's the only thing
game.end.win.1.message=Life shrinks or expands in proportion to one's courage
game.end.win.2.message=Winning isn't everything, but wanting it is

game.end.lose.header=Commiserations, {0}
game.end.lose.icon=fa-solid fa-face-dizzy
game.end.lose.1.message=Doubt kills more dreams than failure ever will
game.end.lose.2.message=Fight to avoid defeat, a surefire losing strategy
game.end.lose.3.message=Accept finite disappointment, never lose infinite hope
game.end.lose.0.message=Doubt kills more dreams than failure ever will
game.end.lose.1.message=Fight to avoid defeat, a surefire losing strategy
game.end.lose.2.message=Accept finite disappointment, never lose infinite hope
Loading

0 comments on commit 7bf4307

Please sign in to comment.