Files
fairhopper/README.md
2023-05-11 15:08:24 +02:00

612 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FairHopper
## Useful links
- [Frontend](https://fairhopper.mjerenja.com)
- [API](https://api.fairhopper.mjerenja.com)
- [API Docs](https://api.fairhopper.mjerenja.com/docs)
- [FairHopper](https://gitea.ekirin.com/Intis/fairhopper)
- [FairHopper SDK](https://gitea.ekirin.com/Intis/fairhopper-sdk)
- Websockets: wss://fairhopper.mjerenja.com/ws
## 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
scale 1024 width
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
### Start server as docker container
Build image:
```sh
docker build . -t CONTAINER_NAME
```
Create docker container:
```sh
docker \
create \
--publish EXTERNAL_API_PORT:8010 \
--publish EXTERNAL_WS_PORT:8011 \
--name=CONTAINER_NAME \
IMAGE_NAME
```
Parameters:
- `EXTERNAL_API_PORT` - REST API port
- `EXTERNAL_WS_PORT` - Websockets port
- `CONTAINER_NAME` - FairHopper container name
- `IMAGE_NAME` - FairHopper image name
Start docker container:
```sh
docker start CONTAINER_NAME -d
```
Stop docker container:
```sh
docker stop CONTAINER_NAME
```
Example:
```sh
docker build . -t fairhopper-service
docker \
run \
--publish 8010:8010 \
--publish 8011:8011 \
--name=fairhopper-service \
fairhopper \
--detach
docker start fairhopper-service -d
docker stop fairhopper-service
```
### Start server on local machine
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 "Flutter\nVisualisation\nService"
}
usecase ExtVis1 as "Visualisation\nClient"
usecase ExtVis2 as "Visualisation\nClient"
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 WS
Client2 ->o WS: Client connect
activate WS #coral
WS -> Client2: Game state
deactivate WS
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 WS
end
== Player reached destination ==
Game -> Game: Lock game for other players
activate Game
Game -> WS: Player reached destination
activate WS #coral
WS o-> Client1: Select product
WS o-> Client2: Select product
deactivate WS
deactivate Game
loop #lightyellow Product select countdown timer (60s)
Game ->o WS: Timer timeout
activate Game
activate WS #coral
WS o-> Client1: Cancel selection
WS o-> Client2: Cancel selection
deactivate WS
Game -> Game: Unlock game
deactivate Game
end
Client1 -> WS: Product selected
activate WS #coral
WS o-> Game: Product selected
activate Game
WS o-> Client2: Product selected
deactivate WS
Game -> Game: Unlock game
Game -> WS: Game state
activate WS #coral
WS o-> Client1: Game state
WS o-> Client2: Game state
deactivate WS
deactivate Game
```
## 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
{
"player": {
"id": "test-player-pero",
"name": "Pero",
"active": true,
"position": {
"x": 10,
"y": 10
},
"move_count": 1,
"move_attempt_count": 1,
"state": "ON_DESTINATION"
},
"products": [
{
"name": "CocaCola",
"id": "cocacola-id",
"description": null
},
{
"name": "Pepsi",
"id": "pepsi-id",
"description": null
},
{
"name": "Fanta",
"id": "fanta-id",
"description": null
},
{
"name": "Snickers",
"id": "snickers-id",
"description": null
},
{
"name": "Mars",
"id": "mars-id",
"description": null
},
{
"name": "Burek",
"id": "burek-id",
"description": null
}
],
"timeout": 5
}
```
### Product purchase timer tick
Message: `product_purchase_timer_tick`
Data:
```json
{
"time_left": 4,
"player": {
"id": "test-player-pero",
"name": "Pero",
"active": true,
"position": {
"x": 10,
"y": 10
},
"move_count": 1,
"move_attempt_count": 1,
"state": "ON_DESTINATION"
}
}
```
### Product purchase timer done
Message: `product_purchase_done`
Data:
```json
{
"player": {
"id": "test-player-pero",
"name": "Pero",
"active": true,
"position": {
"x": 10,
"y": 10
},
"move_count": 1,
"move_attempt_count": 1,
"state": "ON_DESTINATION"
},
"product": {
"name": "CocaCola",
"id": "cocacola-id",
"description": null
}
}
```
If product selection timeout occured, product will be null.