Skip to content

Commit

Permalink
Merge pull request #13 from jrsmth/feature/ultimate/main
Browse files Browse the repository at this point in the history
Feature/ultimate/main
  • Loading branch information
jrsmth authored Nov 11, 2023
2 parents 873e0d6 + b7cd4b9 commit edb63ac
Show file tree
Hide file tree
Showing 24 changed files with 933 additions and 290 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `0.0.x` : End-to-end Flask PoC
- `0.1.x` : GitHub Actions
- `0.2.x` : Simple Tic Tac Toe (3 x 3)
- `0.3.x` : Implement ultimate edition (9 x 9)

<br>

# Releases
<!-- @LatestFirst -->

## [0.3.0] - ???
- UMA-17: Give the user the option of standard or ultimate tic-tac-toe
- UMA-19: Create interactive 9x9 board and allow player to make a turn
- UMA-20: Force ultimate players to play in the correct square after a move is placed
- UMA-21: Make game completable through correct outer square selection and evaluation

## [0.2.1] - 07/11/2023
- UMA-11: Allow players to make their turns
- UMA-12: Display results to each player when the game is over
Expand Down Expand Up @@ -47,4 +54,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[0.1.1]: https://github.com/jrsmth/ultima/compare/0.1.0...0.1.1
[0.1.2]: https://github.com/jrsmth/ultima/compare/0.1.1...0.1.2
[0.2.0]: https://github.com/jrsmth/ultima/compare/0.1.2...0.2.0
[0.2.1]: https://github.com/jrsmth/ultima/compare/0.2.0...0.2.1
[0.2.1]: https://github.com/jrsmth/ultima/compare/0.2.0...0.2.1
[0.3.0]: https://github.com/jrsmth/ultima/compare/0.2.1...0.3.0
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ make build:
env = "dev"
# local : make sure redis is running
# dev : ensure IP is whitelisted on render redis
# prod :
# prod
make start:
export FLASK_ENV=$(env) && make build && gunicorn -b 0.0.0.0:8080 --chdir src/app app:app

Expand Down
122 changes: 98 additions & 24 deletions src/app/game/game.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import render_template, url_for, redirect, Blueprint

from src.app.model.board.threeboard import ThreeBoard
from src.app.model.mood import Mood
from src.app.model.status import Status
from src.version.version import __version__
Expand All @@ -25,7 +26,10 @@ def game(game_id, user_id):
else:
player_two_active = True

game_state = get_game_state(redis)
game_mode = redis.get("gameMode")
board = redis.get_complex("board")
game_state = get_game_state(redis, board)

print(game_state)
print("user id")
print(user_id)
Expand All @@ -38,34 +42,37 @@ def game(game_id, user_id):
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.1.icon")
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_icon = messages.load("game.end.win.1.icon")
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_icon = messages.load("game.end.lose.3.icon")
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_icon = messages.load("game.end.win.3.icon")
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_icon = messages.load("game.end.lose.1.icon")
notification_mood = Mood.SAD.value

return render_template(
"game.html",
board=board,
playableSquare=redis.get("playableSquare"),
innerStates=redis.get_complex("innerStates"),
zero=redis.get("0"),
one=redis.get("1"),
two=redis.get("2"),
Expand All @@ -77,6 +84,7 @@ def game(game_id, user_id):
eight=redis.get("8"),
gameComplete=game_complete,
gameId=game_id,
gameMode=game_mode,
notificationActive=notification_active,
notificationHeader=notification_header,
notificationMessage=notification_message,
Expand All @@ -93,10 +101,12 @@ def game(game_id, user_id):
)

@game_page.route("/game/<game_id>/place-move/<user_id>/<square>")
def place_move(game_id, user_id, square):
def place_standard_move(game_id, user_id, square):
# Set player's move
symbol = redis.get(user_id)
redis.set(square, symbol)
board_list = redis.get_complex("board")
board_list[int(square)] = int(symbol)
redis.set_complex("board", board_list)

# Switch player turn
if redis.get("whoseTurn") == 'player1':
Expand All @@ -106,16 +116,39 @@ def place_move(game_id, user_id, square):

return redirect(url_for("game_page.game", game_id=game_id, user_id=user_id))

return game_page
@game_page.route("/game/<game_id>/place-move/<user_id>/<outer_square>/<inner_square>")
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)

print("[place_ultimate_move] outer_square: " + outer_square)
print("[place_ultimate_move] inner_square: " + inner_square)
print(board)

# 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...
else:
redis.set("playableSquare", inner_square)

print("[place_ultimate_move] playableSquare: " + redis.get("playableSquare"))

# Switch player turn
if redis.get("whoseTurn") == 'player1':
redis.set("whoseTurn", "player2")
elif redis.get("whoseTurn") == 'player2':
redis.set("whoseTurn", "player1")

return redirect(url_for("game_page.game", game_id=game_id, user_id=user_id))

# Closing return
return game_page

def get_game_state(redis):
board = {
"0": redis.get("0"), "1": redis.get("1"), "2": redis.get("2"),
"3": redis.get("3"), "4": redis.get("4"), "5": redis.get("5"),
"6": redis.get("6"), "7": redis.get("7"), "8": redis.get("8"),
}

def get_game_state(redis, board):
winning_combos = [
[0, 1, 2],
[3, 4, 5],
Expand All @@ -127,22 +160,35 @@ def get_game_state(redis):
[2, 4, 6]
]

print(board)

if list(board.values()).count("0") == 0:
if isinstance(board[0], list):
print("[get_game_state] board[0]: " + str(board[0]))
outer_state = Status.IN_PROGRESS
inner_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 inner_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(list(board.values()).count("1")))
if (list(board.values()).count("1")) >= 3:
player_moves = get_player_moves("1", board)
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

if (list(board.values()).count("2")) >= 3:
player_moves = get_player_moves("2", board)
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
Expand All @@ -151,4 +197,32 @@ def get_game_state(redis):


def get_player_moves(player, board):
return [int(k) for k, v in board.items() if v == player]
player_moves = []
for index in range(len(board)):
if board[index] == player:
player_moves.append(index)

return player_moves


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


def create_false_board(states):
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])

print("[create_false_board] board: " + str(board.list()))
return board.list()
45 changes: 32 additions & 13 deletions src/app/login/login.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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.status import Status


# Login Logic
Expand All @@ -13,19 +15,35 @@ def login():
if request.method == "POST":
print(request.form["name"]) # log me
print(request.form["gameId"])
board = ThreeBoard()
board_list = ThreeBoard.list(board)
# first index is squareNo
# second index is the state: 0 for empty, 1 for cross, 2 for circle
redis.set("0", board_list[0])
redis.set("1", board_list[1])
redis.set("2", board_list[2])
redis.set("3", board_list[3])
redis.set("4", board_list[4])
redis.set("5", board_list[5])
redis.set("6", board_list[6])
redis.set("7", board_list[7])
redis.set("8", board_list[8])
print(request.form["gameMode"])

game_mode = request.form["gameMode"]
if game_mode == "STANDARD":
redis.set("gameMode", game_mode)
board = ThreeBoard()
board_list = ThreeBoard.list(board)
# second index is the state: 0 for empty, 1 for cross, 2 for circle
redis.set_complex("board", board_list)
redis.set_complex("innerStates", [])

elif game_mode == "ULTIMATE":
redis.set("gameMode", game_mode)
board = NineBoard()
board_list = NineBoard.list(board)
# second index is the state: 0 for empty, 1 for cross, 2 for circle
redis.set_complex("board", board_list)
inner_states = [
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
]
redis.set_complex("innerStates", inner_states)
redis.set("playableSquare", "-1") # -1 is all squares...

else:
print("Game Mode already set [" + redis.get("gameMode") + "]") # TODO :: err handle

print(redis.get_complex("board"))

# TODO :: set redis obj as dict { $game_id: [player1: "", player2: ""] }
if request.form["gameId"] == "":
Expand All @@ -45,6 +63,7 @@ def login():

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

# Closing return
return login_page


Expand Down
4 changes: 3 additions & 1 deletion src/app/model/board/nineboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ class NineBoard(Board):
bot_rhs = ThreeBoard()

def list(self):
return
return [self.top_lhs.list(), self.top_mid.list(), self.top_rhs.list(),
self.mid_lhs.list(), self.mid_mid.list(), self.mid_rhs.list(),
self.bot_lhs.list(), self.bot_mid.list(), self.bot_rhs.list()]
18 changes: 17 additions & 1 deletion src/app/util/redis.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from flask_redis import FlaskRedis


Expand All @@ -13,4 +14,19 @@ def get(self, key):

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

def set_complex(self, key, complex_value):
json_value = json.dumps(
complex_value,
default=lambda o: o.__dict__,
sort_keys=True,
indent=4
)
return self.client.set(key, json_value)
# Set a complex key-value element by converting to json string

def get_complex(self, key):
json_value = self.client.get(key).decode('utf-8')
return json.loads(json_value)
# Get a complex key-value element by converting from json string
14 changes: 4 additions & 10 deletions src/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,19 @@ login.error.invalid-game-id=Invalid Game ID: Please try again or leave blank to

## Game
game.end.draw.header=Draw
game.end.draw.1.icon=fa-solid fa-face-relieved
game.end.draw.icon=fa-solid fa-face-surprise
game.end.draw.1.message=Phew, what a battle!
game.end.draw.2.icon=fa-solid fa-face-rolling-eyes
game.end.draw.2.message=Well that was waste of time...
game.end.draw.3.icon= fa-solid fa-face-meh-blank
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.win.header=Congratulations, {0}!
game.end.win.1.icon=fa-solid fa-face-smile-wink
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.icon=fa-solid fa-face-smile
game.end.win.2.message=Life shrinks or expands in proportion to one's courage
game.end.win.3.icon=fa-solid fa-face-smile-beam
game.end.win.3.message=Winning isn't everything, but wanting it is

game.end.lose.header=Commiserations, {0}
game.end.lose.1.icon=fa-solid fa-face-dizzy
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.icon=fa-solid fa-face-grimace
game.end.lose.2.message=Fight to avoid defeat, a surefire losing strategy
game.end.lose.3.icon=fa-solid fa-face-sad-cry
game.end.lose.3.message=Accept finite disappointment, never lose infinite hope
Loading

0 comments on commit edb63ac

Please sign in to comment.