Files
fairhopper/README.md
2023-05-12 09:30:50 +02:00

9.8 KiB
Raw Blame History

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

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 "Unlock game and restart" 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 --> UnlockGame: Unlock game\nand restart
SelectionTimeout -> EndPlayer
EndPlayer --> UnlockGame: Unlock game\nand restart

FairHopper Game Server

Start server as docker container

Build image:

docker build . -t CONTAINER_NAME

Create docker container:

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:

docker start CONTAINER_NAME -d

Stop docker container:

docker stop CONTAINER_NAME

Example:

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, 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

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

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 #skyblue
  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 #skyblue
    activate WS #coral
      WS o-> Client1: Selection timeout
      WS o-> Client2: Selection timeout
    deactivate WS
    Game -> Game: Unlock game
  deactivate Game
end

Client1 -> Client1: Product selection
activate Client1 #greenyellow
  Client1 -> Client1: Dispense product
  Client1 ->o WS: Product selected
deactivate Client1

activate WS #coral
	WS o-> Game: Product selected
    activate Game #skyblue
	WS o-> Client2: Product selected
deactivate WS


Game -> Game: Unlock game
    Game ->o 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.

Start game

Endpoint: POST /game

Request body:

{
  "player_name": "Pero"
}

Response body:

{
  "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
  • 423 Locked: Game locked, product selection in progress

Response body:

{
  "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:

{
  "player": {
    "id": "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
{
  "message": message_type,
  "data": ...
}

Game state structure

Direction: Game server -> Clients

Message: game_dump

Data:

{
  "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
          }
        }
      ]
    }
  ]
}

Player reached destination

Direction: Game server -> Clients

Message: player_reached_destination

Data:

{
  "player": {
    "id": "2e0f1a50-eaa6-4efd-b0c3-adbf7000eec2",
    "name": "Joso",
    "active": true,
    "position": {
      "x": 5,
      "y": 5
    },
    "move_count": 6,
    "move_attempt_count": 6,
    "state": "ON_DESTINATION"
  }
}

Product selection timeout

Direction: Game server -> Clients

Message: product_selection_timeout

Data: null

Product selection done

Message: product_selection_done

Direction: Client -> Game server, Game server -> Clients

Data: null