Compare commits
2 Commits
integrated
...
f54344a17f
| Author | SHA1 | Date | |
|---|---|---|---|
| f54344a17f | |||
| 3ac07f3072 |
@ -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 () {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user