413e395a75b32879f6869ee50dabcf1c4bffafe2
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
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 <<end>>
state "End Game" as EndGame <<end>>
state "Unlock game" as UnlockGame <<end>>
[*] -> 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, ultimate dependency management software for Python.
Install Poetry:
pip install poetry
Install virtual environment:
poetry install
Setting up
Copy settings_template.py to settings.py.
Edit settings.py and customize application.
Starting FairHopper Game Server
make run
By default, FairHopper runs on port 8010. To run FairHopper on different port, start uvicorn directly:
poetry run uvicorn main:app --host 0.0.0.0 --port 8010 --workers=1
To activate virtual environment:
poetry shell
WebSockets server runs on port 8011. To run WS Server on different port, edit settings.py configuration.
System overview
Architecture
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
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"
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
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.
Start game
Endpoint: POST /game
Request body:
{
"player_name": "Pero"
}
Response body:
{
"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:
{
"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:
{
"player": {
"uuid": "string",
"name": "Pero",
"position": {
"x": 50,
"y": 50
},
"move_count": 10,
"move_attempt_count": 12
}
}
Get Game Info
GET /game
Response body:
{
"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:
{
"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
}
}
]
}
]
}
Description
Languages
Python
75.9%
JavaScript
11%
HTML
5%
Dockerfile
2.8%
Makefile
2.6%
Other
2.7%