Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Emit events on difficulty and character update #55

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions backend/app/adapter/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ async def read_unselected_investigators(game_id: str):

@_router.patch("/games/{game_id}/investigator", status_code=200)
async def switch_investigator(game_id: str, req: UpdateInvestigatorDto):
uc = SwitchInvestigatorUseCase(shared_context["repository"])
uc = SwitchInvestigatorUseCase(
shared_context["repository"],
evt_emitter=shared_context["evt_emit"],
)
try:
await uc.execute(game_id, req.player_id, req.investigator)
except GameError as e:
Expand All @@ -85,7 +88,9 @@ async def switch_investigator(game_id: str, req: UpdateInvestigatorDto):
@_router.patch("/games/{game_id}/difficulty", response_model=UpdateCommonRespDto)
async def update_game_difficulty(game_id: str, req: UpdateDifficultyDto):
uc = UpdateGameDifficultyUseCase(
repository=shared_context["repository"], settings=shared_context["settings"]
repository=shared_context["repository"],
evt_emitter=shared_context["evt_emit"],
settings=shared_context["settings"],
)
try:
response = await uc.execute(game_id, req.level)
Expand Down
33 changes: 30 additions & 3 deletions backend/app/adapter/event_emitter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Dict
import logging
import socketio

from app.dto import Investigator, Difficulty
from app.config import LOG_FILE_PATH, RTC_HOST, RTC_PORT
from app.constant import RealTimeCommConst as RtcConst
from app.constant import RealTimeCommConst as RtcConst, GameRtcEvent
from app.domain import Game

_logger = logging.getLogger(__name__)
Expand All @@ -14,6 +16,14 @@ class AbsEventEmitter:
async def create_game(self, game: Game):
raise NotImplementedError("AbsEventEmitter.create_game")

async def switch_character(
self, game_id: str, player_id: str, new_invstg: Investigator
):
raise NotImplementedError("AbsEventEmitter.switch_character")

async def update_difficulty(self, game_id: str, level: Difficulty):
raise NotImplementedError("AbsEventEmitter.update_difficulty")


class SocketIoEventEmitter(AbsEventEmitter):
def __init__(self):
Expand All @@ -23,12 +33,29 @@ def __init__(self):

async def create_game(self, game: Game):
data = {"gameID": game.id, "players": [p.id for p in game.players]}
await self.do_emit(data, evt=RtcConst.EVENTS.NEW_ROOM)

async def switch_character(
self, game_id: str, player_id: str, new_invstg: Investigator
):
data = {
"gameID": game_id,
"player_id": player_id,
"investigator": new_invstg.value,
}
await self.do_emit(data, evt=RtcConst.EVENTS.CHARACTER)

async def update_difficulty(self, game_id: str, level: Difficulty):
data = {"gameID": game_id, "level": level.value}
await self.do_emit(data, evt=RtcConst.EVENTS.DIFFICULTY)

async def do_emit(self, data: Dict, evt: GameRtcEvent):
try:
if not self._client.connected:
await self._client.connect(self._url, namespace=self._namespace)
await self._client.emit(RtcConst.EVENTS.NEW_ROOM.value, data=data)
await self._client.emit(evt.value, data=data)
except Exception as e:
_logger.error("send new-room event: %s", e)
_logger.error("emit event: %s", e)
# TODO, reconnect if connection inactive

async def deinit(self):
Expand Down
46 changes: 35 additions & 11 deletions backend/app/adapter/sio_srv.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
import socketio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from pydantic import ValidationError
from pydantic import BaseModel, ValidationError

from app.constant import RealTimeCommConst as RtcConst
from app.constant import RealTimeCommConst as RtcConst, GameRtcEvent
from app.config import RTC_HOST, RTC_PORT, LOG_FILE_PATH
from app.adapter.repository import get_rtc_room_repository
from app.dto import RtcRoomMsgData, ChatMsgData, RtcInitMsgData
from app.dto import (
RtcRoomMsgData,
ChatMsgData,
RtcInitMsgData,
RtcCharacterMsgData,
RtcDifficultyMsgData,
)
from app.domain import GameError, GameErrorCodes, GameFuncCodes

srv = socketio.AsyncServer(async_mode="asgi")
Expand All @@ -27,22 +33,24 @@
_repo = get_rtc_room_repository()


@srv.on(RtcConst.EVENTS.CHAT.value, namespace=RtcConst.NAMESPACE)
async def _forward_chat_msg(sid, data: Dict):
async def _generic_forward_msg(sid, data: Dict, evt: GameRtcEvent, msgtype: BaseModel):
try:
ChatMsgData(**data)
validated = msgtype(**data)
await srv.emit(
RtcConst.EVENTS.CHAT.value,
evt.value,
data,
namespace=RtcConst.NAMESPACE,
room=data["gameID"],
room=validated.gameID,
skip_sid=sid,
)
except ValidationError as e:
error = e.errors(include_url=False, include_input=False)
await srv.emit(
RtcConst.EVENTS.CHAT.value, data=error, namespace=RtcConst.NAMESPACE, to=sid
)
await srv.emit(evt.value, data=error, namespace=RtcConst.NAMESPACE, to=sid)


@srv.on(RtcConst.EVENTS.CHAT.value, namespace=RtcConst.NAMESPACE)
async def _forward_chat_msg(sid, data: Dict):
await _generic_forward_msg(sid, data, evt=RtcConst.EVENTS.CHAT, msgtype=ChatMsgData)


async def check_room_exist(repo, data: RtcInitMsgData):
Expand Down Expand Up @@ -136,6 +144,22 @@ async def _new_game_room(sid, data: Dict):
_logger.error("%s", e)


@srv.on(RtcConst.EVENTS.CHARACTER.value, namespace=RtcConst.NAMESPACE)
async def _player_switch_character(sid, data: Dict):
# TODO, ensure this event is sent by authorized http server
await _generic_forward_msg(
sid, data, evt=RtcConst.EVENTS.CHARACTER, msgtype=RtcCharacterMsgData
)


@srv.on(RtcConst.EVENTS.DIFFICULTY.value, namespace=RtcConst.NAMESPACE)
async def _update_game_difficulty(sid, data: Dict):
# TODO, ensure this event is sent by authorized http server
await _generic_forward_msg(
sid, data, evt=RtcConst.EVENTS.DIFFICULTY, msgtype=RtcDifficultyMsgData
)


def gen_srv_task(host: str):
cfg = Config()
cfg.bind = [host]
Expand Down
2 changes: 2 additions & 0 deletions backend/app/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class GameRtcEvent(Enum):
DEINIT = "deinit"
CHAT = "chat"
NEW_ROOM = "new_room"
CHARACTER = "character"
DIFFICULTY = "difficulty"


class RealTimeCommConst:
Expand Down
18 changes: 18 additions & 0 deletions backend/app/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@


class PlayerDto(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
nickname: str


class CreateGameReqDto(BaseModel):
model_config = ConfigDict(extra="forbid")
players: List[PlayerDto]


Expand All @@ -37,13 +39,15 @@ class Investigator(Enum):


class SingleInvestigatorDto(BaseModel):
model_config = ConfigDict(extra="forbid")
investigator: Investigator


ListInvestigatorsDto = RootModel[List[SingleInvestigatorDto]]


class UpdateInvestigatorDto(BaseModel):
model_config = ConfigDict(extra="forbid")
investigator: Investigator
player_id: str

Expand All @@ -58,6 +62,7 @@ class Difficulty(Enum):


class UpdateDifficultyDto(BaseModel):
model_config = ConfigDict(extra="forbid")
level: Difficulty


Expand All @@ -84,3 +89,16 @@ class RtcInitMsgData(BaseModel):
player: PlayerDto
gameID: str
client: str # client session ID


class RtcCharacterMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
investigator: Investigator
player_id: str


class RtcDifficultyMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
level: Difficulty
4 changes: 4 additions & 0 deletions backend/app/usecase/config_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ async def execute(self, game_id: str, player_id: str, new_invstg: Investigator):

game.switch_character(player_id, new_invstg)
await self.repository.save(game)
if self.evt_emitter:
await self.evt_emitter.switch_character(game_id, player_id, new_invstg)


class UpdateGameDifficultyUseCase(AbstractUseCase):
Expand All @@ -83,5 +85,7 @@ async def execute(self, game_id: str, level: Difficulty) -> UpdateCommonRespDto:
)
game.update_difficulty(level)
await self.repository.save(game)
if self.evt_emitter:
await self.evt_emitter.update_difficulty(game_id, level)
message = "Update Game {} Difficulty Successfully".format(game.id)
return UpdateCommonRespDto(message=message)
62 changes: 62 additions & 0 deletions backend/tests/e2e/test_socketio.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ async def verify_chat(self, expect_sender, expect_error: Dict = None):
self._msg_log.append(item)
assert self.messages_log == expect_sender.messages_log

async def verify_character_update(self, expect_player, expect_character: str):
evts: List = await self._sio_client.receive(timeout=3)
assert len(evts) == 2
assert evts[0] == RtcConst.EVENTS.CHARACTER.value
assert evts[1]["player_id"] == expect_player.player_id
assert evts[1]["investigator"] == expect_character

async def verify_difficulty(self, expect: str):
evts: List = await self._sio_client.receive(timeout=3)
assert len(evts) == 2
assert evts[0] == RtcConst.EVENTS.DIFFICULTY.value
assert evts[1]["level"] == expect


class MockiHttpServer(MockiAbstractClient):
async def new_room(self, room_id: str, members: List[str]):
Expand All @@ -115,6 +128,14 @@ async def new_room(self, room_id: str, members: List[str]):
},
)

async def switch_character(self, room_id: str, player: str, character: str):
data = {"player_id": player, "gameID": room_id, "investigator": character}
await self._sio_client.emit(RtcConst.EVENTS.CHARACTER.value, data=data)

async def set_difficulty(self, room_id: str, level: str):
data = {"level": level, "gameID": room_id}
await self._sio_client.emit(RtcConst.EVENTS.DIFFICULTY.value, data=data)


class TestRealTimeComm:
@pytest.mark.asyncio
Expand Down Expand Up @@ -205,3 +226,44 @@ async def test_enter_room(self):
await client.leave(room_id="c0034")
await client.disconnect()
await http_server.disconnect()

@pytest.mark.asyncio
async def test_forward_game_state_msg(self):
http_server = MockiHttpServer(RtcConst.NAMESPACE)
clients = [
MockClient(nickname="Fabio", player_id="u0017"),
MockClient(nickname="Von Mc", player_id="u0018"),
]
game_room = "a0020"

await http_server.connect(SERVER_URL)
await http_server.new_room(game_room, members=[c.player_id for c in clients])
for client in clients:
await client.connect(SERVER_URL)
await client.join(room_id=game_room)

await clients[0].verify_join(clients[:], True)
await clients[1].verify_join(clients[1:], True)

await http_server.switch_character(
game_room, clients[1].player_id, character="magician"
)
await clients[0].verify_character_update(clients[1], "magician")
await clients[1].verify_character_update(clients[1], "magician")
await http_server.switch_character(
game_room, clients[0].player_id, character="driver"
)
await clients[0].verify_character_update(clients[0], "driver")
await clients[1].verify_character_update(clients[0], "driver")

await http_server.set_difficulty(game_room, level="expert")
await clients[0].verify_difficulty("expert")
await clients[1].verify_difficulty("expert")
await http_server.set_difficulty(game_room, level="standard")
await clients[0].verify_difficulty("standard")
await clients[1].verify_difficulty("standard")

for client in clients:
await client.leave(room_id=game_room)
await client.disconnect()
await http_server.disconnect()
2 changes: 1 addition & 1 deletion backend/tests/unit/usecase/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,6 @@ async def test_ok(self):
repository = MockGameRepository(mock_fetched=mockgame)
settings = {"host": "unit.test.app.com"}
data = UpdateDifficultyDto(level="standard")
uc = UpdateGameDifficultyUseCase(repository, settings)
uc = UpdateGameDifficultyUseCase(repository, settings=settings)
resp = await uc.execute(mockgame.id, data.level)
assert resp.message is not None
Loading