Skip to content

Commit

Permalink
[#12] chore : merge pull request #58 from branch feature/backend/star…
Browse files Browse the repository at this point in the history
…t-game
  • Loading branch information
metalalive authored Feb 24, 2024
2 parents aa9a5e2 + 33303b1 commit 9e38e61
Show file tree
Hide file tree
Showing 16 changed files with 308 additions and 28 deletions.
14 changes: 14 additions & 0 deletions backend/app/adapter/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
UpdateInvestigatorDto,
UpdateDifficultyDto,
UpdateCommonRespDto,
GameStartDto,
)
from app.usecase import (
CreateGameUseCase,
GetAvailableInvestigatorsUseCase,
SwitchInvestigatorUseCase,
UpdateGameDifficultyUseCase,
GameStartUseCase,
)
from app.config import LOG_FILE_PATH, REST_HOST, REST_PORT
from app.domain import GameError
Expand Down Expand Up @@ -99,6 +101,18 @@ async def update_game_difficulty(game_id: str, req: UpdateDifficultyDto):
return GameErrorHTTPResponse(e)


@_router.patch("/games/{game_id}/start", status_code=200)
async def start_game(game_id: str, req: GameStartDto):
uc = GameStartUseCase(
repository=shared_context["repository"],
evt_emitter=shared_context["evt_emit"],
)
try:
await uc.execute(game_id, req.player_id)
except GameError as e:
return GameErrorHTTPResponse(e)


@asynccontextmanager
async def lifetime_server_context(app: FastAPI):
# TODO, parameters should be in separate python module or `json` , `toml` file
Expand Down
12 changes: 11 additions & 1 deletion backend/app/adapter/event_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
import logging
import socketio

from app.dto import Investigator, Difficulty, RtcCharacterMsgData, RtcDifficultyMsgData
from app.dto import (
Investigator,
Difficulty,
RtcCharacterMsgData,
RtcDifficultyMsgData,
RtcGameStartMsgData,
)
from app.config import LOG_FILE_PATH, RTC_HOST, RTC_PORT
from app.constant import RealTimeCommConst as RtcConst, GameRtcEvent
from app.domain import Game
Expand Down Expand Up @@ -45,6 +51,10 @@ async def update_difficulty(self, game_id: str, level: Difficulty):
data = RtcDifficultyMsgData.serialize(game_id, level)
await self.do_emit(data, evt=RtcConst.EVENTS.DIFFICULTY)

async def start_game(self, game_id: str, player_id: str):
data = RtcGameStartMsgData.serialize(game_id, player_id)
await self.do_emit(data, evt=RtcConst.EVENTS.GAME_START)

async def do_emit(self, data: Union[Dict, bytes], evt: GameRtcEvent):
try:
if not self._client.connected:
Expand Down
12 changes: 12 additions & 0 deletions backend/app/adapter/sio_srv.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
RtcInitMsgData,
RtcCharacterMsgData,
RtcDifficultyMsgData,
RtcGameStartMsgData,
)
from app.domain import GameError, GameErrorCodes, GameFuncCodes

Expand Down Expand Up @@ -177,6 +178,17 @@ async def _update_game_difficulty(sid, data: Dict):
)


@srv.on(RtcConst.EVENTS.GAME_START.value, namespace=RtcConst.NAMESPACE)
async def _start_game(sid, data: bytes):
# TODO, ensure this event is sent by authorized http server
await _generic_forward_msg(
sid,
data,
evt=RtcConst.EVENTS.GAME_START,
validator=RtcGameStartMsgData.deserialize,
)


def gen_srv_task(host: str):
cfg = Config()
cfg.bind = [host]
Expand Down
1 change: 1 addition & 0 deletions backend/app/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class GameRtcEvent(Enum):
NEW_ROOM = "new_room"
CHARACTER = "character"
DIFFICULTY = "difficulty"
GAME_START = "game_start"


class RealTimeCommConst:
Expand Down
27 changes: 27 additions & 0 deletions backend/app/domain/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class GameErrorCodes(Enum):
INVESTIGATOR_CHOSEN = (1003, 409)
INVALID_PLAYER = (1004, 422)
GAME_NOT_FOUND = (1005, 404)
PLAYER_ALREADY_STARTED = (1006, 400)


class GameFuncCodes(Enum):
Expand All @@ -26,6 +27,7 @@ class GameFuncCodes(Enum):
SWITCH_CHARACTER = 1003
CLEAR_CHARACTER_SELECTION = 1004
UPDATE_DIFFICULTY = 1005
START_GAME = 1006
## TODO, rename the following members
USE_CASE_EXECUTE = 1099
RTC_ENDPOINT = 1098 ## for real-time communication like socket.io server endpoint
Expand Down Expand Up @@ -121,6 +123,11 @@ def switch_character(self, player_id: str, new_invstg: Investigator):
e_code=GameErrorCodes.INVALID_PLAYER,
fn_code=GameFuncCodes.SWITCH_CHARACTER,
)
if player.started:
raise GameError(
e_code=GameErrorCodes.PLAYER_ALREADY_STARTED,
fn_code=GameFuncCodes.SWITCH_CHARACTER,
)
old_invstg = player.get_investigator()
try:
if old_invstg:
Expand All @@ -140,3 +147,23 @@ def filter_unselected_investigators(self, num: int) -> List[Investigator]:

def update_difficulty(self, difficulty: Difficulty):
self._difficulty = difficulty

def start(self, player_id: str):
player = self.get_player(player_id)
if player is None:
raise GameError(
e_code=GameErrorCodes.INVALID_PLAYER,
fn_code=GameFuncCodes.START_GAME,
)
if player.get_investigator() is None:
raise GameError(
e_code=GameErrorCodes.INVALID_INVESTIGATOR,
fn_code=GameFuncCodes.START_GAME,
)
player.start()
all_started = all([p.started for p in self.players])
if all_started:
pass
# TODO
# - initialize all types of cards, card deck, map (number of cultists in each
# location), player status e.g. sanity points
22 changes: 17 additions & 5 deletions backend/app/domain/player.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
from typing import Optional
from app.dto import Investigator


class Player:
def __init__(self, id: str, nickname: str):
self._id = id
self._nickname = nickname
self._investigator = None
self._nickname: str = nickname
self._investigator: Optional[Investigator] = None
self._rdy_start: bool = False

def set_investigator(self, investigator):
self._investigator = investigator
def set_investigator(self, value: Investigator):
self._investigator = value

def get_investigator(self):
def get_investigator(self) -> Optional[Investigator]:
return self._investigator

@property
def id(self):
return self._id

@property
def started(self) -> bool:
return self._rdy_start

def start(self):
self._rdy_start = True
31 changes: 31 additions & 0 deletions backend/app/dto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Investigator as InvestigatorFbs,
DifficultyConfig,
Difficulty as DifficultyFbs,
GameStart as GameStartFbs,
)

# data transfer objects (DTO) in the application
Expand Down Expand Up @@ -136,6 +137,11 @@ class UpdateCommonRespDto(BaseModel):
message: str


class GameStartDto(BaseModel):
model_config = ConfigDict(extra="forbid")
player_id: str


class RtcRoomMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
Expand Down Expand Up @@ -211,3 +217,28 @@ def deserialize(data: bytes):
game_id = obj.GameId().decode("utf-8")
lvl = Difficulty.from_fbs(obj.Level())
return RtcDifficultyMsgData(gameID=game_id, level=lvl)


class RtcGameStartMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
player_id: str

def serialize(game_id: str, player_id: str) -> bytes:
builder = flatbuffers.Builder(128)
game_id = builder.CreateString(game_id)
player_id = builder.CreateString(player_id)
GameStartFbs.Start(builder)
GameStartFbs.AddGameId(builder, game_id)
GameStartFbs.AddPlayerId(builder, player_id)
starter = GameStartFbs.End(builder)
builder.Finish(starter)
serial = builder.Output() # byte-array
return bytes(serial)

def deserialize(data: bytes):
buf = bytearray(data)
obj = GameStartFbs.GameStart.GetRootAs(buf, offset=0)
game_id = obj.GameId().decode("utf-8")
player = obj.PlayerId().decode("utf-8")
return RtcGameStartMsgData(gameID=game_id, player_id=player)
5 changes: 5 additions & 0 deletions backend/app/dto/rtc.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ table DifficultyConfig {
level: Difficulty;
}

table GameStart {
game_id: string (required);
player_id: string (required);
}

78 changes: 78 additions & 0 deletions backend/app/dto/rtc/GameStart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# automatically generated by the FlatBuffers compiler, do not modify

# namespace:

import flatbuffers
from flatbuffers.compat import import_numpy

np = import_numpy()


class GameStart(object):
__slots__ = ["_tab"]

@classmethod
def GetRootAs(cls, buf, offset=0):
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = GameStart()
x.Init(buf, n + offset)
return x

@classmethod
def GetRootAsGameStart(cls, buf, offset=0):
"""This method is deprecated. Please switch to GetRootAs."""
return cls.GetRootAs(buf, offset)

# GameStart
def Init(self, buf, pos):
self._tab = flatbuffers.table.Table(buf, pos)

# GameStart
def GameId(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None

# GameStart
def PlayerId(self):
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
if o != 0:
return self._tab.String(o + self._tab.Pos)
return None


def GameStartStart(builder):
builder.StartObject(2)


def Start(builder):
GameStartStart(builder)


def GameStartAddGameId(builder, gameId):
builder.PrependUOffsetTRelativeSlot(
0, flatbuffers.number_types.UOffsetTFlags.py_type(gameId), 0
)


def AddGameId(builder, gameId):
GameStartAddGameId(builder, gameId)


def GameStartAddPlayerId(builder, playerId):
builder.PrependUOffsetTRelativeSlot(
1, flatbuffers.number_types.UOffsetTFlags.py_type(playerId), 0
)


def AddPlayerId(builder, playerId):
GameStartAddPlayerId(builder, playerId)


def GameStartEnd(builder):
return builder.EndObject()


def End(builder):
return GameStartEnd(builder)
1 change: 1 addition & 0 deletions backend/app/usecase/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
GetAvailableInvestigatorsUseCase, # noqa: F401
SwitchInvestigatorUseCase, # noqa: F401
UpdateGameDifficultyUseCase, # noqa: F401
GameStartUseCase, # noqa: F401
)
14 changes: 14 additions & 0 deletions backend/app/usecase/config_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,17 @@ async def execute(self, game_id: str, level: Difficulty) -> UpdateCommonRespDto:
await self.evt_emitter.update_difficulty(game_id, level)
message = "Update Game {} Difficulty Successfully".format(game.id)
return UpdateCommonRespDto(message=message)


class GameStartUseCase(AbstractUseCase):
async def execute(self, game_id: str, player_id: str):
game = await self.repository.get_game(game_id)
if game is None:
raise GameError(
e_code=GameErrorCodes.GAME_NOT_FOUND,
fn_code=GameFuncCodes.USE_CASE_EXECUTE,
)
game.start(player_id)
await self.repository.save(game)
if self.evt_emitter:
await self.evt_emitter.start_game(game_id, player_id)
33 changes: 29 additions & 4 deletions backend/tests/e2e/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,35 @@ def test_update_game_difficulty_ok(self, test_client):
message = respbody.get("message")
assert message == "Update Game {} Difficulty Successfully".format(game_id)

def test_update_game_difficulty_nonexist_game(self, test_client):
url = "/games/{}/difficulty"
reqbody = {"level": "standard"}
response = test_client.patch(url.format("xxxxx"), headers={}, json=reqbody)
@pytest.mark.parametrize(
"method, url_pattern, reqbody",
[
(
"patch",
"/games/{}/investigator",
{"investigator": "hunter", "player_id": "8964"},
),
("patch", "/games/{}/difficulty", {"level": "standard"}),
("patch", "/games/{}/start", {"player_id": "996"}),
],
)
def test_update_game_state_nonexist(
self, test_client, method, url_pattern, reqbody
):
response = test_client.request(
method, url_pattern.format("xxxxx"), headers={}, json=reqbody
)
assert response.status_code == 404
error_detail = response.json()
assert error_detail["reason"] == GameErrorCodes.GAME_NOT_FOUND.value[0]

def test_game_start_ok(self, test_client):
game_id = self.create_game_common(test_client)
response = test_client.patch(
"/games/{}/start".format(game_id), headers={}, json={"player_id": "9487"}
)
assert response.status_code == 200
response = test_client.patch(
"/games/{}/start".format(game_id), headers={}, json={"player_id": "9527"}
)
assert response.status_code == 200
Loading

0 comments on commit 9e38e61

Please sign in to comment.