4 Commits

Author SHA1 Message Date
80c7c80451 uuid -> id 2023-03-31 17:20:23 +02:00
d45aca6c30 uuid -> id 2023-03-31 17:16:00 +02:00
28a981980f Product purchase 2023-03-31 13:06:27 +02:00
e1e77aba96 Optimizations 2023-03-31 12:19:48 +02:00
14 changed files with 429 additions and 243 deletions

View File

@ -92,7 +92,7 @@ To activate virtual environment:
poetry shell
```
WebSockets server runs on port **8011**. To run WS Server on different port, edit `settings.py` configuration.
WebSockets server runs on port **8011**. To run WS Server on different port, edit `settings.py` configuration.
## System overview
@ -220,7 +220,7 @@ Response body:
}
},
"player": {
"uuid": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25",
"id": "75bba7cd-a4c1-4b50-b0b5-6382c2822a25",
"name": "Pero",
"position": {
"x": 0,
@ -234,17 +234,17 @@ Response body:
### Player Move
- POST `/player/{uuid}/move/left`
- POST `/player/{uuid}/move/right`
- POST `/player/{uuid}/move/up`
- POST `/player/{uuid}/move/down`
- 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 uuid not valid, probably timeout
- 403 Forbidden: Player id not valid, probably timeout
- 409 Conflict: Invalid move, obstacle or position out of board
- 422 Unprocessable Content: Validation error
@ -252,7 +252,7 @@ Response body:
```json
{
"player": {
"uuid": "string",
"id": "string",
"name": "Pero",
"position": {
"x": 50,
@ -266,7 +266,7 @@ Response body:
### Get Player Info
GET `/player/{{uuid}}`
GET `/player/{{id}}`
Request body: None
@ -274,7 +274,7 @@ Response body:
```json
{
"player": {
"uuid": "string",
"id": "string",
"name": "Pero",
"position": {
"x": 50,
@ -334,7 +334,7 @@ Data:
},
"players": [
{
"uuid": "test-player-id",
"id": "test-player-id",
"name": "Pero",
"active": true,
"position": {
@ -345,7 +345,7 @@ Data:
"move_attempt_count": 3
},
{
"uuid": "95962b49-0003-4bf2-b205-71f2590f2318",
"id": "95962b49-0003-4bf2-b205-71f2590f2318",
"name": "Mirko",
"active": true,
"position": {

View File

@ -34,6 +34,15 @@ POST http://localhost:8010/player/test-player-pero/move/up
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
POST http://localhost:8010/player/test-player-mirko/move/left
###

View File

@ -8,6 +8,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<link rel="stylesheet" href="styles.css">
<script src="js/frontend.js"></script>
<title>FairHopper Visualisation Client</title>
</head>
@ -42,179 +43,4 @@
</main>
</body>
<script>
const BOARD_ICONS = {
PLAYER: "😀",
PLAYER_ON_DESTINATION: "😎",
OBSTACLE: "🔥",
DESTINATION: "🏠",
};
function createBoard(board) {
let html = "";
for (let y = 0; y < board.height; y++) {
let colHtml = "";
for (let x = 0; x < board.width; x++) {
colHtml += `<div class="cell" id="cell-${x}-${y}">&nbsp;</div>`;
}
html += `
<div class="flex-grid">
${colHtml}
</div>
`;
}
document.getElementById("board-content").innerHTML = html;
}
function findCell(position) {
return document.getElementById(`cell-${position.x}-${position.y}`);
}
function renderCellContent(position, content) {
const cell = findCell(position);
if (cell) {
cell.innerText = content;
}
}
function renderPlayerList(players) {
const html = players.filter(player => player.active).map((player) => {
const onDestination = player.state == "ON_DESTINATION";
return `
<li class="${onDestination ? "text-success" : ""}">
${player.name} (${player.move_count})
${onDestination ? "✅" : ""}
</li>
`;
}).join("");
document.getElementById("players-content").innerHTML = html;
}
function renderPlayers(players) {
players.filter(player => player.active).forEach(player => {
const cell = findCell(player.position);
const onDestination = player.state == "ON_DESTINATION";
const playerIcon = onDestination ? BOARD_ICONS.PLAYER_ON_DESTINATION : BOARD_ICONS.PLAYER;
if (cell) {
const html = `
<div class="player-tooltip">${player.name}</div>
${playerIcon}
`;
cell.innerHTML = html;
}
});
}
function getLayerObjectsOfType(layers, type) {
let objects = [];
layers.forEach(layer => {
objects = objects.concat(layer.objects.filter(obj => obj.type === type))
});
return objects;
}
function renderObstacles(layers) {
const objects = getLayerObjectsOfType(layers, "OBSTACLE");
objects.forEach(obj => {
renderCellContent(obj.position, BOARD_ICONS.OBSTACLE);
});
}
function renderDestination(position) {
renderCellContent(position, BOARD_ICONS.DESTINATION);
}
function renderGameDump(data) {
createBoard(data.board);
renderObstacles(data.layers)
renderDestination(data.destination.position);
renderPlayerList(data.players);
renderPlayers(data.players);
}
function productPurchaseStart(products, purchaseTimeout) {
console.log("productPurchaseStart:", products);
const containerElement = document.getElementById("purchase-container");
const contentElement = document.getElementById("products-content");
const purchaseTimeoutElement = document.getElementById("purchase-countdown");
const html = products.map(product => {
return `
<div class="card product">
<img src="img/products/${product.name}.jpeg" class="card-img-topx" alt="${product.name}">
<div class="card-body">
<h5 class="card-title">${product.name}</h5>
</div>
</div>
`;
}).join("");
contentElement.innerHTML = html;
containerElement.classList.remove("d-none")
purchaseTimeoutElement.innerText = purchaseTimeout;
}
function productPurchaseTimerTick(timeLeft) {
const purchaseTimeoutElement = document.getElementById("purchase-countdown");
purchaseTimeoutElement.innerText = timeLeft;
}
function productPurchased(product) {
console.log("productPurchased:", product)
}
function productPurchaseDone() {
console.log("productPurchaseDone")
const container = document.getElementById("purchase-container");
container.classList.add("d-none")
}
function wsConnect() {
let ws = new WebSocket('ws://localhost:8011');
ws.onopen = () => {
console.log("WS connected")
};
ws.onmessage = (e) => {
const wsMessage = JSON.parse(e.data);
console.log("WS message received:", wsMessage)
switch (wsMessage.message) {
case "game_dump":
renderGameDump(wsMessage.data);
break;
case "product_purchase_start":
productPurchaseStart(wsMessage.data.products, wsMessage.data.timeout)
break;
case "product_purchase_timer_tick":
productPurchaseTimerTick(wsMessage.data.time_left)
break;
case "product_purchased":
productPurchased(wsMessage.data)
break;
case "product_purchase_done":
productPurchaseDone()
break;
default:
console.error("Unknown message:", wsMessage)
}
};
ws.onclose = (e) => {
setTimeout(function () {
wsConnect();
}, 1000);
};
ws.onerror = (err) => {
console.error("Socket encountered error:", err.message, "Closing socket");
ws.close();
};
}
window.onload = () => {
wsConnect();
}
</script>
</html>

179
frontend/js/frontend.js Normal file
View File

@ -0,0 +1,179 @@
const BOARD_ICONS = {
PLAYER: "😀",
PLAYER_ON_DESTINATION: "😎",
OBSTACLE: "🔥",
DESTINATION: "🏠",
};
function createBoard(board) {
let html = "";
for (let y = 0; y < board.height; y++) {
let colHtml = "";
for (let x = 0; x < board.width; x++) {
colHtml += `<div class="cell" id="cell-${x}-${y}">&nbsp;</div>`;
}
html += `
<div class="flex-grid">
${colHtml}
</div>
`;
}
document.getElementById("board-content").innerHTML = html;
}
function findCell(position) {
return document.getElementById(`cell-${position.x}-${position.y}`);
}
function renderCellContent(position, content) {
const cell = findCell(position);
if (cell) {
cell.innerText = content;
}
}
function renderPlayerList(players) {
const html = players
.filter((player) => player.active)
.map((player) => {
const onDestination = player.state == "ON_DESTINATION";
return `
<li class="${onDestination ? "text-success" : ""}">
${player.name} (${player.move_count})
${onDestination ? "✅" : ""}
</li>
`;
})
.join("");
document.getElementById("players-content").innerHTML = html;
}
function renderPlayers(players) {
players
.filter((player) => player.active)
.forEach((player) => {
const cell = findCell(player.position);
const onDestination = player.state == "ON_DESTINATION";
const playerIcon = onDestination ? BOARD_ICONS.PLAYER_ON_DESTINATION : BOARD_ICONS.PLAYER;
if (cell) {
const html = `
<div class="player-tooltip">${player.name}</div>
${playerIcon}
`;
cell.innerHTML = html;
}
});
}
function getLayerObjectsOfType(layers, type) {
let objects = [];
layers.forEach((layer) => {
objects = objects.concat(layer.objects.filter((obj) => obj.type === type));
});
return objects;
}
function renderObstacles(layers) {
const objects = getLayerObjectsOfType(layers, "OBSTACLE");
objects.forEach((obj) => {
renderCellContent(obj.position, BOARD_ICONS.OBSTACLE);
});
}
function renderDestination(position) {
renderCellContent(position, BOARD_ICONS.DESTINATION);
}
function renderGameDump(data) {
createBoard(data.board);
renderObstacles(data.layers);
renderDestination(data.destination.position);
renderPlayerList(data.players);
renderPlayers(data.players);
}
function productPurchaseStart(products, purchaseTimeout) {
console.log("productPurchaseStart:", products);
const containerElement = document.getElementById("purchase-container");
const contentElement = document.getElementById("products-content");
const purchaseTimeoutElement = document.getElementById("purchase-countdown");
const html = products
.map((product) => {
return `
<div class="card product">
<img src="img/products/${product.name}.jpeg" class="card-img-topx" alt="${product.name}">
<div class="card-body">
<h5 class="card-title">${product.name}</h5>
</div>
</div>
`;
})
.join("");
contentElement.innerHTML = html;
containerElement.classList.remove("d-none");
purchaseTimeoutElement.innerText = purchaseTimeout;
}
function productPurchaseTimerTick(timeLeft) {
const purchaseTimeoutElement = document.getElementById("purchase-countdown");
purchaseTimeoutElement.innerText = timeLeft;
}
function productPurchased(product) {
console.log("productPurchased:", product);
}
function productPurchaseDone() {
console.log("productPurchaseDone");
const container = document.getElementById("purchase-container");
container.classList.add("d-none");
}
function wsConnect() {
let ws = new WebSocket("ws://localhost:8011");
ws.onopen = () => {
console.log("WS connected");
};
ws.onmessage = (e) => {
const wsMessage = JSON.parse(e.data);
console.log("WS message received:", wsMessage);
switch (wsMessage.message) {
case "game_dump":
renderGameDump(wsMessage.data);
break;
case "product_purchase_start":
productPurchaseStart(wsMessage.data.products, wsMessage.data.timeout);
break;
case "product_purchase_timer_tick":
productPurchaseTimerTick(wsMessage.data.time_left);
break;
case "product_purchased":
productPurchased(wsMessage.data);
break;
case "product_purchase_done":
productPurchaseDone();
break;
default:
console.error("Unknown message:", wsMessage);
}
};
ws.onclose = (e) => {
setTimeout(function () {
wsConnect();
}, 1000);
};
ws.onerror = (err) => {
console.error("Socket encountered error:", err.message, "Closing socket");
ws.close();
};
}
window.onload = () => {
wsConnect();
};

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel as PydanticBaseModel
from hopper.enums import PlayerState
@ -25,7 +27,7 @@ class PositionDto(BaseModel):
class PlayerDto(BaseModel):
uuid: str
id: str
name: str
active: bool
position: PositionDto
@ -38,6 +40,11 @@ class DestinationDto(BaseModel):
position: PositionDto
class ProductDto(BaseModel):
name: str
id: str
description: Optional[str] = None
class StartGameRequestDto(BaseModel):
player_name: str
@ -61,3 +68,11 @@ class PlayerInfoResponseDto(MovePlayerResponseDto):
class ErrorResponseDto(BaseModel):
detail: str
class GetProductsResponse(BaseModel):
products: list[ProductDto]
class PurchaseProductDto(BaseModel):
product_id: str

View File

@ -6,22 +6,26 @@ from hopper.api.dto import (
DestinationDto,
ErrorResponseDto,
GameInfoDto,
GetProductsResponse,
MovePlayerResponseDto,
PingResponse,
PlayerInfoResponseDto,
ProductDto,
PurchaseProductDto,
StartGameRequestDto,
StartGameResponseDto,
)
from hopper.engine import GameEngine
from hopper.enums import Direction, PlayerMoveResult
from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds
from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds, PurchaseForbiddenForPlayer
from hopper.models.player import Player
from settings import settings
router = APIRouter()
def get_player(uuid: str, engine: GameEngine = Depends(get_game_engine)) -> Player:
player = engine.players.find(uuid)
def get_player(id: str, engine: GameEngine = Depends(get_game_engine)) -> Player:
player = engine.players.find(id)
if player is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Player not found"
@ -72,7 +76,7 @@ async def start_game(
@router.get(
"/player/{uuid}",
"/player/{id}",
response_model=PlayerInfoResponseDto,
responses={
status.HTTP_403_FORBIDDEN: {
@ -81,7 +85,7 @@ async def start_game(
},
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponseDto,
"description": " Player with uuid not found, probably kicked out",
"description": " Player with id not found, probably kicked out",
},
},
)
@ -92,7 +96,7 @@ async def get_player_info(
@router.post(
"/player/{uuid}/move/{direction}",
"/player/{id}/move/{direction}",
response_model=MovePlayerResponseDto,
status_code=status.HTTP_201_CREATED,
responses={
@ -106,7 +110,7 @@ async def get_player_info(
},
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponseDto,
"description": " Player with uuid not found, probably kicked out",
"description": " Player with id not found, probably kicked out",
},
status.HTTP_409_CONFLICT: {
"model": ErrorResponseDto,
@ -144,3 +148,42 @@ async def move_player(
response.status_code = status.HTTP_200_OK
return MovePlayerResponseDto(player=player)
@router.get("/products", response_model=GetProductsResponse)
async def get_products() -> GetProductsResponse:
return GetProductsResponse(
products=settings.products,
)
@router.get("/products/{id}", response_model=ProductDto)
async def get_product(id: str) -> ProductDto:
for product in settings.products:
if product.id == id:
return ProductDto.from_orm(product)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Product not found"
)
@router.post("/player/{id}/product/purchase")
async def purchase_product(
body: PurchaseProductDto,
engine: GameEngine = Depends(get_game_engine),
player: Player = Depends(get_player),
):
for product in settings.products:
if product.id == body.product_id:
try:
await engine.purchase_product(player=player, product=product)
except PurchaseForbiddenForPlayer:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Purchase forbidden for this player",
)
break
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Product not found"
)

View File

@ -5,7 +5,10 @@ from typing import Callable, Optional
class CountdownTimer(Thread):
def __init__(
self, seconds: int, timer_tick_callback: Optional[Callable[[int], None]] = None, timer_done_callback: Optional[Callable[[], None]] = None
self,
seconds: int,
timer_tick_callback: Optional[Callable[[int], None]] = None,
timer_done_callback: Optional[Callable[[], None]] = None,
) -> None:
self.seconds = seconds
self.stop_event = Event()
@ -18,7 +21,7 @@ class CountdownTimer(Thread):
while time_left and not self.stop_event.is_set():
time.sleep(1)
time_left -= 1
if self.timer_tick_callback:
if self.timer_tick_callback and not self.stop_event.is_set():
self.timer_tick_callback(time_left)
if time_left == 0 and self.timer_done_callback:

View File

@ -5,7 +5,12 @@ from typing import Optional
from hopper.countdown_timer import CountdownTimer
from hopper.enums import Direction, GameState, PlayerMoveResult, PlayerState
from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds
from hopper.errors import (
Collision,
GameLockForMovement,
PositionOutOfBounds,
PurchaseForbiddenForPlayer,
)
from hopper.models.board import (
BOARD_DUMP_CHARS,
BoardLayout,
@ -17,11 +22,28 @@ from hopper.models.board import (
create_random_position,
)
from hopper.models.player import Player, PlayerList, Position
from hopper.models.product import Product
from hopper.watchdog import InactivityWatchdog
from hopper.ws_server import WSServer
from settings import settings
def create_player_start_position(board_width: int, board_height: int) -> Position:
"""Create random position somewhere on the board border"""
border_len = (board_width + board_height) * 2
rnd_position = random.randint(0, border_len - 1)
if rnd_position < board_width * 2:
x = rnd_position % board_width
y = 0 if rnd_position < board_width else board_height - 1
else:
rnd_position -= 2 * board_width
x = 0 if rnd_position < board_height else board_width - 1
y = rnd_position % board_height
return Position(x=x, y=y)
class GameEngine:
def __init__(self, board: GameBoard, ws_server: WSServer = None) -> None:
self.board = board
@ -29,7 +51,8 @@ class GameEngine:
self.players = PlayerList()
self._inacivity_watchdog = None
self._purchase_countdown_timer: Optional[CountdownTimer] = None
self.reset_game()
self.game_state = GameState.RUNNING
self.__debug_print_board()
def dump_board(self) -> list[list[str]]:
dump = self.board.dump()
@ -61,15 +84,21 @@ class GameEngine:
)
self._inacivity_watchdog.start()
def reset_game(self) -> None:
async def send_game_dump(self):
self.__debug_print_board()
await self.ws_server.send_game_dump()
async def reset_game(self) -> None:
self.__debug_print_board()
self.game_state = GameState.RUNNING
self.players.clear()
await self.send_game_dump()
async def start_game_for_player(self, player_name: str) -> Player:
self._start_inactivity_watchdog()
player = Player(
name=player_name,
position=self._create_player_start_position(),
position=create_player_start_position(self.board.width, self.board.height),
state=PlayerState.CREATED,
)
self.players.append(player)
@ -77,32 +106,10 @@ class GameEngine:
logging.info(f"Starting new game for player: {player}")
self.__debug_print_board()
if self.ws_server:
await self.ws_server.send_game_dump()
#!!!!!!!!!!!!!!!
await self._player_on_destination(player)
#!!!!!!!!!!!!!!!
await self.send_game_dump()
await asyncio.sleep(settings.game.MOVE_DELAY)
return player
def _create_player_start_position(self) -> Position:
"""Create random position somewhere on the board border"""
border_len = (self.board.width + self.board.height) * 2
rnd_position = random.randint(0, border_len - 1)
if rnd_position < self.board.width * 2:
x = rnd_position % self.board.width
y = 0 if rnd_position < self.board.width else self.board.height - 1
else:
rnd_position -= 2 * self.board.width
x = 0 if rnd_position < self.board.height else self.board.width - 1
y = rnd_position % self.board.height
return Position(x=x, y=y)
def _move_position(self, position: Position, direction: Direction) -> Position:
new_position = Position(position.x, position.y)
if direction == Direction.LEFT:
@ -149,10 +156,8 @@ class GameEngine:
await self._player_on_destination(player)
return PlayerMoveResult.DESTINATION_REACHED
if self.ws_server:
await self.ws_server.send_game_dump()
await self.send_game_dump()
self.__debug_print_board()
await asyncio.sleep(settings.game.MOVE_DELAY)
return PlayerMoveResult.OK
@ -171,8 +176,7 @@ class GameEngine:
logging.info(f"Player {player} reached destination!")
self.game_state = GameState.LOCK_FOR_MOVEMENT
await self.ws_server.send_game_dump()
self.__debug_print_board()
await self.send_game_dump()
await self.ws_server.send_product_purchase_start_message(
player=player, products=settings.products
@ -183,6 +187,7 @@ class GameEngine:
)
def on_purchase_timer_tick(time_left) -> None:
logging.info(f"Purchase countdown timer tick, time left: {time_left}")
asyncio.run(
self.ws_server.send_product_purchase_time_left_message(
player=player, time_left=time_left
@ -198,6 +203,7 @@ class GameEngine:
)
)
self.game_state = GameState.RUNNING
asyncio.run(self.send_game_dump())
self._purchase_countdown_timer = CountdownTimer(
seconds=settings.purchase_timeout,
@ -206,6 +212,24 @@ class GameEngine:
)
self._purchase_countdown_timer.start()
async def purchase_product(self, player: Player, product: Product) -> None:
if not player.state == PlayerState.ON_DESTINATION:
raise PurchaseForbiddenForPlayer()
if self._purchase_countdown_timer:
self._purchase_countdown_timer.stop()
await self.ws_server.send_product_purchase_done_message(
player=player, product=product
)
await self.reset_game()
def _reset_player(self, player) -> None:
# move player to start position
player.position = create_player_start_position(
self.board.width, self.board.height
)
player.state = PlayerState.CREATED
player.last_seen = None
def get_board_layout(self) -> BoardLayout:
return BoardLayout(board=self.board, players=self.players)

View File

@ -12,3 +12,7 @@ class Collision(BaseError):
class GameLockForMovement(BaseError):
...
class PurchaseForbiddenForPlayer(BaseError):
...

View File

@ -15,7 +15,7 @@ class Position:
@dataclass
class Player:
name: str
uuid: str = field(default_factory=lambda: str(uuid.uuid4()))
id: str = field(default_factory=lambda: str(uuid.uuid4()))
position: Position = field(default_factory=lambda: Position(0, 0))
move_count: int = 0
move_attempt_count: int = 0
@ -31,8 +31,8 @@ class Player:
class PlayerList(list[Player]):
def find(self, uuid: str) -> Optional[Player]:
def find(self, id: str) -> Optional[Player]:
for player in self:
if player.uuid == uuid:
if player.id == id:
return player
return None

View File

@ -6,5 +6,5 @@ from typing import Optional
@dataclass
class Product:
name: str
uuid: str = field(default_factory=lambda: str(uuid.uuid4()))
id: str = field(default_factory=lambda: str(uuid.uuid4()))
description: Optional[str] = None

View File

@ -6,7 +6,14 @@ from typing import Optional, TypeVar
from pydantic import Field
from pydantic.generics import GenericModel
from hopper.api.dto import BaseModel, BoardDto, DestinationDto, PlayerDto, PositionDto
from hopper.api.dto import (
BaseModel,
BoardDto,
DestinationDto,
PlayerDto,
PositionDto,
ProductDto,
)
from hopper.enums import ObjectType
@ -20,12 +27,6 @@ class LayerDto(BaseModel):
objects: list[LayerObjectDto]
class ProductDto(BaseModel):
name: str
uuid: str
description: Optional[str] = None
class GameDumpDto(BaseModel):
board: BoardDto
destination: DestinationDto

51
sdk/demo.py Normal file
View File

@ -0,0 +1,51 @@
import random
from fh_sdk import Direction, FairHopper, Position
import math
HOST = "http://localhost"
PORT = 8010
def calc_angle(position1: Position, position2: Position) -> float:
x1, y1 = position1.x, position1.y
x2, y2 = position2.x, position2.y
return math.atan2(y2 - y1, x2 - x1) * (180 / math.pi)
fh = FairHopper(host=HOST, port=PORT)
res = fh.ping()
game = fh.start_game(player_name=f"Mirko {random.randint(0, 9999)}")
print(game.player.position)
quit()
res = fh.get_game_info()
print(">>>>>", res)
res = fh.get_player_info("XX")
print(">>>>>", res)
position = game.player.position
dest_position = game.destination.position
# p1 = PositionDto(x=0, y=20)
# p2 = PositionDto(x=10, y=10)
# angle = calc_angle(p1, p2)
# print(angle)
# quit()
for _ in range(10):
angle = calc_angle(position, dest_position) + 180
if 0 <= angle < 90:
direction = Direction.RIGHT
elif 90 <= angle <= 180:
direction = Direction.DOWN
elif 180 <= angle <= 270:
direction = Direction.RIGHT
else:
direction = Direction.UP
print(position, dest_position, int(angle), direction)
move_response = fh.move(game.player.id, direction)
position = move_response.player.position

View File

@ -2,18 +2,49 @@ import logging
from hopper.models.config import (
BoardSettings,
DebugSettings,
GameSettings,
InactivityWatchdogSettings,
Settings,
WSServerSettings,
)
from hopper.models.player import Player, Position
from hopper.models.product import Product
settings = Settings(
game=GameSettings(),
board=BoardSettings(),
board=BoardSettings(
WIDTH=20,
HEIGHT=20,
OBSTACLE_COUNT=10,
),
inacivity_watchdog=InactivityWatchdogSettings(),
purchase_timeout=5,
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(),
purchase_timeout=10,
debug=None,
debug=DebugSettings(
PRINT_BOARD=True,
PLAYERS=[
Player(
name="Pero",
id="test-player-pero",
position=Position(x=9, y=10),
can_be_deactivated=False,
),
Player(
name="Mirko",
id="test-player-mirko",
position=Position(x=10, y=5),
can_be_deactivated=False,
),
],
),
)