2 Commits

Author SHA1 Message Date
f54344a17f Tweak frontend and game logic 2023-03-26 00:37:58 +01:00
3ac07f3072 Optimize views 2023-03-25 18:59:35 +01:00
6 changed files with 105 additions and 50 deletions

View File

@ -14,7 +14,6 @@
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<h1>FairHopper WS Client</h1> <h1>FairHopper WS Client</h1>
<div class="row"> <div class="row">
<div class="col-10"> <div class="col-10">
<div class="board-container"> <div class="board-container">
@ -23,7 +22,7 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<h3>Players</h3> <h3>Players</h3>
<ul id="players-content"></ul> <ul class="players" id="players-content"></ul>
</div> </div>
</div> </div>
</div> </div>
@ -46,8 +45,12 @@
document.getElementById("board-content").innerHTML = html; document.getElementById("board-content").innerHTML = html;
} }
function renderCellContent(x, y, content) { function findCell(position) {
const cell = document.getElementById(`cell-${x}-${y}`); return document.getElementById(`cell-${position.x}-${position.y}`);
}
function renderCellContent(position, content) {
const cell = findCell(position);
if (cell) { if (cell) {
cell.innerText = content; cell.innerText = content;
} }
@ -56,7 +59,10 @@
function renderPlayerList(players) { function renderPlayerList(players) {
const html = players.map((player) => { const html = players.map((player) => {
return ` return `
<li>${player.name} (${player.move_count})</li> <li class="${player.reached_destination ? "text-success" : ""}">
${player.name} (${player.move_count})
${player.reached_destination ? "✅" : ""}
</li>
`; `;
}).join(""); }).join("");
document.getElementById("players-content").innerHTML = html; document.getElementById("players-content").innerHTML = html;
@ -64,7 +70,20 @@
function renderPlayers(players) { function renderPlayers(players) {
players.forEach(player => { players.forEach(player => {
renderCellContent(player.position.x, player.position.y, "😎"); const cell = findCell(player.position);
if (cell) {
const playerIcon = "😎";
const html = `
<div class="player-tooltip">${player.name}</div>
${playerIcon}
`;
cell.innerHTML = html;
}
//renderCellContent(player.position.x, player.position.y, "😎");
}); });
} }
@ -79,12 +98,12 @@
function renderObstacles(layers) { function renderObstacles(layers) {
const objects = getLayerObjectsOfType(layers, "OBSTACLE"); const objects = getLayerObjectsOfType(layers, "OBSTACLE");
objects.forEach(obj => { objects.forEach(obj => {
renderCellContent(obj.position.x, obj.position.y, "🔥"); renderCellContent(obj.position, "🔥");
}); });
} }
function renderDestination(position) { function renderDestination(position) {
renderCellContent(position.x, position.y, "🏠"); renderCellContent(position, "🏠");
} }
window.onload = function () { window.onload = function () {

View File

@ -17,4 +17,32 @@ body {
flex: 1; flex: 1;
text-align: center; text-align: center;
background-color: beige; background-color: beige;
position: relative;
}
ul.players {
list-style-type: none;
padding-left: 0;
}
.player-tooltip {
position: absolute;
top: -25px;
font-size: 8pt;
padding: 2px 10px;
color: white;
background-color: darkred;
border-radius: 5px;
z-index: 1000;
}
.player-tooltip::after {
content: " ";
position: absolute;
top: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: darkred transparent transparent transparent;
} }

View File

@ -15,10 +15,25 @@ from hopper.api.dto import (
from hopper.engine import GameEngine from hopper.engine import GameEngine
from hopper.enums import Direction, PlayerMoveResult from hopper.enums import Direction, PlayerMoveResult
from hopper.errors import Collision, PositionOutOfBounds from hopper.errors import Collision, PositionOutOfBounds
from hopper.models.player import Player
router = APIRouter() router = APIRouter()
def get_player(uuid: str, engine: GameEngine = Depends(get_game_engine)) -> Player:
player = engine.players.find(uuid)
if player is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Player not found"
)
if not player.active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Player kicked out due to inactivity",
)
return player
@router.get("/ping", response_model=PingResponse) @router.get("/ping", response_model=PingResponse)
async def ping() -> PingResponse: async def ping() -> PingResponse:
return PingResponse( return PingResponse(
@ -60,19 +75,8 @@ async def start_game(
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
) )
async def get_player_info( async def get_player_info(
uuid: str, player: Player = Depends(get_player),
engine: GameEngine = Depends(get_game_engine),
) -> MovePlayerResponseDto: ) -> MovePlayerResponseDto:
player = engine.players.find(uuid)
if player is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Player not found"
)
if not player.active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Player kicked out due to inactivity",
)
return PlayerInfoResponseDto(player=player) return PlayerInfoResponseDto(player=player)
@ -96,22 +100,11 @@ async def get_player_info(
}, },
) )
async def move_player( async def move_player(
uuid: str,
direction: Direction, direction: Direction,
response: Response, response: Response,
engine: GameEngine = Depends(get_game_engine), engine: GameEngine = Depends(get_game_engine),
player: Player = Depends(get_player),
) -> MovePlayerResponseDto: ) -> MovePlayerResponseDto:
player = engine.players.find(uuid)
if player is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Player not found"
)
if not player.active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Player kicked out due to inactivity",
)
try: try:
move_result = await engine.move_player(player, direction) move_result = await engine.move_player(player, direction)
except PositionOutOfBounds: except PositionOutOfBounds:

View File

@ -69,14 +69,8 @@ class GameEngine:
return player return player
async def move_player( def _move_position(self, position: Position, direction: Direction) -> Position:
self, player: Player, direction: Direction new_position = Position(position.x, position.y)
) -> PlayerMoveResult:
player.reset_timeout()
new_position = Position(player.position.x, player.position.y)
logging.info(f"Player {player} move to {direction}")
if direction == Direction.LEFT: if direction == Direction.LEFT:
new_position.x -= 1 new_position.x -= 1
elif direction == Direction.RIGHT: elif direction == Direction.RIGHT:
@ -87,39 +81,56 @@ class GameEngine:
new_position.y += 1 new_position.y += 1
else: else:
raise ValueError(f"Unhandled direction: {direction}") raise ValueError(f"Unhandled direction: {direction}")
return new_position
async def move_player(
self, player: Player, direction: Direction
) -> PlayerMoveResult:
player.reset_timeout()
# player will not be able to move once they reach the destination
if player.reached_destination:
return PlayerMoveResult.DESTINATION_REACHED
logging.info(f"Player {player} move to {direction}")
new_position = self._move_position(player.position, direction)
player.move_attempt_count += 1 player.move_attempt_count += 1
if not self.position_in_board_bounds(new_position): if not self._position_in_board_bounds(new_position):
raise PositionOutOfBounds() raise PositionOutOfBounds()
if self.colided_with_obstacle(new_position): if self._colided_with_obstacle(new_position):
raise Collision() raise Collision()
player.position = new_position player.position = new_position
player.move_count += 1 player.move_count += 1
if self._is_player_on_destination(player):
player.reached_destination = True
logging.info(f"Player {player} reached destination!")
if self.ws_server: if self.ws_server:
await self.ws_server.send_game_state() await self.ws_server.send_game_state()
if self.is_player_on_destination(player):
logging.info(f"Player {player} reached destination!")
return PlayerMoveResult.DESTINATION_REACHED
self.__debug_print_board() self.__debug_print_board()
if player.reached_destination:
return PlayerMoveResult.DESTINATION_REACHED
await asyncio.sleep(settings.game.MOVE_DELAY) await asyncio.sleep(settings.game.MOVE_DELAY)
return PlayerMoveResult.OK return PlayerMoveResult.OK
def is_player_on_destination(self, player: Player) -> bool: def _is_player_on_destination(self, player: Player) -> bool:
return player.position == self.board.destination.position return player.position == self.board.destination.position
def position_in_board_bounds(self, position: Position) -> bool: def _position_in_board_bounds(self, position: Position) -> bool:
return ( return (
0 <= position.x < self.board.width and 0 <= position.y < self.board.height 0 <= position.x < self.board.width and 0 <= position.y < self.board.height
) )
def colided_with_obstacle(self, position: Position) -> bool: def _colided_with_obstacle(self, position: Position) -> bool:
return self.board.get_object_at_position(position) is not None return self.board.get_object_at_position(position) is not None
def get_board_layout(self) -> BoardLayout: def get_board_layout(self) -> BoardLayout:

View File

@ -22,6 +22,7 @@ class Player:
) )
active: bool = True active: bool = True
can_be_deactivated: bool = True can_be_deactivated: bool = True
reached_destination: bool = False
def reset_timeout(self) -> None: def reset_timeout(self) -> None:
self.last_seen = datetime.datetime.now() self.last_seen = datetime.datetime.now()

View File

@ -15,9 +15,12 @@ class LayerDto(BaseModel):
name: str name: str
objects: list[LayerObjectDto] objects: list[LayerObjectDto]
class GameStatePlayerDto(PlayerDto):
reached_destination: bool
class GameStateDto(BaseModel): class GameStateDto(BaseModel):
board: BoardDto board: BoardDto
destination: DestinationDto destination: DestinationDto
players: list[PlayerDto] players: list[GameStatePlayerDto]
layers: list[LayerDto] layers: list[LayerDto]