Compare commits
9 Commits
76ee207bce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| afbb3d7436 | |||
| 21a7f111b2 | |||
| fb4651ec23 | |||
| 6ff6433be3 | |||
| 2653eabb6c | |||
| b56071e2c7 | |||
| 78c3286c17 | |||
| b2a132a002 | |||
| d660845d30 |
@ -9,7 +9,6 @@ from hopper.models.config import (
|
|||||||
Settings,
|
Settings,
|
||||||
WSServerSettings,
|
WSServerSettings,
|
||||||
)
|
)
|
||||||
from hopper.models.product import Product
|
|
||||||
|
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
game=GameSettings(),
|
game=GameSettings(),
|
||||||
@ -21,14 +20,6 @@ settings = Settings(
|
|||||||
inacivity_watchdog=InactivityWatchdogSettings(),
|
inacivity_watchdog=InactivityWatchdogSettings(),
|
||||||
purchase_timeout=5,
|
purchase_timeout=5,
|
||||||
log_level=logging.INFO,
|
log_level=logging.INFO,
|
||||||
products=[
|
|
||||||
Product(name="CocaCola", id="cocacola-id"),
|
|
||||||
Product(name="Pepsi", id="pepsi-id"),
|
|
||||||
Product(name="Fanta", id="fanta-id"),
|
|
||||||
Product(name="Snickers", id="snickers-id"),
|
|
||||||
Product(name="Mars", id="mars-id"),
|
|
||||||
Product(name="Burek", id="burek-id"),
|
|
||||||
],
|
|
||||||
ws_server=WSServerSettings(
|
ws_server=WSServerSettings(
|
||||||
HOST="0.0.0.0",
|
HOST="0.0.0.0",
|
||||||
PORT=int(os.environ.get("FAIRHOPPER_WS_PORT", 8011)),
|
PORT=int(os.environ.get("FAIRHOPPER_WS_PORT", 8011)),
|
||||||
|
|||||||
18
Makefile
18
Makefile
@ -5,9 +5,11 @@ INTERNAL_WS_PORT=8011
|
|||||||
EXTERNAL_API_PORT=8010
|
EXTERNAL_API_PORT=8010
|
||||||
EXTERNAL_WS_PORT=8011
|
EXTERNAL_WS_PORT=8011
|
||||||
|
|
||||||
|
timestamp := `/bin/date "+%Y-%m-%d-%H-%M-%S"`
|
||||||
|
|
||||||
run:
|
run:
|
||||||
@poetry run \
|
@ \
|
||||||
|
poetry run \
|
||||||
uvicorn \
|
uvicorn \
|
||||||
main:app \
|
main:app \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
@ -15,7 +17,8 @@ run:
|
|||||||
--workers=1
|
--workers=1
|
||||||
|
|
||||||
run-dev:
|
run-dev:
|
||||||
@poetry run \
|
@ \
|
||||||
|
poetry run \
|
||||||
uvicorn \
|
uvicorn \
|
||||||
main:app \
|
main:app \
|
||||||
--host 0.0.0.0 \
|
--host 0.0.0.0 \
|
||||||
@ -24,7 +27,8 @@ run-dev:
|
|||||||
--reload
|
--reload
|
||||||
|
|
||||||
create-requirements:
|
create-requirements:
|
||||||
@poetry export \
|
@ \
|
||||||
|
poetry export \
|
||||||
--without-hashes \
|
--without-hashes \
|
||||||
--format=requirements.txt \
|
--format=requirements.txt \
|
||||||
> requirements.txt
|
> requirements.txt
|
||||||
@ -37,15 +41,17 @@ docker-clean:
|
|||||||
|
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
@docker \
|
@ \
|
||||||
|
docker \
|
||||||
buildx build \
|
buildx build \
|
||||||
--build-arg INTERNAL_API_PORT=$(INTERNAL_API_PORT) \
|
--build-arg INTERNAL_API_PORT=$(INTERNAL_API_PORT) \
|
||||||
--build-arg INTERNAL_WS_PORT=$(INTERNAL_WS_PORT) \
|
--build-arg INTERNAL_WS_PORT=$(INTERNAL_WS_PORT) \
|
||||||
--tag $(CONTAINER_NAME) \
|
--tag $(CONTAINER_NAME):$(timestamp) \
|
||||||
.
|
.
|
||||||
|
|
||||||
docker-run:
|
docker-run:
|
||||||
@docker \
|
@ \
|
||||||
|
docker \
|
||||||
run \
|
run \
|
||||||
--publish $(EXTERNAL_API_PORT):$(INTERNAL_API_PORT) \
|
--publish $(EXTERNAL_API_PORT):$(INTERNAL_API_PORT) \
|
||||||
--publish $(EXTERNAL_WS_PORT):$(INTERNAL_WS_PORT) \
|
--publish $(EXTERNAL_WS_PORT):$(INTERNAL_WS_PORT) \
|
||||||
|
|||||||
140
README.md
140
README.md
@ -44,8 +44,7 @@ state "Product Selected" as ProductSelected
|
|||||||
state "Selection Timeout" as SelectionTimeout
|
state "Selection Timeout" as SelectionTimeout
|
||||||
state "End Player's Game" as EndPlayer
|
state "End Player's Game" as EndPlayer
|
||||||
state "Lock Game" as LockGame <<end>>
|
state "Lock Game" as LockGame <<end>>
|
||||||
state "End Game" as EndGame <<end>>
|
state "Unlock game and restart" as UnlockGame <<end>>
|
||||||
state "Unlock game" as UnlockGame <<end>>
|
|
||||||
|
|
||||||
[*] -> StartGame
|
[*] -> StartGame
|
||||||
StartGame -> MovePlayer
|
StartGame -> MovePlayer
|
||||||
@ -55,9 +54,9 @@ DestinationReached --> ProductSelection
|
|||||||
DestinationReached -> LockGame: Lock game for all other players
|
DestinationReached -> LockGame: Lock game for all other players
|
||||||
ProductSelection --> ProductSelected
|
ProductSelection --> ProductSelected
|
||||||
ProductSelection --> SelectionTimeout
|
ProductSelection --> SelectionTimeout
|
||||||
ProductSelected --> EndGame: End game\nfor all players
|
ProductSelected --> UnlockGame: Unlock game\nand restart
|
||||||
SelectionTimeout -> EndPlayer
|
SelectionTimeout -> EndPlayer
|
||||||
EndPlayer --> UnlockGame: Unlock game\n for all players
|
EndPlayer --> UnlockGame: Unlock game\nand restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## FairHopper Game Server
|
## FairHopper Game Server
|
||||||
@ -222,7 +221,7 @@ end
|
|||||||
== Player reached destination ==
|
== Player reached destination ==
|
||||||
|
|
||||||
Game -> Game: Lock game for other players
|
Game -> Game: Lock game for other players
|
||||||
activate Game
|
activate Game #skyblue
|
||||||
Game -> WS: Player reached destination
|
Game -> WS: Player reached destination
|
||||||
activate WS #coral
|
activate WS #coral
|
||||||
WS o-> Client1: Select product
|
WS o-> Client1: Select product
|
||||||
@ -232,25 +231,30 @@ deactivate Game
|
|||||||
|
|
||||||
loop #lightyellow Product select countdown timer (60s)
|
loop #lightyellow Product select countdown timer (60s)
|
||||||
Game ->o WS: Timer timeout
|
Game ->o WS: Timer timeout
|
||||||
activate Game
|
activate Game #skyblue
|
||||||
activate WS #coral
|
activate WS #coral
|
||||||
WS o-> Client1: Cancel selection
|
WS o-> Client1: Selection timeout
|
||||||
WS o-> Client2: Cancel selection
|
WS o-> Client2: Selection timeout
|
||||||
deactivate WS
|
deactivate WS
|
||||||
Game -> Game: Unlock game
|
Game -> Game: Unlock game
|
||||||
deactivate Game
|
deactivate Game
|
||||||
end
|
end
|
||||||
|
|
||||||
Client1 -> WS: Product selected
|
Client1 -> Client1: Product selection
|
||||||
|
activate Client1 #greenyellow
|
||||||
|
Client1 -> Client1: Dispense product
|
||||||
|
Client1 ->o WS: Product selected
|
||||||
|
deactivate Client1
|
||||||
|
|
||||||
activate WS #coral
|
activate WS #coral
|
||||||
WS o-> Game: Product selected
|
WS o-> Game: Product selected
|
||||||
activate Game
|
activate Game #skyblue
|
||||||
WS o-> Client2: Product selected
|
WS o-> Client2: Product selected
|
||||||
deactivate WS
|
deactivate WS
|
||||||
|
|
||||||
|
|
||||||
Game -> Game: Unlock game
|
Game -> Game: Unlock game
|
||||||
Game -> WS: Game state
|
Game ->o WS: Game state
|
||||||
activate WS #coral
|
activate WS #coral
|
||||||
WS o-> Client1: Game state
|
WS o-> Client1: Game state
|
||||||
WS o-> Client2: Game state
|
WS o-> Client2: Game state
|
||||||
@ -323,6 +327,7 @@ Response code:
|
|||||||
- 403 Forbidden: Player id not valid, probably timeout
|
- 403 Forbidden: Player id not valid, probably timeout
|
||||||
- 409 Conflict: Invalid move, obstacle or position out of board
|
- 409 Conflict: Invalid move, obstacle or position out of board
|
||||||
- 422 Unprocessable Content: Validation error
|
- 422 Unprocessable Content: Validation error
|
||||||
|
- 423 Locked: Game locked, product selection in progress
|
||||||
|
|
||||||
Response body:
|
Response body:
|
||||||
```json
|
```json
|
||||||
@ -400,6 +405,8 @@ Response body:
|
|||||||
|
|
||||||
### Game state structure
|
### Game state structure
|
||||||
|
|
||||||
|
Direction: Game server -> Clients
|
||||||
|
|
||||||
Message: `game_dump`
|
Message: `game_dump`
|
||||||
|
|
||||||
Data:
|
Data:
|
||||||
@ -503,109 +510,42 @@ Data:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Product purchase start
|
### Player reached destination
|
||||||
|
|
||||||
Message: `product_purchase_start`
|
Direction: Game server -> Clients
|
||||||
|
|
||||||
|
Message: `player_reached_destination`
|
||||||
|
|
||||||
Data:
|
Data:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"player": {
|
"player": {
|
||||||
"id": "test-player-pero",
|
"id": "2e0f1a50-eaa6-4efd-b0c3-adbf7000eec2",
|
||||||
"name": "Pero",
|
"name": "Joso",
|
||||||
"active": true,
|
"active": true,
|
||||||
"position": {
|
"position": {
|
||||||
"x": 10,
|
"x": 5,
|
||||||
"y": 10
|
"y": 5
|
||||||
},
|
},
|
||||||
"move_count": 1,
|
"move_count": 6,
|
||||||
"move_attempt_count": 1,
|
"move_attempt_count": 6,
|
||||||
"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"
|
"state": "ON_DESTINATION"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Product purchase timer done
|
### Product selection timeout
|
||||||
|
|
||||||
Message: `product_purchase_done`
|
Direction: Game server -> Clients
|
||||||
|
|
||||||
Data:
|
Message: `product_selection_timeout`
|
||||||
```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.
|
Data: `null`
|
||||||
|
|
||||||
|
### Product selection done
|
||||||
|
|
||||||
|
Message: `product_selection_done`
|
||||||
|
|
||||||
|
Direction: Client -> Game server, Game server -> Clients
|
||||||
|
|
||||||
|
Data: `null`
|
||||||
|
|||||||
@ -34,13 +34,6 @@ POST http://localhost:8010/player/test-player-pero/move/up
|
|||||||
POST http://localhost:8010/player/test-player-pero/move/down
|
POST http://localhost:8010/player/test-player-pero/move/down
|
||||||
###
|
###
|
||||||
|
|
||||||
# purchase product
|
|
||||||
POST http://localhost:8010/player/test-player-pero/product/purchase
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"product_id": "cocacola-id"
|
|
||||||
}
|
|
||||||
###
|
###
|
||||||
|
|
||||||
# move Mirko left
|
# move Mirko left
|
||||||
|
|||||||
@ -22,18 +22,6 @@
|
|||||||
<h1 class="mt-1 mb-2">
|
<h1 class="mt-1 mb-2">
|
||||||
FairHopper Visualisation Client
|
FairHopper Visualisation Client
|
||||||
</h1>
|
</h1>
|
||||||
<button type="button" class="btn btn-primary test-button">
|
|
||||||
Launch demo modal
|
|
||||||
</button>
|
|
||||||
<div id="purchase-container" class="purchase-container d-none">
|
|
||||||
<div class="d-flex header">
|
|
||||||
<h3>
|
|
||||||
Product selection
|
|
||||||
</h3>
|
|
||||||
<h3 id="purchase-countdown" class="countdown"></h3>
|
|
||||||
</div>
|
|
||||||
<div id="products-content" class="products-content"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<div class="board-container">
|
<div class="board-container">
|
||||||
@ -68,18 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.querySelector(".test-button").onclick = () => {
|
|
||||||
playerReachedDestination({
|
|
||||||
player: {
|
|
||||||
name: "Pero Perić",
|
|
||||||
move_count: 123,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -104,6 +104,10 @@ function playerReachedDestination(data) {
|
|||||||
playerOnDestinationModal.show();
|
playerOnDestinationModal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function productSelectionTimeout() {
|
||||||
|
playerOnDestinationModal.hide();
|
||||||
|
}
|
||||||
|
|
||||||
function productSelectionDone() {
|
function productSelectionDone() {
|
||||||
playerOnDestinationModal.hide();
|
playerOnDestinationModal.hide();
|
||||||
}
|
}
|
||||||
@ -127,6 +131,9 @@ function wsConnect() {
|
|||||||
case "player_reached_destination":
|
case "player_reached_destination":
|
||||||
playerReachedDestination(wsMessage.data);
|
playerReachedDestination(wsMessage.data);
|
||||||
break;
|
break;
|
||||||
|
case "product_selection_timeout":
|
||||||
|
productSelectionTimeout();
|
||||||
|
break;
|
||||||
case "product_selection_done":
|
case "product_selection_done":
|
||||||
productSelectionDone();
|
productSelectionDone();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ main.main-container {
|
|||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
.flex-grid:last-of-type {
|
.flex-grid:last-of-type {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
@ -56,44 +56,3 @@ ul.players {
|
|||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: darkred transparent transparent transparent;
|
border-color: darkred transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase-container {
|
|
||||||
width: 50vw;
|
|
||||||
position: fixed;
|
|
||||||
top: 200px;
|
|
||||||
left: 50%;
|
|
||||||
padding: 20px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background-color: darkred;
|
|
||||||
z-index: 999;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-container .header {
|
|
||||||
color: white;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-container .header .countdown {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-container .products-content {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 10px;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-container .products-content .product.selected {
|
|
||||||
background-color: pink;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-container .products-content .product .card-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-container .products-content .product img {
|
|
||||||
margin: 20px;
|
|
||||||
max-height: 300px;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -179,14 +179,14 @@ class GameEngine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_purchase_timer_tick(time_left) -> None:
|
def on_purchase_timer_tick(time_left) -> None:
|
||||||
logging.info(f"Product selection countdown timer tick, time left: {time_left}")
|
logging.info(
|
||||||
|
f"Product selection countdown timer tick, time left: {time_left}"
|
||||||
|
)
|
||||||
|
|
||||||
def on_purchase_timer_done() -> None:
|
def on_purchase_timer_done() -> None:
|
||||||
logging.info("Ding ding! Product selection countdown timer timeout")
|
logging.info("Ding ding! Product selection countdown timer timeout")
|
||||||
self._purchase_countdown_timer = None
|
self._purchase_countdown_timer = None
|
||||||
asyncio.run(
|
asyncio.run(self.ws_server.send_product_selection_timeout_message())
|
||||||
self.ws_server.send_product_selection_done_message()
|
|
||||||
)
|
|
||||||
self.game_state = GameState.RUNNING
|
self.game_state = GameState.RUNNING
|
||||||
asyncio.run(self.send_game_dump())
|
asyncio.run(self.send_game_dump())
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,13 @@ BOARD_DUMP_CHARS: dict[ObjectType, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_random_position(board_width: int, board_height: int) -> Position:
|
||||||
|
return Position(
|
||||||
|
x=random.randint(0, board_width - 1),
|
||||||
|
y=random.randint(0, board_height - 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LayerObject:
|
class LayerObject:
|
||||||
type_: ObjectType
|
type_: ObjectType
|
||||||
@ -102,10 +109,3 @@ class BoardLayout:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return layers
|
return layers
|
||||||
|
|
||||||
|
|
||||||
def create_random_position(board_width: int, board_height: int) -> Position:
|
|
||||||
return Position(
|
|
||||||
x=random.randint(0, board_width - 1),
|
|
||||||
y=random.randint(0, board_height - 1),
|
|
||||||
)
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ from dataclasses import dataclass
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from hopper.models.player import Player
|
from hopper.models.player import Player
|
||||||
from hopper.models.product import Product
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -46,5 +45,4 @@ class Settings:
|
|||||||
ws_server: WSServerSettings
|
ws_server: WSServerSettings
|
||||||
purchase_timeout: int = 10 # seconds
|
purchase_timeout: int = 10 # seconds
|
||||||
log_level: int = logging.INFO
|
log_level: int = logging.INFO
|
||||||
products: List[Product] = None
|
|
||||||
debug: Optional[DebugSettings] = None
|
debug: Optional[DebugSettings] = None
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Product:
|
|
||||||
name: str
|
|
||||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
||||||
description: Optional[str] = None
|
|
||||||
@ -6,13 +6,7 @@ from typing import Optional, TypeVar
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydantic.generics import GenericModel
|
from pydantic.generics import GenericModel
|
||||||
|
|
||||||
from hopper.api.dto import (
|
from hopper.api.dto import BaseModel, BoardDto, DestinationDto, PlayerDto, PositionDto
|
||||||
BaseModel,
|
|
||||||
BoardDto,
|
|
||||||
DestinationDto,
|
|
||||||
PlayerDto,
|
|
||||||
PositionDto,
|
|
||||||
)
|
|
||||||
from hopper.enums import ObjectType
|
from hopper.enums import ObjectType
|
||||||
|
|
||||||
|
|
||||||
@ -65,6 +59,10 @@ class WSProductSelectionDoneMessage(WSMessage):
|
|||||||
message: str = "product_selection_done"
|
message: str = "product_selection_done"
|
||||||
|
|
||||||
|
|
||||||
|
class WSProductSelectionTimeoutMessage(WSMessage):
|
||||||
|
message: str = "product_selection_timeout"
|
||||||
|
|
||||||
|
|
||||||
class WSPlayerReachedDestinationMessage(WSMessage):
|
class WSPlayerReachedDestinationMessage(WSMessage):
|
||||||
message: str = "player_reached_destination"
|
message: str = "player_reached_destination"
|
||||||
data: PlayerReachedDestinationDto
|
data: PlayerReachedDestinationDto
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import asyncio
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from threading import Thread
|
from threading import Thread, Event
|
||||||
|
|
||||||
from hopper.models.player import PlayerList
|
from hopper.models.player import PlayerList
|
||||||
from hopper.ws_server import WSServer
|
from hopper.ws_server import WSServer
|
||||||
@ -10,19 +10,18 @@ from settings import settings
|
|||||||
|
|
||||||
|
|
||||||
class InactivityWatchdog(Thread):
|
class InactivityWatchdog(Thread):
|
||||||
def __init__(
|
def __init__(self, players: PlayerList, ws_server: WSServer = None) -> None:
|
||||||
self, players: PlayerList, ws_server: WSServer = None
|
|
||||||
) -> None:
|
|
||||||
self.players = players
|
self.players = players
|
||||||
self.ws_server = ws_server
|
self.ws_server = ws_server
|
||||||
self.stopped = False
|
self.stop_event = Event()
|
||||||
super().__init__(daemon=True)
|
super().__init__(daemon=True)
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
logging.info("Starting inactivity watchdog")
|
logging.info("Starting inactivity watchdog")
|
||||||
while not self.stopped:
|
while not self.stop_event.is_set():
|
||||||
self.cleanup_players()
|
self.cleanup_players()
|
||||||
time.sleep(settings.inacivity_watchdog.TICK_INTERVAL)
|
if not self.stop_event.is_set():
|
||||||
|
time.sleep(settings.inacivity_watchdog.TICK_INTERVAL)
|
||||||
|
|
||||||
def cleanup_players(self) -> None:
|
def cleanup_players(self) -> None:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
@ -64,4 +63,4 @@ class InactivityWatchdog(Thread):
|
|||||||
asyncio.run(self.ws_server.send_game_dump())
|
asyncio.run(self.ws_server.send_game_dump())
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
self.stopped = True
|
self.stop_event.set()
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from hopper.models.ws_dto import (
|
|||||||
WSMessage,
|
WSMessage,
|
||||||
WSPlayerReachedDestinationMessage,
|
WSPlayerReachedDestinationMessage,
|
||||||
WSProductSelectionDoneMessage,
|
WSProductSelectionDoneMessage,
|
||||||
|
WSProductSelectionTimeoutMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +64,9 @@ class WSServer(Thread):
|
|||||||
try:
|
try:
|
||||||
# we're expecting nothing from client, but read if client sends a message
|
# we're expecting nothing from client, but read if client sends a message
|
||||||
rcv_data = await websocket.recv()
|
rcv_data = await websocket.recv()
|
||||||
await self.handle_rcv_message(client=websocket, raw_message=rcv_data)
|
await self.handle_rcv_message(
|
||||||
|
client=websocket, raw_message=rcv_data
|
||||||
|
)
|
||||||
except ConnectionClosedOK:
|
except ConnectionClosedOK:
|
||||||
logging.info(f"Connection closed OK for client: {websocket.id}")
|
logging.info(f"Connection closed OK for client: {websocket.id}")
|
||||||
connected = False
|
connected = False
|
||||||
@ -126,6 +129,10 @@ class WSServer(Thread):
|
|||||||
message = WSProductSelectionDoneMessage()
|
message = WSProductSelectionDoneMessage()
|
||||||
await self.send_message_to_clients(message)
|
await self.send_message_to_clients(message)
|
||||||
|
|
||||||
|
async def send_product_selection_timeout_message(self) -> None:
|
||||||
|
message = WSProductSelectionTimeoutMessage()
|
||||||
|
await self.send_message_to_clients(message)
|
||||||
|
|
||||||
async def run_async(self) -> None:
|
async def run_async(self) -> None:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Starting FairHopper Websockets Server on {self.host}:{self.port}"
|
f"Starting FairHopper Websockets Server on {self.host}:{self.port}"
|
||||||
|
|||||||
@ -9,7 +9,6 @@ from hopper.models.config import (
|
|||||||
WSServerSettings,
|
WSServerSettings,
|
||||||
)
|
)
|
||||||
from hopper.models.player import Player, Position
|
from hopper.models.player import Player, Position
|
||||||
from hopper.models.product import Product
|
|
||||||
|
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
game=GameSettings(),
|
game=GameSettings(),
|
||||||
@ -21,14 +20,6 @@ settings = Settings(
|
|||||||
inacivity_watchdog=InactivityWatchdogSettings(),
|
inacivity_watchdog=InactivityWatchdogSettings(),
|
||||||
purchase_timeout=5,
|
purchase_timeout=5,
|
||||||
log_level=logging.INFO,
|
log_level=logging.INFO,
|
||||||
products=[
|
|
||||||
Product(name="CocaCola", id="cocacola-id"),
|
|
||||||
Product(name="Pepsi", id="pepsi-id"),
|
|
||||||
Product(name="Fanta", id="fanta-id"),
|
|
||||||
Product(name="Snickers", id="snickers-id"),
|
|
||||||
Product(name="Mars", id="mars-id"),
|
|
||||||
Product(name="Burek", id="burek-id"),
|
|
||||||
],
|
|
||||||
ws_server=WSServerSettings(),
|
ws_server=WSServerSettings(),
|
||||||
debug=DebugSettings(
|
debug=DebugSettings(
|
||||||
PRINT_BOARD=True,
|
PRINT_BOARD=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user