diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0282a..2a9d4a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Releases -## [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) diff --git a/src/app/game/game.py b/src/app/game/game.py index 2dd5c6a..a66cb5e 100644 --- a/src/app/game/game.py +++ b/src/app/game/game.py @@ -3,8 +3,14 @@ from flask import render_template, url_for, redirect, Blueprint, Response +from src.app.model.board.board import map_to_symbol from src.app.model.board.threeboard import ThreeBoard +from src.app.model.combos import get_wins +from src.app.model.game import generate_board +from src.app.model.mode.gamemode import GameMode +from src.app.model.mode.playermode import PlayerMode from src.app.model.mood import Mood +from src.app.model.notification import Notification from src.app.model.status import Status from src.version.version import __version__ @@ -15,258 +21,253 @@ def construct_blueprint(messages, socket, redis): @game_page.route("/game//") def game(game_id, user_id): - player_one_active = False - player_two_active = False - notification_active = False - game_complete = False - notification_header = "" - notification_message = "" - notification_icon = "" - notification_mood = Mood.NEUTRAL.value - update_game_state(game_id, user_id + ' has joined the game') + check_status(game_id) - if redis.get("whoseTurn") == 'player1': - player_one_active = True - else: - player_two_active = True - - game_mode = redis.get("gameMode") - board = redis.get_complex("board") - game_state = get_game_state(redis, board) - - if game_state != Status.IN_PROGRESS: - notification_active = True - game_complete = True - - if game_state == Status.DRAW: - notification_header = messages.load("game.end.draw.header") - notification_message = messages.load("game.end.draw.1.message") - notification_icon = messages.load("game.end.draw.icon") - - elif game_state == Status.PLAYER_ONE_WINS: - if redis.get("player1") == user_id: - notification_header = messages.load_with_params("game.end.win.header", [redis.get("player1")]) - notification_icon = messages.load("game.end.win.icon") - notification_message = messages.load("game.end.win.1.message") - notification_mood = Mood.HAPPY.value - else: - notification_header = messages.load_with_params("game.end.lose.header", [redis.get("player2")]) - notification_icon = messages.load("game.end.lose.icon") - notification_message = messages.load("game.end.lose.3.message") - notification_mood = Mood.SAD.value - - elif game_state == Status.PLAYER_TWO_WINS: - if redis.get("player2") == user_id: - notification_header = messages.load_with_params("game.end.win.header", [redis.get("player2")]) - notification_icon = messages.load("game.end.win.icon") - notification_message = messages.load("game.end.win.3.message") - notification_mood = Mood.HAPPY.value - else: - notification_header = messages.load_with_params("game.end.lose.header", [redis.get("player1")]) - notification_icon = messages.load("game.end.lose.icon") - notification_message = messages.load("game.end.lose.1.message") - notification_mood = Mood.SAD.value - - return render_template( - "game.html", - gameId=game_id, - gameMode=game_mode, - userId=user_id, - version=__version__, - - - # Question :: Should I have an ~overloaded method for simply .get()? - # zero=redis.get("0"), - # one=redis.get("1"), - # two=redis.get("2"), - # three=redis.get("3"), - # four=redis.get("4"), - # five=redis.get("5"), - # six=redis.get("6"), - # seven=redis.get("7"), - # eight=redis.get("8"), - # gameComplete=game_complete, - # gameMode=game_mode, - # playerMode=redis.get("playerMode"), - # notificationActive=notification_active, - # notificationHeader=notification_header, - # notificationMessage=notification_message, - # notificationIcon=notification_icon, - # notificationMood=notification_mood, - # playerOneActive=player_one_active, - # playerTwoActive=player_two_active, - # thisUserId=user_id, - # thisUserSymbol=redis.get(user_id), - # whoseTurn=redis.get("whoseTurn") - ) + return render_template("game.html", gameId=game_id, userId=user_id, version=__version__) + + @socket.on('restart') + def restart(message): + game_id = message['gameId'] + user_id = message['userId'] + game_state = redis.get_complex(game_id) + print(f"[restart] Received restart from [{user_id}] for {game_id}") + + game_state["complete"] = False + game_state["player_one"]["notification"] = Notification() + game_state["player_two"]["notification"] = Notification() + game_state["board"] = generate_board(game_state["game_mode"]) + game_state["outer_states"] = [ # List of states for each outer square -> # Question :: better way? + Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, + Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, + Status.IN_PROGRESS.value, Status.IN_PROGRESS.value, Status.IN_PROGRESS.value + ] + + if game_state["player_mode"] == PlayerMode.SINGLE.value: + game_state["player_turn"] = 1 + + redis.set_complex(game_id, game_state) + update_game_state(game_id, user_id + ' has restarted the game') @game_page.route("/game//place-move//") def place_standard_move(game_id, user_id, square): + print("[place_standard_move] [" + game_id + "] [" + user_id + "] Placing square with index: " + square) + # Set player's move - symbol = redis.get(user_id) - board_list = redis.get_complex("board") - board_list[int(square)] = int(symbol) - redis.set_complex("board", board_list) + index_tuple = (int(square)) + current_state = set_player_move(game_id, user_id, index_tuple) # Switch player turn - if redis.get("whoseTurn") == 'player1': - # TODO :: I need to test the game state here to prevent the Computer from placing after I win... - redis.set("whoseTurn", "player2") - if redis.get("playerMode") == "SINGLE": - print("[SINGLE] Computer is placing move") - # Of the remaining available squares, select one at random - current_board = redis.get_complex("board") - print("[SINGLE] Board: " + str(current_board)) - available_squares = [index for index, square in enumerate(current_board) if square == 0] - print("[SINGLE] Available: " + str(available_squares)) - if available_squares: - place_standard_move(game_id, "Computer", random.choice(available_squares)) - - elif redis.get("whoseTurn") == 'player2': - redis.set("whoseTurn", "player1") - - update_game_state(game_id, 'move placement') + if current_state["player_turn"] == 1: + current_state["player_turn"] = 2 + redis.set_complex(game_id, current_state) + if check_status(game_id) != Status.IN_PROGRESS: + update_game_state(game_id, user_id + ' has placed move on square ' + square) + return Response(status=204) + + # Place Computer move [single mode] + elif current_state["player_mode"] == PlayerMode.SINGLE.value: + set_standard_computer_move(game_id, current_state["board"]) + + elif current_state["player_turn"] == 2: + current_state["player_turn"] = 1 + redis.set_complex(game_id, current_state) + + check_status(game_id) + update_game_state(game_id, user_id + ' has placed move on square ' + square) return Response(status=204) - # return redirect(url_for("game_page.game", game_id=game_id, user_id=user_id)) @game_page.route("/game//place-move///") def place_ultimate_move(game_id, user_id, outer_square, inner_square): - # Set player's move - symbol = redis.get(user_id) - board = redis.get_complex("board") - board[int(outer_square)][int(inner_square)] = int(symbol) - redis.set_complex("board", board) + outer_index = int(outer_square) + inner_index = int(inner_square) + print(f"[place_ultimate_move] [{game_id}] [{user_id}] Placing move with on [{outer_square}] [{inner_square}]") - print("[place_ultimate_move] outer_square: " + str(outer_square)) - print("[place_ultimate_move] inner_square: " + str(inner_square)) - print(board) + # Set player's move + index_tuple = (outer_index, inner_index) + current_state = set_player_move(game_id, user_id, index_tuple) # Set next playable outer square - if get_game_state(redis, board[int(inner_square)]) != Status.IN_PROGRESS: - redis.set("playableSquare", "-1") # -1 is all squares... + status = calculate_game_status(current_state, current_state["board"][inner_index]) + if status != Status.IN_PROGRESS: + current_state["playable_square"] = -1 else: - redis.set("playableSquare", inner_square) - - print("[place_ultimate_move] playableSquare: " + redis.get("playableSquare")) + current_state["playable_square"] = inner_index + print("[place_ultimate_move] Next playable square set to: " + str(current_state["playable_square"])) # Switch player turn - if redis.get("whoseTurn") == 'player1': - # TODO :: I need to test the game state here to prevent the Computer from placing after I win... - redis.set("whoseTurn", "player2") - if redis.get("playerMode") == "SINGLE": - print("[SINGLE][ULTIMATE] Computer is placing move") - # Of the remaining available squares, select one at random - - # Prevent computer from placing in an already completed outer square - chosen_outer_square = int(inner_square) - if redis.get("playableSquare") == "-1": - inner_states = redis.get_complex("innerStates") - available_outers = [index for index, state in enumerate(inner_states) if state == 1] - chosen_outer_square = random.choice(available_outers) - - current_board = redis.get_complex("board")[chosen_outer_square] - print("[SINGLE][ULTIMATE] Board: " + str(current_board)) - available_squares = [index for index, square in enumerate(current_board) if square == 0] - print("[SINGLE][ULTIMATE] Available: " + str(available_squares)) - if available_squares: - place_ultimate_move(game_id, "Computer", chosen_outer_square, random.choice(available_squares)) - - elif redis.get("whoseTurn") == 'player2': - redis.set("whoseTurn", "player1") - - return redirect(url_for("game_page.game", game_id=game_id, user_id=user_id)) + if current_state["player_turn"] == 1: + current_state["player_turn"] = 2 + redis.set_complex(game_id, current_state) + + if check_status(game_id) != Status.IN_PROGRESS: + update_game_state(game_id, user_id + ' has placed on [' + outer_square + "] [" + inner_square + "]") + return Response(status=204) + + # Place Computer move [single mode] + elif current_state["player_mode"] == PlayerMode.SINGLE.value: + set_ultimate_computer_move(game_id, inner_index) + + elif current_state["player_turn"] == 2: + current_state["player_turn"] = 1 + redis.set_complex(game_id, current_state) + + check_status(game_id) + update_game_state(game_id, user_id + ' has placed on [' + str(outer_square) + "] [" + str(inner_square) + "]") + return Response(status=204) @game_page.route('/game/state/') - def retrieve_game_state(game_id): # TODO :: rename -> get_game_state? + def get_game_state(game_id): game_state = redis.get_complex(game_id) print('[retrieve_game_state] Retrieving game state: ' + str(game_state)) return Response(status=200, content_type='application/json', response=json.dumps(game_state)) - # Question :: Custom Response class? def update_game_state(game_id, description): print('[update_game_state] Game state update: ' + description) print('[update_game_state] Game state updated for game id: ' + game_id) socket.emit('update_game_state', redis.get_complex(game_id)) - # Closing return - return game_page - + def check_status(game_id): + state = redis.get_complex(game_id) + print("[check_status] Checking status for game with state: " + str(state)) + status = calculate_game_status(state, state["board"]) + print("[check_status] Status determined to be: " + str(status)) + + if status != Status.IN_PROGRESS: + state["player_one"]["notification"] = build_notification(state, messages, status, 1) + state["player_two"]["notification"] = build_notification(state, messages, status, 2) + state["complete"] = True + redis.set_complex(game_id, state) + return status + + def calculate_game_status(state, test_board): + print("[calculate_game_status] Calculating status for game with mode: " + state["game_mode"]) + if isinstance(test_board[0], list): + return calculate_ultimate_status(state, test_board) + if has_player_won(test_board, 1): + return Status.PLAYER_ONE_WINS + elif has_player_won(test_board, 2): + return Status.PLAYER_TWO_WINS + if test_board.count(0) == 0: + return Status.DRAW + else: + return Status.IN_PROGRESS -def get_game_state(redis, board): - winning_combos = [ - [0, 1, 2], - [3, 4, 5], - [6, 7, 8], - [0, 3, 6], - [1, 4, 7], - [2, 5, 8], - [0, 4, 8], - [2, 4, 6] - ] - - if isinstance(board[0], list): - print("[get_game_state] board[0]: " + str(board[0])) - inner_states = [] + def calculate_ultimate_status(state, board): + print(f"[calculate_ultimate_status] Calculating status for board: {board}") + outer_states = [] for outer_square in board: - inner_state = get_game_state(redis, outer_square) - inner_states.append(inner_state.value) - print("[get_game_state] inner_states: " + str(inner_states)) - if len(inner_states) == 9 and inner_states.count(1) == 0: + outer_state = calculate_game_status(state, outer_square) + outer_states.append(outer_state.value) + print("[calculate_ultimate_status] outer_states: " + str(outer_states)) + if len(outer_states) == 9 and outer_states.count(1) == 0: return Status.DRAW - redis.set_complex("innerStates", inner_states) - return get_game_state(redis, create_false_board(inner_states)) - - if board.count(0) == 0: - return Status.DRAW - # FixMe :: this check needs to happen after test each player has won... - # Note :: There is probs a clean way to split this up so that you don't iterate when not nec... - - print("count: " + str(board.count(1))) - if (board.count(1)) >= 3: - player_moves = get_player_moves(1, board) - print(player_moves) - for combo in winning_combos: - print(combo) - if set(combo).issubset(set(player_moves)): - return Status.PLAYER_ONE_WINS + state["outer_states"] = outer_states + redis.set_complex(state["game_id"], state) + return calculate_game_status(state, create_false_board(outer_states)) + + def set_player_move(game_id, user_id, index_tuple): + current_state = redis.get_complex(game_id) + board = current_state["board"] + print("[set_player_move] Board retrieved: " + str(board)) + + players = [current_state["player_one"], current_state["player_two"]] + user_symbol = [player for player in players if player["name"] == user_id][0]["symbol"] # Could be more elegant + + if current_state["game_mode"] == GameMode.STANDARD.value: + board[index_tuple] = user_symbol + elif current_state["game_mode"] == GameMode.ULTIMATE.value: + board[index_tuple[0]][index_tuple[1]] = user_symbol + + current_state["board"] = board + return current_state + + def set_standard_computer_move(game_id, board): + available_squares = [index for index, square in enumerate(board) if square == 0] + print("[set_standard_computer_move] Available squares: " + str(available_squares)) + if available_squares: + print("[set_standard_computer_move] Computer is placing move in random available square") + place_standard_move(game_id, "Computer", str(random.choice(available_squares))) + + def set_ultimate_computer_move(game_id, inner_square): + print("[set_ultimate_computer_move] Computer is placing move in a random available square") + game_state = redis.get_complex(game_id) - if (board.count(2)) >= 3: - player_moves = get_player_moves(2, board) - for combo in winning_combos: - if set(combo).issubset(set(player_moves)): - return Status.PLAYER_TWO_WINS + # Prevent computer from placing in an already completed outer square + chosen_outer_square = inner_square + if game_state["playable_square"] == -1: + available_outers = [index for index, state in enumerate(game_state["outer_states"]) if state == 1] + chosen_outer_square = random.choice(available_outers) - return Status.IN_PROGRESS + current_board = game_state["board"][chosen_outer_square] + print("[set_ultimate_computer_move] Board: " + str(current_board)) + available_squares = [index for index, square in enumerate(current_board) if square == 0] + print("[set_ultimate_computer_move] Available: " + str(available_squares)) + if available_squares: + place_ultimate_move(game_id, "Computer", chosen_outer_square, random.choice(available_squares)) + + # Blueprint return + return game_page -def get_player_moves(player, board): - player_moves = [] - for index in range(len(board)): - if board[index] == player: - player_moves.append(index) +def has_player_won(board, player): + if (board.count(player)) >= 3: + player_moves = [] - return player_moves + for index in range(len(board)): + if board[index] == player: + player_moves.append(index) + for combo in get_wins(): + if set(combo).issubset(set(player_moves)): + return True -def convert_states_to_symbols(state): - if state == 1: return 0 - if state == 2: return 0 - if state == 3: return 1 - if state == 4: return 2 + return False -def create_false_board(states): +# In ULTIMATE mode, a 'False Board' is defined to be the 3x3 board of resolved outer squares +def create_false_board(states): # Question :: a more elegant way must exist? board = ThreeBoard() - board.top_lhs = convert_states_to_symbols(states[0]) - board.top_mid = convert_states_to_symbols(states[1]) - board.top_rhs = convert_states_to_symbols(states[2]) - board.mid_lhs = convert_states_to_symbols(states[3]) - board.mid_mid = convert_states_to_symbols(states[4]) - board.mid_rhs = convert_states_to_symbols(states[5]) - board.bot_lhs = convert_states_to_symbols(states[6]) - board.bot_mid = convert_states_to_symbols(states[7]) - board.bot_rhs = convert_states_to_symbols(states[8]) + board.top_lhs = map_to_symbol(states[0]) + board.top_mid = map_to_symbol(states[1]) + board.top_rhs = map_to_symbol(states[2]) + board.mid_lhs = map_to_symbol(states[3]) + board.mid_mid = map_to_symbol(states[4]) + board.mid_rhs = map_to_symbol(states[5]) + board.bot_lhs = map_to_symbol(states[6]) + board.bot_mid = map_to_symbol(states[7]) + board.bot_rhs = map_to_symbol(states[8]) print("[create_false_board] board: " + str(board.list())) return board.list() + + +def build_notification(game_state, messages, game_status, player): + print(f"[build_notification] Building notification for player [{player}] with status [{game_status}]") + notification = Notification() + notification.active = True + random_message = str(random.randrange(3)) + + if game_status == Status.DRAW: + notification.title = messages.load("game.end.draw.header") + notification.content = messages.load("game.end.draw." + random_message + ".message") + notification.icon = messages.load("game.end.draw.icon") + return notification + + player_won = (game_status == Status.PLAYER_ONE_WINS and player == 1) or \ + (game_status == Status.PLAYER_TWO_WINS and player == 2) + + player_name = game_state["player_" + ("one" if player == 1 else "two")]["name"] + + if player_won: + notification.title = messages.load_with_params("game.end.win.header", [player_name]) + notification.content = messages.load("game.end.win." + random_message + ".message") + notification.icon = messages.load("game.end.win.icon") + notification.mood = Mood.HAPPY.value + else: + notification.title = messages.load_with_params("game.end.lose.header", [player_name]) + notification.content = messages.load("game.end.lose." + random_message + ".message") + notification.icon = messages.load("game.end.lose.icon") + notification.mood = Mood.SAD.value + + return notification diff --git a/src/app/login/login.py b/src/app/login/login.py index 3a2f04c..a799b11 100644 --- a/src/app/login/login.py +++ b/src/app/login/login.py @@ -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 @@ -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()) @@ -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 diff --git a/src/app/model/board/board.py b/src/app/model/board/board.py index 25f4a51..b4bb915 100644 --- a/src/app/model/board/board.py +++ b/src/app/model/board/board.py @@ -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): @@ -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 diff --git a/src/app/model/combos.py b/src/app/model/combos.py new file mode 100644 index 0000000..35199c9 --- /dev/null +++ b/src/app/model/combos.py @@ -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 diff --git a/src/app/model/game.py b/src/app/model/game.py index a0a1f31..26b75ef 100644 --- a/src/app/model/game.py +++ b/src/app/model/game.py @@ -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 @@ -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 @@ -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 diff --git a/src/app/model/notification.py b/src/app/model/notification.py new file mode 100644 index 0000000..ef01a1c --- /dev/null +++ b/src/app/model/notification.py @@ -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 diff --git a/src/app/model/player.py b/src/app/model/player.py index d4ec769..8743cd3 100644 --- a/src/app/model/player.py +++ b/src/app/model/player.py @@ -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 diff --git a/src/app/socket/socket.py b/src/app/socket/socket.py index 5c039eb..c391a7a 100644 --- a/src/app/socket/socket.py +++ b/src/app/socket/socket.py @@ -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 diff --git a/src/app/util/messages.py b/src/app/util/messages.py index 6b448db..dce0bc6 100644 --- a/src/app/util/messages.py +++ b/src/app/util/messages.py @@ -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 diff --git a/src/app/util/redis.py b/src/app/util/redis.py index 8af6a2b..1598049 100644 --- a/src/app/util/redis.py +++ b/src/app/util/redis.py @@ -1,3 +1,4 @@ +from json import JSONDecodeError import jsons from flask_redis import FlaskRedis @@ -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("\',", "\",") diff --git a/src/resources/messages.properties b/src/resources/messages.properties index eebc901..8061a59 100644 --- a/src/resources/messages.properties +++ b/src/resources/messages.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/src/resources/static/scripts/game.js b/src/resources/static/scripts/game.js index c116875..8db3535 100644 --- a/src/resources/static/scripts/game.js +++ b/src/resources/static/scripts/game.js @@ -1,11 +1,8 @@ let userId; -let thisSymbol; -let gameId; - -async function init(gameId) { // TODO :: convert all to JQuery? - let gameState; - userId = $('#user-id')[0].value; // Question :: better way? +let gameState; +let socket; +async function init(gameId) { // Retrieve game state await $.get(`/game/state/${gameId}`).then(res => { console.debug("Initialising game state"); @@ -16,223 +13,226 @@ async function init(gameId) { // TODO :: convert all to JQuery? console.error(err); }); - // Init user info - const thisUser = [gameState['player_one'], gameState['player_two']].filter(obj => { + const thisPlayer = [gameState['player_one'], gameState['player_two']].filter(obj => { return obj.name === userId - }) - - const playerOne = gameState['player_one']['name']; - const playerTwo = gameState['player_two']['name']; - $('#player-one-name')[0].innerText = playerOne; - $('#player-two-name')[0].innerText = playerTwo; + })[0]; - const playerTurn = gameState['player_turn']; - $('#player-one') - .addClass(playerTurn === 1 ? ' active' : '') - .addClass(userId === playerOne ? ' this-user' : ''); - $('#player-two') - .addClass(playerTurn === 2 ? ' active' : '') - .addClass(userId === playerTwo ? ' this-user' : ''); - $('#cross') - .addClass(playerTurn === 1 ? ' active' : '') - .addClass(userId === playerOne ? ' this-user' : ''); - $('#circle') - .addClass(playerTurn === 2 ? ' active' : '') - .addClass(userId === playerTwo ? ' this-user' : ''); - // TODO :: refactor^^ + // Init user info + initUserInfo(thisPlayer); // Init board - if (gameState['game_mode'] === "STANDARD") createStandardBoard(userId, thisUser['symbol']); - if (gameState['game_mode'] === "ULTIMATE") createUltimateBoard(userId, thisUser['symbol']); - - + if (gameState['game_mode'] === "standard") initStandardBoard(userId, thisPlayer['symbol'], gameState['board']); + if (gameState['game_mode'] === "ultimate") initUltimateBoard(userId, thisPlayer['symbol'], gameState['board']); +} - thisSymbol = document.getElementById('this-user-symbol').value; - const gameMode = document.getElementById('game-mode').value; +function initUserInfo(thisPlayer) { + const playerOneName = gameState['player_one']['name']; + const playerTwoName = gameState['player_two']['name']; + $('#player-one-name')[0].innerText = playerOneName; + $('#player-two-name')[0].innerText = playerTwoName; - if (gameMode === "STANDARD") initStandard(userId, thisSymbol); - if (gameMode === "ULTIMATE") initUltimate(userId, thisSymbol); + const playerTurn = gameState['player_turn']; + const playerOne = $('#player-one'); + const playerTwo = $('#player-two'); + const cross = $('#cross'); + const circle = $('#circle'); + + playerOne.removeClass('active'); + playerTwo.removeClass('active'); + cross.removeClass('active'); + circle.removeClass('active'); + + const gameStarted = (playerTwoName !== ""); + const gameInProgress = gameStarted && !gameState['complete']; + if (gameInProgress) { + playerOne + .addClass(playerTurn === 1 ? ' active' : '') + .addClass(userId === playerOneName ? ' this-user' : ''); + playerTwo + .addClass(playerTurn === 2 ? ' active' : '') + .addClass(userId === playerTwoName ? ' this-user' : ''); + cross + .addClass(playerTurn === 1 ? ' active' : '') + .addClass(userId === playerOneName ? ' this-user' : ''); + circle + .addClass(playerTurn === 2 ? ' active' : '') + .addClass(userId === playerTwoName ? ' this-user' : ''); - const copyGameId = document.getElementById("copy-game-id"); - const span = copyGameId.querySelector("span"); - span.onclick = function () { - document.execCommand("copy"); } - span.addEventListener("copy", function (event) { - event.preventDefault(); - if (event.clipboardData) { - event.clipboardData.setData("text/plain", span.textContent); - const copy = copyGameId.getElementsByClassName("fa-copy")[0]; - const check = copyGameId.getElementsByClassName("fa-check")[0]; - copy.style.display = 'none'; - $(check).addClass("ticked"); - setTimeout((function () { - $(check).removeClass('ticked'); - copy.style.display = 'block'; - $(copy).addClass("fade-in"); - }), 1000); - } - }); + initNotification(thisPlayer["notification"]); } -function createStandardBoard(userId, thisSymbol) { - // Get the threeboard - // Create 9 html elements with value from board - // Append to threeboard... - // TODO^^ +function initNotification(playerNotification) { + const playerTurn = $('#player-turn'); + const notification = $('#notification'); + const notificationContent = $('#notification-content'); + notificationContent.empty(); + + if (playerNotification["active"]) { + playerTurn.addClass('hide'); + notification.addClass('active'); + notification.addClass(playerNotification["mood"]); + + notificationContent.append( + ` +

${playerNotification["title"]}

+

${playerNotification["content"]}

+ ` + ); + + } else { + playerTurn.removeClass('hide'); + notification.removeClass(); + } +} +function initStandardBoard(userId, thisSymbol, board) { + const threeboard = $('#three-board'); + threeboard.addClass('active'); + threeboard.empty(); for (let i = 0; i < 9; i++) { - const square = document.getElementById(`three-square-${i}`).getElementsByClassName("square")[0]; - const state = square.innerHTML; - if (state === thisSymbol) { - square.parentElement.classList.add("this-user"); - } else if (state !== "0") { - square.parentElement.classList.add("opponent-user"); - } - - if (state === "0") { - square.innerHTML = ''; - } else if (state === "1") { - square.innerHTML = '' - } else if (state === "2") { - square.innerHTML = '' - } + const classList = getSquareClass(board[i], thisSymbol); + const markup = + ` +
+
${markupSymbol(board[i])}
+
+ `; + + threeboard.append(markup); } } -function createUltimateBoard(userId, thisSymbol) { - const innerStates = JSON.parse(document.getElementById("inner-states").value); - const playableSquare = document.getElementById("playable-square").value; - console.log(playableSquare) +function initUltimateBoard(userId, thisSymbol, board) { + const nineboard = $('#nine-board'); + nineboard.addClass('active'); + nineboard.empty(); + const outerStates = gameState["outer_states"]; for (let i = 0; i < 9; i++) { - const outerSquare = document.getElementById(`nine-square-${i}`); - - if (innerStates[i] === 2) { outerSquare.classList.add("draw") } - if (innerStates[i] === 3) { outerSquare.classList.add(thisSymbol === '1' ? "this-user" : "opponent-user"); console.log(i) } - if (innerStates[i] === 4) { outerSquare.classList.add(thisSymbol === '2' ? "this-user" : "opponent-user") } + let innerSquares = ''; + let outerClasses = ''; + if (outerStates[i] === 2) { outerClasses += "draw" } + if (outerStates[i] === 3) { outerClasses += (thisSymbol === 1 ? "this-user" : "opponent") } + if (outerStates[i] === 4) { outerClasses += (thisSymbol === 2 ? "this-user" : "opponent") } - if ( - (playableSquare === "-1" || playableSquare === i.toString()) && - (!outerSquare.classList.contains("this-user") && !outerSquare.classList.contains("opponent-user") && !outerSquare.classList.contains("draw")) - ) { outerSquare.classList.add("playable") } + const playableSquare = (gameState["playable_square"] === -1) || (gameState["playable_square"] === i); + const notTaken = (outerStates[i] === 1); + const playable = playableSquare && notTaken; + if (playable) outerClasses += 'playable'; - let outerBoard = [] for (let j = 0; j < 9; j++) { - const innerSquare = document.getElementById(`nine-square-${i}-${j}`).getElementsByClassName("square")[0]; - - const innerState = innerSquare.innerHTML; - if (innerState === thisSymbol) { - innerSquare.parentElement.classList.add("this-user"); - } else if (innerState !== "0") { - innerSquare.parentElement.classList.add("opponent-user"); - } - - if (innerState === "0") { - innerSquare.innerHTML = ''; - } else if (innerState === "1") { - innerSquare.innerHTML = '' - } else if (innerState === "2") { - innerSquare.innerHTML = '' + let classList = ''; + if (!outerClasses.includes("this-user") && !outerClasses.includes("opponent")) { + classList = getSquareClass(board[i][j], thisSymbol); } - outerBoard.push(innerState) + innerSquares += + ` +
+
+ ${markupSymbol(board[i][j])} +
+
+ ` } - // // if outerSquare has a win or lose status, apply colouring and prevent selection... - // // how to obtain status? outer_board array? or can we generate that here(!) - // // make a request to the backend to tell if the board is complete? - // // if count of 1 > 3 or count 2 > 3, make the call to get game state for that square - // $.get(`/game/${gameId}/state//${outerSquare}`) - // .then((res) => { - // if (res === "Status.PLAYER_ONE_WINS") { - // // Add class to the outer square, depending on thisPlayer - // } else if (res === "Status.PLAYER_TWO_WINS") { - // // Add class to the outer square, depending on thisPlayer - // } - // }) - // .catch((e) => { - // console.log(`Error getting the [${outerSquare}] game state for game with ID [${gameId}]`); - // console.log(e); - // }) - // - // // Question :: would an outer board array we more performant? + const outerSquare = + ` +
+
+ ${innerSquares} +
+
+ ` + + nineboard.append(outerSquare); } } -function placeStandardMove(square) { - const userSymbol = document.getElementById('this-user-symbol').value; - const playerOneActive = document.getElementById('player-one-active').value; - const playerTwoActive = document.getElementById('player-two-active').value; - const gameComplete = document.getElementById('game-complete').value; - - if (gameComplete === 'True') return; - if (userSymbol === '1' && playerTwoActive === 'True') return; - if (userSymbol === '2' && playerOneActive === 'True') return; - - if (document.getElementById(`three-square-${square}`).getElementsByClassName("square")[0].innerHTML !== '') { - return; +function placeStandardMove(index) { + if (allowedToPlace(index)) { + $.get(`/game/${gameState['game_id']}/place-move/${userId}/${index}`) + .catch(err => { + console.error(`[placeStandardMove] Error placing move for square with index [${index}]`); + console.error(err); + } + ); + // TODO :: disallow a second user click whilst first is processing... } - - $.get(`/game/${gameId}/place-move/${userId}/${square}`); - // location.reload(); } -// Place Ultimate Move -function placeUltimateMove(outerSquare, innerSquare) { - const userSymbol = document.getElementById('this-user-symbol').value; - const playerOneActive = document.getElementById('player-one-active').value; - const playerTwoActive = document.getElementById('player-two-active').value; - const gameComplete = document.getElementById('game-complete').value; - - if (gameComplete === 'True') return; - if (userSymbol === '1' && playerTwoActive === 'True') return; - if (userSymbol === '2' && playerOneActive === 'True') return; - - // Disallow if square not playable - const outerSquareClasses = document.getElementById(`nine-square-${outerSquare}`).classList; - if (!outerSquareClasses.contains('playable')) return - - // Disallow if inner square complete - if (document.getElementById(`nine-square-${outerSquare}-${innerSquare}`).getElementsByClassName("square")[0].innerHTML !== '') { - return; +function placeUltimateMove(outerIndex, innerIndex) { + const outerSquare = $(`#nine-${outerIndex}`); + const innerSquare = $(`#nine-${outerIndex}-${innerIndex}`); + + const innerSquareComplete = innerSquare.find('.square')[0].innerHTML.trim() !== ''; + const outerSquareComplete = outerSquare.hasClass('this-user') || + outerSquare.hasClass('opponent') || + outerSquare.hasClass('draw'); + + const canPlace = allowedToPlace(outerIndex, innerIndex) && + outerSquare.hasClass('playable') && + !innerSquareComplete && + !outerSquareComplete + + if (canPlace) { + $.get(`/game/${gameState['game_id']}/place-move/${userId}/${outerIndex}/${innerIndex}`) + .catch(err => { + console.error(`[placeStandardMove] Error placing move for square with index [${index}]`); + console.error(err); + } + ); + // TODO :: disallow a second user click whilst first is processing... } +} - // Disallow if outer square complete - if (outerSquareClasses.contains('this-user') || outerSquareClasses.contains('opponent-user') || outerSquareClasses.contains('draw')) { - return; +function enableCopy() { // TODO :: switch to jquery + const copyGameId = document.getElementById("copy-game-id"); + const span = copyGameId.querySelector("span"); + span.onclick = function () { + document.execCommand("copy"); } - $.get(`/game/${gameId}/place-move/${userId}/${outerSquare}/${innerSquare}`); - location.reload(); + // TODO :: revisit the animation and styling + span.addEventListener("copy", function (event) { + event.preventDefault(); + if (event.clipboardData) { + event.clipboardData.setData("text/plain", span.textContent); + const copy = copyGameId.getElementsByClassName("fa-copy")[0]; + const check = copyGameId.getElementsByClassName("fa-check")[0]; + + copy.style.display = 'none'; + $(check).addClass("ticked"); + setTimeout((function () { + $(check).removeClass('ticked'); + copy.style.display = 'block'; + $(copy).addClass("fade-in"); + }), 1000); + } + }); } function restart() { - const formData = { - name: document.getElementById("this-user-id").value, - gameId: gameId, - gameMode: document.getElementById("game-mode").value, - playerMode: document.getElementById("player-mode").value, - restart: true - } - - $.post('/', formData); - location.reload(); + socket.emit('restart', { + gameId: gameState["game_id"], + userId: userId + }); } -$(document).ready(function(){ +function connectSocket(gameId) { // const socket = io(); - const socket = io.connect('http://localhost:8080'); // ?? + socket = io.connect('http://localhost:8080'); // ?? // const socket = io(); console.log(socket); socket.on('connect', function() { console.log('connected'); - socket.emit('my event', {data: 'I\'m connected!'}); + socket.emit('my event', {data: 'I\'m connected!'}); // TODO :: user specific msg? 'James has joined'? }); // socket.on('connect', function() { @@ -247,4 +247,31 @@ $(document).ready(function(){ console.debug('Game state update received'); init(message['game_id']); }); -}); \ No newline at end of file +} + +function markupSymbol(value) { + if (value === 0) return ''; + if (value === 1) return ''; + if (value === 2) return ''; +} + +function isUserTurn() { + return (gameState['player_turn'] === 1 && gameState['player_one']['name'] === userId) || + (gameState['player_turn'] === 2 && gameState['player_two']['name'] === userId); +} + +function getSquareClass(square, thisSymbol) { + if (square === thisSymbol) return 'this-user'; + if (square !== 0) return 'opponent'; + if (!isUserTurn() || (gameState['player_two']['name'] === "")) return 'inactive'; + return ''; +} + +function allowedToPlace(outer, inner) { + const gameStarted = (gameState['player_two']['name'] !== ""); + const gameIncomplete = !gameState['complete']; + const alreadyPlayed = gameState['game_mode'] === "standard" ? + gameState['board'][outer] !== 0 : + gameState['board'][outer][inner] !== 0; + return gameStarted && gameIncomplete && isUserTurn() && !alreadyPlayed; +} \ No newline at end of file diff --git a/src/resources/static/styles/css/base.css b/src/resources/static/styles/css/base.css index 9d9c3a7..f46f76d 100644 --- a/src/resources/static/styles/css/base.css +++ b/src/resources/static/styles/css/base.css @@ -32,4 +32,4 @@ body { height: 100%; } -/*# sourceMappingURL=base.py.css.map */ +/*# sourceMappingURL=base.css.map */ diff --git a/src/resources/static/styles/css/game.css b/src/resources/static/styles/css/game.css index 97465ad..f50787d 100644 --- a/src/resources/static/styles/css/game.css +++ b/src/resources/static/styles/css/game.css @@ -154,27 +154,8 @@ header #copy-game-id .fade-in { } /** Boards **/ -#board-section .threeboard.STANDARD { - display: grid; -} -#board-section .threeboard.ULTIMATE { - display: none; -} -#board-section .nineboard.STANDARD { - display: none; -} -#board-section .nineboard.ULTIMATE { - display: grid; -} -#board-section.this-user-1.player-1-False .threeboard .shadow-square .square, -#board-section.this-user-1.player-1-False .nineboard .shadow-square.inner .square, #board-section.this-user-2.player-2-False .threeboard .shadow-square .square, -#board-section.this-user-2.player-2-False .nineboard .shadow-square.inner .square, #board-section.game-complete-True .threeboard .shadow-square .square, -#board-section.game-complete-True .nineboard .shadow-square.inner .square { - border: 1px solid rgba(231, 4, 83, 0.2509803922) !important; - cursor: default !important; -} - .board { + display: none; margin: 2rem auto 0 auto; height: 50%; aspect-ratio: 1/1; @@ -182,21 +163,23 @@ header #copy-game-id .fade-in { min-height: 400px; grid-template-columns: 33.3% 33.3% 33.3%; } +.board.active { + display: grid; +} /** Three Board */ -.threeboard .shadow-square { +#three-board .shadow { margin: 0.5rem; border: 1px solid rgba(231, 4, 83, 0.2509803922); background-color: #2b0710; border-radius: 0.5rem; } -.threeboard .shadow-square .square { +#three-board .shadow .square { border-radius: inherit; transform: translate(-0.25rem, -0.25rem); border: 1px solid rgba(231, 4, 83, 0.2509803922); background-color: #2b0710; transition: 0.5s; - cursor: pointer; align-content: center; justify-content: center; display: flex; @@ -204,89 +187,95 @@ header #copy-game-id .fade-in { height: 100%; width: 100%; } -.threeboard .shadow-square .square:hover { +#three-board .shadow .square:hover:not(.inactive) { opacity: 1; border: 1px solid #d8ff00; + cursor: pointer; } -.threeboard .shadow-square .square .symbol { +#three-board .shadow .square .symbol { font-size: 4rem; position: absolute; margin-top: calc(47.5% - 2rem); } -.threeboard .shadow-square.this-user { - border: 1px solid #d8ff00; +#three-board .shadow:has(.this-user) { + border: 1px solid #d8ff00 !important; } -.threeboard .shadow-square.this-user .square { - border: 1px solid #d8ff00; - background-color: #d8ff00; - cursor: default; +#three-board .shadow:has(.this-user) .square { + background-color: #d8ff00 !important; + border: 1px solid #d8ff00 !important; + cursor: default !important; } -.threeboard .shadow-square.this-user .square .symbol { +#three-board .shadow:has(.this-user) .square .symbol { color: #2b0710; } -.threeboard .shadow-square.opponent-user { - border: 1px solid #e70453; +#three-board .shadow:has(.opponent) { + border: 1px solid #e70453 !important; } -.threeboard .shadow-square.opponent-user .square { - border: 1px solid #e70453; - background-color: #e70453; - cursor: default; +#three-board .shadow:has(.opponent) .square { + background-color: #e70453 !important; + border: 1px solid #e70453 !important; + cursor: default !important; } -.threeboard .shadow-square.opponent-user .square .symbol { +#three-board .shadow:has(.opponent) .square .symbol { color: #2b0710; } /** Nine Board */ -.nineboard .shadow-square.outer { +#nine-board .shadow.outer { margin: 0.5rem; } -.nineboard .shadow-square.outer:not(.playable, .this-user, .opponent-user) .inner:not(.playable, .this-user, .opponent-user) { - cursor: default !important; +#nine-board .shadow.outer.draw { opacity: 0.3; } -.nineboard .shadow-square.outer:not(.playable, .this-user, .opponent-user) .inner:not(.playable, .this-user, .opponent-user) .square { - cursor: default !important; - border: 1px solid rgba(231, 4, 83, 0.2509803922) !important; +#nine-board .shadow.outer.this-user .inner, #nine-board .shadow.outer.opponent .inner { + opacity: 1 !important; + color: #2b0710; } -.nineboard .shadow-square.outer .square { - display: grid; - grid-template-columns: 33.3% 33.33% 33.3%; - height: 100%; +#nine-board .shadow.outer.this-user .inner .square, #nine-board .shadow.outer.opponent .inner .square { + cursor: default !important; } -.nineboard .shadow-square.outer.this-user .shadow-square.inner { +#nine-board .shadow.outer.this-user .inner { border: 1px solid #d8ff00 !important; - opacity: 1; } -.nineboard .shadow-square.outer.this-user .shadow-square.inner .square { +#nine-board .shadow.outer.this-user .inner .square { + background-color: #d8ff00; border: 1px solid #d8ff00 !important; - background-color: #d8ff00 !important; - cursor: default !important; } -.nineboard .shadow-square.outer.opponent-user .shadow-square.inner { +#nine-board .shadow.outer.opponent .inner { border: 1px solid #e70453 !important; - opacity: 1; } -.nineboard .shadow-square.outer.opponent-user .shadow-square.inner .square { +#nine-board .shadow.outer.opponent .inner .square { + background-color: #e70453; border: 1px solid #e70453 !important; - background-color: #e70453 !important; +} +#nine-board .shadow.outer:not(.playable, .this-user, .opponent-user) .inner:not(.playable, .this-user, .opponent-user) { cursor: default !important; + opacity: 0.5; } -.nineboard .shadow-square.outer.draw .shadow-square.inner { - opacity: 0.1; +#nine-board .shadow.outer:not(.playable, .this-user, .opponent-user) .inner:not(.playable, .this-user, .opponent-user) .square { + cursor: default !important; + border: 1px solid rgba(231, 4, 83, 0.2509803922) !important; } -.nineboard .shadow-square.inner { +#nine-board .shadow.outer:not(.playable, .this-user, .opponent-user) .inner .this-user.square { + border: 1px solid #d8ff00 !important; +} +#nine-board .shadow.outer .square { + display: grid; + grid-template-columns: 33.3% 33.33% 33.3%; + height: 100%; +} +#nine-board .shadow.inner { margin: 0.375rem; border: 1px solid rgba(231, 4, 83, 0.2509803922); background-color: #2b0710; border-radius: 0.5rem; } -.nineboard .shadow-square.inner .square { +#nine-board .shadow.inner .square { border-radius: inherit; transform: translate(-0.25rem, -0.25rem); border: 1px solid rgba(231, 4, 83, 0.2509803922); background-color: #2b0710; transition: 0.5s; - cursor: pointer; align-content: center; justify-content: center; display: flex; @@ -294,89 +283,93 @@ header #copy-game-id .fade-in { height: 100%; width: 100%; } -.nineboard .shadow-square.inner .square:hover { +#nine-board .shadow.inner .square:hover:not(.inactive) { opacity: 1; border: 1px solid #d8ff00; + cursor: pointer; } -.nineboard .shadow-square.inner .square .symbol { +#nine-board .shadow.inner .square .symbol { font-size: 1.5rem; position: absolute; margin-top: calc(50% - 0.75rem); } -.nineboard .shadow-square.inner.this-user { - border: 1px solid #d8ff00; +#nine-board .shadow.inner:has(.this-user) { + border: 1px solid #d8ff00 !important; + opacity: 1 !important; } -.nineboard .shadow-square.inner.this-user .square { - border: 1px solid #d8ff00; - background-color: #d8ff00; - cursor: default; +#nine-board .shadow.inner:has(.this-user) .square { + background-color: #d8ff00 !important; + border: 1px solid #d8ff00 !important; + cursor: default !important; } -.nineboard .shadow-square.inner.this-user .square .symbol { +#nine-board .shadow.inner:has(.this-user) .square .symbol { color: #2b0710; } -.nineboard .shadow-square.inner.opponent-user { - border: 1px solid #e70453; +#nine-board .shadow.inner:has(.opponent) { + border: 1px solid #e70453 !important; + opacity: 1 !important; } -.nineboard .shadow-square.inner.opponent-user .square { - border: 1px solid #e70453; - background-color: #e70453; - cursor: default; +#nine-board .shadow.inner:has(.opponent) .square { + background-color: #e70453 !important; + border: 1px solid #e70453 !important; + cursor: default !important; } -.nineboard .shadow-square.inner.opponent-user .square .symbol { +#nine-board .shadow.inner:has(.opponent) .square .symbol { color: #2b0710; } +#nine-board .shadow.inner:has(.draw) { + opacity: 0.1; +} /** Notification */ -.notification { +#notification { margin: 0 auto; height: 5rem; width: 600px; letter-spacing: 0.5px; align-items: center; text-align: left; + display: none; +} +#notification.active { + display: flex; } -.notification.error { +#notification.error { margin: 2rem 1rem 0 1rem; width: 14rem; } -.notification.error .notification-content { +#notification.error #notification-content { margin: 1rem; } -.notification.active-True { - display: flex; -} -.notification.active-False { - display: none; -} -.notification i { +#notification i { font-size: 1.5rem; margin: 0 1.5rem; } -.notification h3 { +#notification h3 { margin: 0 0 0.5rem 0; font-weight: 400; } -.notification p { +#notification p { margin: 0; font-size: 0.7rem; font-weight: 200; } -.notification.happy { +#notification.happy { color: #d8ff00; } -.notification.happy p { +#notification.happy p { color: whitesmoke; } -.notification.sad { +#notification.sad { color: #e70453; } -.notification.sad h3 { +#notification.sad h3 { color: #ff2c63; } -.notification.sad p { +#notification.sad p { color: whitesmoke; } -.notification .restart { +#notification #restart { cursor: pointer; } diff --git a/src/resources/static/styles/css/game.css.map b/src/resources/static/styles/css/game.css.map index 2cd63c1..0fd19e6 100644 --- a/src/resources/static/styles/css/game.css.map +++ b/src/resources/static/styles/css/game.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../scss/animation.scss","../scss/base.scss","../scss/colours.scss","../scss/game.scss","../scss/spacing.scss"],"names":[],"mappings":"AACA;EACE;IACE;;EAEF;IACE;;;AAIJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;;ACfJ;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE,kBCTO;EDUP,OCQc;EDPd;;;AEbF;AACA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAKN;AAEE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;AACA;EACE;EACA;;AAEA;EACE,ODjDS;;ACoDX;EACE;EACA;;AAEA;EACE;EACA,ODhEC;;ACiED;EAAS;;AAET;EACE;;AACA;EAAW;;AACX;EAAc,ODhET;;ACkEL;EACE;;AAEA;EACE;EACA;EACA;;AAKI;EAAU;;AACV;EAAU;;;AAK1B;AACA;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA,ODrGC;;ACwGH;EACE,ODzGC;EC0GD;EACA;EACA;;AAGF;EACE;EACA;EACA,kBD1HM;;AC4HN;EACI,OD/GG;;;ACqHb;AAEE;EAAuB;;AACvB;EAAuB;;AACvB;EAAuB;;AACvB;EAAuB;;AAKrB;AAAA;AAAA;AAAA;EAEE;EACA;;;AAKN;EACE;EACA,QCxJmB;EDyJnB;EACA,YC5Je;ED6Jf,YC5Je;ED6Jf;;;AAGF;AAEE;EACE;EACA;EACA,kBDnKQ;ECoKR;;AAEA;EACE;EACA;EACA;EACA,kBD1KM;EC2KN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA,kBDvLK;ECwLL;;AAEA;EACE,ODzME;;AC8MR;EACE;;AAEA;EACE;EACA,kBD3MD;EC4MC;;AAEA;EACE,ODvNE;;;AC8NZ;AAEE;EACE;;AAGE;EACE;EACA;;AAEA;EACE;EACA;;AAKN;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAIJ;EACE;;AAIJ;EACE;EACA;EACA,kBDnRQ;ECoRR;;AAEA;EACE;EACA;EACA;EACA,kBD1RM;EC2RN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA,kBDvSK;ECwSL;;AAEA;EACE,ODzTE;;AC8TR;EACE;;AAEA;EACE;EACA,kBD3TD;EC4TC;;AAEA;EACE,ODvUE;;;AC8UZ;AACA;EACE;EACA;EACA,OCrVe;EDsVf;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;;AAIJ;EAAiB;;AACjB;EAAiB;;AAEjB;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE,ODtWS;;ACwWT;EACE,ODpWU;;ACwWZ;EACE,ODpXC;;ACsXD;EACE,OD3XI;;AC8XN;EACE,ODhXQ;;ACoXd;EACE","file":"game.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../scss/animation.scss","../scss/base.scss","../scss/colours.scss","../scss/game.scss","../scss/spacing.scss"],"names":[],"mappings":"AACA;EACE;IACE;;EAEF;IACE;;;AAIJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;;ACfJ;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE,kBCTO;EDUP,OCQc;EDPd;;;AEbF;AACA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAKN;AAEE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;AACA;EACE;EACA;;AAEA;EACE,ODjDS;;ACoDX;EACE;EACA;;AAEA;EACE;EACA,ODhEC;;ACiED;EAAS;;AAET;EACE;;AACA;EAAW;;AACX;EAAc,ODhET;;ACkEL;EACE;;AAEA;EACE;EACA;EACA;;AAKI;EAAU;;AACV;EAAU;;;AAK1B;AACA;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA,ODrGC;;ACwGH;EACE,ODzGC;EC0GD;EACA;EACA;;AAGF;EACE;EACA;EACA,kBD1HM;;AC4HN;EACI,OD/GG;;;ACqHb;AACA;EAEE;EACA;EACA,QCzImB;ED0InB;EACA,YC7Ie;ED8If,YC7Ie;ED8If;;AAPA;EAAW;;;AAUb;AAEE;EACE;EACA;EACA,kBDpJQ;ECqJR;;AAEA;EACE;EACA;EACA;EACA,kBD3JM;EC4JN;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE,OD1LE;;AC+LR;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE,ODxME;;;AC+MZ;AAEE;EACE;;AAEA;EACE;;AAIA;EACE;EACA,OD3NI;;AC4NJ;EACE;;AAMJ;EACE;;AACA;EACE,kBDxNG;ECyNH;;AAMJ;EACE;;AACA;EACE,kBDxOH;ECyOG;;AAMJ;EACE;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;;AAIJ;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA,kBDhRQ;ECiRR;;AAEA;EACE;EACA;EACA;EACA,kBDvRM;ECwRN;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE,ODvTE;;AC4TR;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE,ODtUE;;AC2UR;EACE;;;AAKN;AACA;EACE;EACA;EACA,OCxVe;EDyVf;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE,OD3WS;;AC6WT;EACE,ODzWU;;AC6WZ;EACE,ODzXC;;AC2XD;EACE,ODhYI;;ACmYN;EACE,ODrXQ;;ACyXd;EACE","file":"game.css"} \ No newline at end of file diff --git a/src/resources/static/styles/scss/game.scss b/src/resources/static/styles/scss/game.scss index 87e9ea4..10a9c53 100644 --- a/src/resources/static/styles/scss/game.scss +++ b/src/resources/static/styles/scss/game.scss @@ -133,24 +133,9 @@ header { } /** Boards **/ -#board-section { - .threeboard.STANDARD { display: grid; } - .threeboard.ULTIMATE { display: none; } - .nineboard.STANDARD { display: none; } - .nineboard.ULTIMATE { display: grid; } - - &.this-user-1.player-1-False, - &.this-user-2.player-2-False, - &.game-complete-True { - .threeboard .shadow-square .square, - .nineboard .shadow-square.inner .square { - border: 1px solid $rose25 !important; - cursor: default !important; - } - } -} - .board { + &.active { display: grid; } + display: none; margin: 2rem auto 0 auto; height: $boardLengthDefault; aspect-ratio: 1 / 1; @@ -160,8 +145,8 @@ header { } /** Three Board */ -.threeboard { - .shadow-square { +#three-board { + .shadow { margin: .5rem; border: 1px solid $rose25; background-color: $aubergine; @@ -173,7 +158,6 @@ header { border: 1px solid $rose25; background-color: $aubergine; transition: .5s; - cursor: pointer; align-content: center; justify-content: center; display: flex; @@ -181,9 +165,10 @@ header { height: 100%; width: 100%; - &:hover { + &:hover:not(.inactive) { opacity: 1; border: 1px solid $chartreuse; + cursor: pointer; } .symbol { @@ -193,13 +178,13 @@ header { } } - &.this-user { - border: 1px solid $chartreuse; + &:has(.this-user) { + border: 1px solid $chartreuse !important; .square { - border: 1px solid $chartreuse; - background-color: $chartreuse; - cursor: default; + background-color: $chartreuse !important; + border: 1px solid $chartreuse !important; + cursor: default !important ; .symbol { color: $aubergine; @@ -207,13 +192,13 @@ header { } } - &.opponent-user { - border: 1px solid $rose; + &:has(.opponent) { + border: 1px solid $rose !important; .square { - border: 1px solid $rose; - background-color: $rose; - cursor: default; + background-color: $rose !important; + border: 1px solid $rose !important; + cursor: default !important; .symbol { color: $aubergine; @@ -224,68 +209,79 @@ header { } /** Nine Board */ -.nineboard { - .shadow-square.outer { +#nine-board { + .shadow.outer { margin: .5rem; - &:not(.playable, .this-user, .opponent-user) { - .inner:not(.playable, .this-user, .opponent-user) { - cursor: default !important; - opacity: .3; + &.draw { + opacity: .3; + } + &.this-user, &.opponent { + .inner { + opacity: 1 !important; + color: $aubergine; .square { cursor: default !important; - border: 1px solid $rose25 !important; } } } - .square { - display: grid; - grid-template-columns: 33.3% 33.33% 33.3%; - height: 100%; - } - - &.this-user .shadow-square.inner { - border: 1px solid $chartreuse !important; - opacity: 1; - - .square { + &.this-user { + .inner { border: 1px solid $chartreuse !important; - background-color: $chartreuse !important; - cursor: default !important; + .square { + background-color: $chartreuse; + border: 1px solid $chartreuse !important; + } } } - &.opponent-user .shadow-square.inner { - border: 1px solid $rose !important; - opacity: 1; - - .square { + &.opponent { + .inner { border: 1px solid $rose !important; - background-color: $rose !important; + .square { + background-color: $rose; + border: 1px solid $rose !important; + } + } + } + + &:not(.playable, .this-user, .opponent-user) { + .inner:not(.playable, .this-user, .opponent-user) { cursor: default !important; + opacity: .5; + + .square { + cursor: default !important; + border: 1px solid $rose25 !important; + } + } + + .inner .this-user.square { + border: 1px solid $chartreuse !important; } } - &.draw .shadow-square.inner { - opacity: .1; + .square { + display: grid; + grid-template-columns: 33.3% 33.33% 33.3%; + height: 100%; } } - .shadow-square.inner { + .shadow.inner { margin: .375rem; border: 1px solid $rose25; background-color: $aubergine; border-radius: .5rem; - .square { + .square { // FixMe :: duplication w/ threeboard border-radius: inherit; - transform: translate(-0.25rem, -0.25rem); + transform: translate(-.25rem, -.25rem); border: 1px solid $rose25; background-color: $aubergine; transition: .5s; - cursor: pointer; align-content: center; justify-content: center; display: flex; @@ -293,9 +289,10 @@ header { height: 100%; width: 100%; - &:hover { + &:hover:not(.inactive) { opacity: 1; border: 1px solid $chartreuse; + cursor: pointer; } .symbol { @@ -305,13 +302,14 @@ header { } } - &.this-user { - border: 1px solid $chartreuse; + &:has(.this-user) { + border: 1px solid $chartreuse !important; + opacity: 1 !important; .square { - border: 1px solid $chartreuse; - background-color: $chartreuse; - cursor: default; + background-color: $chartreuse !important; + border: 1px solid $chartreuse !important; + cursor: default !important; .symbol { color: $aubergine; @@ -319,43 +317,50 @@ header { } } - &.opponent-user { - border: 1px solid $rose; + &:has(.opponent) { + border: 1px solid $rose !important; + opacity: 1 !important; .square { - border: 1px solid $rose; - background-color: $rose; - cursor: default; + background-color: $rose !important; + border: 1px solid $rose !important; + cursor: default !important; .symbol { color: $aubergine; } } } + + &:has(.draw) { // Remove? + opacity: .1; + } } } /** Notification */ -.notification { +#notification { margin: 0 auto; height: 5rem; width: $boardLengthMax; letter-spacing: .5px; align-items: center; text-align: left; + display: none; + + &.active { + display: flex; + } &.error { margin: 2rem 1rem 0 1rem; width: 14rem; - .notification-content { + #notification-content { margin: 1rem; } } - &.active-True { display: flex } - &.active-False { display: none } - i { font-size: 1.5rem; margin: 0 1.5rem; @@ -392,7 +397,7 @@ header { } } - .restart { + #restart { cursor: pointer; } } diff --git a/src/resources/templates/game.html b/src/resources/templates/game.html index 8f988b5..dd18f14 100644 --- a/src/resources/templates/game.html +++ b/src/resources/templates/game.html @@ -8,17 +8,6 @@ - - - - - - - - - - -
badge @@ -46,21 +35,12 @@
- - - - - -
- - - - - - - +
+ +
+
@@ -72,30 +52,8 @@
- {% if gameMode == "STANDARD" %} -
- {% for square in board %} -
{{ square }}
- {% endfor %} -
- {% endif %} - - {% if gameMode == "ULTIMATE" %} -
- {% for outer_square in board %} -
-
- {% set outer_index = loop.index0 %} - {% for inner_square in outer_square %} -
-
{{ inner_square }}
-
- {% endfor %} -
-
- {% endfor %} -
- {% endif %} +
+
@@ -105,6 +63,11 @@