feat(game): damage bots for squeezing diagonally between impassable terrain pieces

This commit is contained in:
Isaac Johnson 2026-09-06 09:00:16 -05:00
parent cb732ad626
commit f04b6c78aa
4 changed files with 35 additions and 19 deletions

View File

@ -858,6 +858,11 @@ class GameEngine:
reason=f"Target square ({target_x}, {target_y}) is occupied by player '{occupant.name}'.", reason=f"Target square ({target_x}, {target_y}) is occupied by player '{occupant.name}'.",
) )
strength_penalty = 0.0
if dx != 0 and dy != 0:
if (player.x + dx, player.y) in self.obstacles and (player.x, player.y + dy) in self.obstacles:
strength_penalty = 0.1
return MoveCheckResult( return MoveCheckResult(
direction=direction_name, direction=direction_name,
dx=dx, dx=dx,
@ -866,6 +871,7 @@ class GameEngine:
target_y=target_y, target_y=target_y,
available=True, available=True,
reason=None, reason=None,
strength_penalty=strength_penalty,
) )
async def check_single_move(self, player_id: str, direction_name: str) -> MoveCheckResult: async def check_single_move(self, player_id: str, direction_name: str) -> MoveCheckResult:
@ -925,12 +931,12 @@ class GameEngine:
) )
def _update_party_strength(self, party: Party): def _update_party_strength(self, party: Party):
total = 0 total = 0.0
for mid in party.member_ids: for mid in party.member_ids:
p = self.players.get(mid) p = self.players.get(mid)
if p: if p:
total += p.strength total += p.strength
party.total_strength = max(1, total) party.total_strength = max(0.1, round(total, 1))
def _resolve_3bout_battle_internal( def _resolve_3bout_battle_internal(
self, party1: Party, party2: Party self, party1: Party, party2: Party
@ -938,8 +944,8 @@ class GameEngine:
bouts: List[BattleBout] = [] bouts: List[BattleBout] = []
p1_bouts_won = 0 p1_bouts_won = 0
p2_bouts_won = 0 p2_bouts_won = 0
p1_total_score = 0 p1_total_score = 0.0
p2_total_score = 0 p2_total_score = 0.0
self._update_party_strength(party1) self._update_party_strength(party1)
self._update_party_strength(party2) self._update_party_strength(party2)
@ -947,8 +953,8 @@ class GameEngine:
for bout_idx in range(1, 4): for bout_idx in range(1, 4):
r1 = random.randint(1, 20) r1 = random.randint(1, 20)
r2 = random.randint(1, 20) r2 = random.randint(1, 20)
score1 = party1.total_strength * r1 score1 = round(party1.total_strength * r1, 1)
score2 = party2.total_strength * r2 score2 = round(party2.total_strength * r2, 1)
p1_total_score += score1 p1_total_score += score1
p2_total_score += score2 p2_total_score += score2
@ -1292,11 +1298,18 @@ class GameEngine:
m.x += dx m.x += dx
m.y += dy m.y += dy
m.visited_locations.append({"x": m.x, "y": m.y}) m.visited_locations.append({"x": m.x, "y": m.y})
if check.strength_penalty > 0:
penalty = 0.2 if m.is_party_leader else 0.1
m.strength = round(max(0.1, m.strength - penalty), 1)
affected_players.append(m) affected_players.append(m)
if check.strength_penalty > 0:
self._update_party_strength(party)
else: else:
player.x = check.target_x player.x = check.target_x
player.y = check.target_y player.y = check.target_y
player.visited_locations.append({"x": player.x, "y": player.y}) player.visited_locations.append({"x": player.x, "y": player.y})
if check.strength_penalty > 0:
player.strength = round(max(0.1, player.strength - 0.1), 1)
affected_players.append(player) affected_players.append(player)
new_pos = {"x": player.x, "y": player.y} new_pos = {"x": player.x, "y": player.y}

View File

@ -83,6 +83,7 @@ class MoveCheckResult(BaseModel):
target_y: int target_y: int
available: bool available: bool
reason: Optional[str] = None reason: Optional[str] = None
strength_penalty: float = 0.0
class AvailableMovesResponse(BaseModel): class AvailableMovesResponse(BaseModel):
@ -117,7 +118,7 @@ class Obstacle(BaseModel):
class PlayerCreate(BaseModel): class PlayerCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=32, description="Display name of the player") name: str = Field(..., min_length=1, max_length=32, description="Display name of the player")
color: str = Field(..., description="Hex color code (e.g. #FF5733) or valid CSS color name") color: str = Field(..., description="Hex color code (e.g. #FF5733) or valid CSS color name")
strength: int = Field(default=1, ge=1, description="Bot strength (default is 1)") strength: float = Field(default=1.0, ge=1, description="Bot strength (default is 1)")
@field_validator("name") @field_validator("name")
@classmethod @classmethod
@ -144,7 +145,7 @@ class Player(BaseModel):
color: str color: str
x: int x: int
y: int y: int
strength: int = 1 strength: float = 1.0
score: int = 0 score: int = 0
party_id: Optional[str] = None party_id: Optional[str] = None
is_party_leader: bool = False is_party_leader: bool = False
@ -158,7 +159,7 @@ class Party(BaseModel):
leader_id: str leader_id: str
leader_name: str leader_name: str
member_ids: List[str] member_ids: List[str]
total_strength: int = 1 total_strength: float = 1.0
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@ -208,11 +209,11 @@ class PartyDefeatResult(BaseModel):
class BattleBout(BaseModel): class BattleBout(BaseModel):
bout_number: int bout_number: int
party1_roll: int = Field(..., ge=1, le=20, description="D20 roll (1 to 20) for party 1") party1_roll: int = Field(..., ge=1, le=20, description="D20 roll (1 to 20) for party 1")
party1_strength: int party1_strength: float
party1_score: int = Field(..., description="party1_strength * party1_roll") party1_score: float = Field(..., description="party1_strength * party1_roll")
party2_roll: int = Field(..., ge=1, le=20, description="D20 roll (1 to 20) for party 2") party2_roll: int = Field(..., ge=1, le=20, description="D20 roll (1 to 20) for party 2")
party2_strength: int party2_strength: float
party2_score: int = Field(..., description="party2_strength * party2_roll") party2_score: float = Field(..., description="party2_strength * party2_roll")
winner_name: Optional[str] = None winner_name: Optional[str] = None
@ -222,8 +223,8 @@ class BattleResult(BaseModel):
party2_name: str party2_name: str
party1_bouts_won: int party1_bouts_won: int
party2_bouts_won: int party2_bouts_won: int
party1_total_score: int party1_total_score: float
party2_total_score: int party2_total_score: float
winner_party_id: str winner_party_id: str
winner_party_name: str winner_party_name: str
winner_leader_id: str winner_leader_id: str
@ -239,7 +240,7 @@ class BattleResult(BaseModel):
description="Remaining bots from the defeated party that merged into the winning party", description="Remaining bots from the defeated party that merged into the winning party",
) )
new_party_size: int new_party_size: int
new_party_strength: int new_party_strength: float
class BattleRequest(BaseModel): class BattleRequest(BaseModel):
@ -267,7 +268,7 @@ class RadarTarget(BaseModel):
color: str color: str
x: int x: int
y: int y: int
strength: int = 1 strength: float = 1.0
distance: int distance: int
party_id: Optional[str] = None party_id: Optional[str] = None
party_name: Optional[str] = None party_name: Optional[str] = None

View File

@ -215,7 +215,9 @@ class SmartBotAgent:
def dist(chk: Dict[str, Any]) -> int: def dist(chk: Dict[str, Any]) -> int:
return max(abs(chk["target_x"] - target_x), abs(chk["target_y"] - target_y)) return max(abs(chk["target_x"] - target_x), abs(chk["target_y"] - target_y))
return min(valid_moves.keys(), key=lambda d: dist(valid_moves[d])) # Bot chooses whether to consider strength penalty: AI prioritizes keeping strength intact
# (orders by has_penalty first, then distance)
return min(valid_moves.keys(), key=lambda d: (valid_moves[d].get("strength_penalty", 0.0) > 0, dist(valid_moves[d])))
def _step_or_attack(self, target: Dict[str, Any]): def _step_or_attack(self, target: Dict[str, Any]):
"""Move adjacent/towards the target while avoiding obstacles.""" """Move adjacent/towards the target while avoiding obstacles."""

View File

@ -485,7 +485,7 @@ export const BoardCanvas: React.FC<BoardCanvasProps> = ({
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
const text = `${player.name} [⚡${player.strength}]`; const text = `${player.name} [⚡${player.strength.toFixed(1)}]`;
const textMetrics = ctx.measureText(text); const textMetrics = ctx.measureText(text);
const bgWidth = textMetrics.width + 12; const bgWidth = textMetrics.width + 12;
const bgHeight = 16; const bgHeight = 16;