# FairHopper ## Game ### Overview - Rectangle board W × H - Destination: center of a board (W / 2, H / 2) - Initial player position: Random on board border - Available moves: - left - right - up - down - Optional on-board obstacles ### Rules - Goal: Reach the goal destination - Player can't move out of board - Player can't move if destination position contains obstacle - Move timeout: 10s. Game is finished if timeout ocurrs. ## Game States ```plantuml hide empty description state "Start Game" as StartGame state "Move" as MovePlayer: Destination reached? state "Destination Reached" as DestinationReached state "Product Selection" as ProductSelection: Enable product selection for winning player state "Product Selected" as ProductSelected state "Selection Timeout" as SelectionTimeout state "End Player's Game" as EndPlayer state "Lock Game" as LockGame <> state "End Game" as EndGame <> state "Unlock game" as UnlockGame <> [*] -> StartGame StartGame -> MovePlayer MovePlayer <-- MovePlayer: NO MovePlayer --> DestinationReached: YES DestinationReached --> ProductSelection DestinationReached -> LockGame: Lock game for all other players ProductSelection --> ProductSelected ProductSelection --> SelectionTimeout ProductSelected --> EndGame: End game\nfor all players SelectionTimeout -> EndPlayer EndPlayer --> UnlockGame: Unlock game\n for all players ``` ## FairHopper Game Server Requirements: - Python 3.10+ ### Install virtual envirnonment Project uses [Poetry](https://python-poetry.org), ultimate dependency management software for Python. Install Poetry: ```sh pip install poetry ``` Install virtual environment: ```sh poetry install ``` ### Setting up Copy `settings_template.py` to `settings.py`. Edit `settings.py` and customize application. ### Starting FairHopper Game Server ```sh make run ``` By default, FairHopper runs on port **8010**. To run FairHopper on different port, start `uvicorn` directly: ```sh poetry run uvicorn main:app --host 0.0.0.0 --port 8010 --workers=1 ``` To activate virtual environment: ```sh poetry shell ``` WebSockets server runs on port **8011**. To run WS Server on different port, edit `settings.py` configuration. ## System overview ### Architecture ```plantuml scale 1024 width actor "Player 1" as P1 actor "Player 2" as P2 actor "Player 3" as P3 package Masterpiece #seashell { rectangle "FairHopper Game Server" #lightcyan { usecase API as "API Server" usecase Game as "Game Engine" usecase WS as "WS Server" } usecase Vis as "Visualisation\nService" } usecase ExtVis1 as "Visualisation\nService" usecase ExtVis2 as "Visualisation\nService" P1 -left-> API: REST API P2 -left-> API: REST API P3 -left-> API: REST API API --> Game Game --> WS: Game State WS --> Vis: WS Game State WS --> ExtVis1: WS Game State WS --> ExtVis2: WS Game State ``` ### WebSockets ```plantuml scale 1024 width box "FairHopper Game Server" #lightcyan participant Game as "Game Engine" participant WS as "WS Server" endbox participant Client1 as "Visualisation\nClient 1" participant Client2 as "Visualisation\nClient 2" == Player movement mode == Game ->o WS: Send initial state Client1 ->o WS: Client connect activate WS #coral WS -> Client1: Game state deactivate Client2 ->o WS: Client connect activate WS #coral WS -> Client2: Game state deactivate loop #lightyellow On game state change Game ->o WS: Game state activate WS #coral WS o-> Client1: Game state WS o-> Client2: Game state deactivate end == Product purchase mode == Game -> WS: Purchase start activate WS #coral WS o-> Client1: Purchase start WS o-> Client2: Purchase start deactivate loop #lightyellow Purchase countdown timer Game ->o WS: Timer count down activate WS #coral WS o-> Client1: Purchase time left WS o-> Client2: Purchase time left deactivate end Game -> WS: Purchase done activate WS #coral WS o-> Client1: Purchase done WS o-> Client2: Purchase done deactivate ``` ## REST API - Start game - Move left - Move right - Move up - Move down - Get player info - Get board info Check REST API interface on [FastAPI docs](http://localhost:8010/docs). ### Start game **Endpoint**: POST `/game` Request body: ```json { "player_name": "Pero" } ``` Response body: ```json { "board": { "width": 101, "height": 101 }, "destination": { "position": { "x": 50, "y": 50 } }, "player": { "id": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25", "name": "Pero", "position": { "x": 0, "y": 10 }, "move_count": 0, "move_attempt_count": 0 } } ``` ### Player Move - POST `/player/{id}/move/left` - POST `/player/{id}/move/right` - POST `/player/{id}/move/up` - POST `/player/{id}/move/down` Request body: None Response code: - 200 OK: Destination reached - 201 Created: Player moved successfully - 403 Forbidden: Player id not valid, probably timeout - 409 Conflict: Invalid move, obstacle or position out of board - 422 Unprocessable Content: Validation error Response body: ```json { "player": { "id": "string", "name": "Pero", "position": { "x": 50, "y": 50 }, "move_count": 10, "move_attempt_count": 12 } } ``` ### Get Player Info GET `/player/{{id}}` Request body: None Response body: ```json { "player": { "id": "string", "name": "Pero", "position": { "x": 50, "y": 50 }, "move_count": 10, "move_attempt_count": 12 } } ``` ### Get Game Info GET `/game` Response body: ```json { "playerId": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25", "board": { "width": 101, "height": 101 }, "destinationPosition": { "x": 50, "y": 50 }, "playerPosition": { "x": 0, "y": 10 } } ``` ## WebSockets ### WS Data format - json ```json { "message": message_type, "data": ... } ``` ### Game state structure Message: `game_dump` Data: ```json { "board": { "width": 10, "height": 10 }, "destination": { "position": { "x": 5, "y": 5 } }, "players": [ { "id": "test-player-pero", "name": "Pero", "active": true, "position": { "x": 3, "y": 3 }, "move_count": 0, "move_attempt_count": 0, "state": "CREATED" }, { "id": "test-player-mirko", "name": "Mirko", "active": true, "position": { "x": 4, "y": 4 }, "move_count": 0, "move_attempt_count": 0, "state": "CREATED" } ], "layers": [ { "name": "obstacles", "objects": [ { "type": "OBSTACLE", "position": { "x": 0, "y": 6 } }, { "type": "OBSTACLE", "position": { "x": 5, "y": 1 } }, { "type": "OBSTACLE", "position": { "x": 1, "y": 6 } } ] }, { "name": "destination", "objects": [ { "type": "DESTINATION", "position": { "x": 5, "y": 5 } } ] }, { "name": "players", "objects": [ { "type": "PLAYER", "position": { "x": 3, "y": 3 } }, { "type": "PLAYER", "position": { "x": 4, "y": 4 } } ] } ] } ``` ### Product purchase start Message: `product_purchase_start` Data: ```json ``` ### Product purchase timer tick Message: `product_purchase_timer_tick` Data: ```json ``` ### Product purchase timer done Message: `product_purchase_done` Data: ```json ```