# 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. ## 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 ``` ### Starting WebSockets Server ```sh make run-ws ``` WebSockets server runs on port **8011**. To run WS Server on different port, edit `settings.py` configuration. ## System overview ### Architecture ```plantuml actor "Player 1" as P1 actor "Player 2" as P2 actor "Player 3" as P3 package Masterpiece { usecase Game as "FairHopper Game Server" usecase WS as "WS Server" usecase Vis as "Visualisation\nService" } P1 -left-> Game: REST API P2 -left-> Game: REST API P3 -left-> Game: REST API Game --> WS: WebSockets WS --> Vis: WebSockets ``` ### WebSockets ```plantuml participant Game as "FairHopper Game Server" participant WS as "WS Server" participant Client1 as "Visualisation\nClient 1" participant Client2 as "Visualisation\nClient 2" Game ->o WS: Server Connect activate WS #coral WS -> Game: Get game state activate Game #yellow Game -> WS: Game state deactivate deactivate 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 ``` ## REST API - Start game - Move left - Move right - Move up - Move down - Get current position - 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": { "uuid": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25", "name": "Pero", "position": { "x": 0, "y": 10 }, "move_count": 0, "move_attempt_count": 0 } } ``` ### Player Move - POST `/player/{uuid}/move/left` - POST `/player/{uuid}/move/right` - POST `/player/{uuid}/move/up` - POST `/player/{uuid}/move/down` Request body: None Response code: - 200 OK: Destination reached - 201 Created: Player moved successfully - 403 Forbidden: Player uuid not valid, probably timeout - 409 Conflict: Invalid move, obstacle or position out of board - 422 Unprocessable Content: Validation error Response body: ```json { "player": { "uuid": "string", "name": "Pero", "position": { "x": 50, "y": 50 }, "move_count": 10, "move_attempt_count": 12 } } ``` ### Get Player Info GET `/player/{{uuid}}` Request body: None Response body: ```json { "player": { "uuid": "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 ### Game state structure URI: `/game-state` Data: ```json { "board": { "width": 21, "height": 21 }, "destination": { "position": { "x": 10, "y": 10 } }, "players": [ { "uuid": "test-player-id", "name": "Pero", "active": true, "position": { "x": 2, "y": 2 }, "move_count": 3, "move_attempt_count": 3 }, { "uuid": "95962b49-0003-4bf2-b205-71f2590f2318", "name": "Mirko", "active": true, "position": { "x": 0, "y": 0 }, "move_count": 15, "move_attempt_count": 20 } ], "layers": [ { "name": "obstacles", "objects": [ { "type": "OBSTACLE", "position": { "x": 4, "y": 2 } }, { "type": "OBSTACLE", "position": { "x": 4, "y": 13 } }, { "type": "OBSTACLE", "position": { "x": 18, "y": 18 } }, { "type": "OBSTACLE", "position": { "x": 5, "y": 4 } }, { "type": "OBSTACLE", "position": { "x": 7, "y": 10 } } ] }, { "name": "destination", "objects": [ { "type": "DESTINATION", "position": { "x": 10, "y": 10 } } ] }, { "name": "players", "objects": [ { "type": "PLAYER", "position": { "x": 2, "y": 2 } }, { "type": "PLAYER", "position": { "x": 0, "y": 0 } } ] } ] } ```