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}'.",
)
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(
direction=direction_name,
dx=dx,
@ -866,6 +871,7 @@ class GameEngine:
target_y=target_y,
available=True,
reason=None,
strength_penalty=strength_penalty,
)
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):
total = 0
total = 0.0
for mid in party.member_ids:
p = self.players.get(mid)
if p:
total += p.strength
party.total_strength = max(1, total)
party.total_strength = max(0.1, round(total, 1))
def _resolve_3bout_battle_internal(
self, party1: Party, party2: Party
@ -938,8 +944,8 @@ class GameEngine:
bouts: List[BattleBout] = []
p1_bouts_won = 0
p2_bouts_won = 0
p1_total_score = 0
p2_total_score = 0
p1_total_score = 0.0
p2_total_score = 0.0
self._update_party_strength(party1)
self._update_party_strength(party2)
@ -947,8 +953,8 @@ class GameEngine:
for bout_idx in range(1, 4):
r1 = random.randint(1, 20)
r2 = random.randint(1, 20)
score1 = party1.total_strength * r1
score2 = party2.total_strength * r2
score1 = round(party1.total_strength * r1, 1)
score2 = round(party2.total_strength * r2, 1)
p1_total_score += score1
p2_total_score += score2
@ -1292,11 +1298,18 @@ class GameEngine:
m.x += dx
m.y += dy
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)
if check.strength_penalty > 0:
self._update_party_strength(party)
else:
player.x = check.target_x
player.y = check.target_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)
new_pos = {"x": player.x, "y": player.y}

View File

@ -83,6 +83,7 @@ class MoveCheckResult(BaseModel):
target_y: int
available: bool
reason: Optional[str] = None
strength_penalty: float = 0.0
class AvailableMovesResponse(BaseModel):
@ -117,7 +118,7 @@ class Obstacle(BaseModel):
class PlayerCreate(BaseModel):
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")
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")
@classmethod
@ -144,7 +145,7 @@ class Player(BaseModel):
color: str
x: int
y: int
strength: int = 1
strength: float = 1.0
score: int = 0
party_id: Optional[str] = None
is_party_leader: bool = False
@ -158,7 +159,7 @@ class Party(BaseModel):
leader_id: str
leader_name: 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))
@ -208,11 +209,11 @@ class PartyDefeatResult(BaseModel):
class BattleBout(BaseModel):
bout_number: int
party1_roll: int = Field(..., ge=1, le=20, description="D20 roll (1 to 20) for party 1")
party1_strength: int
party1_score: int = Field(..., description="party1_strength * party1_roll")
party1_strength: float
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_strength: int
party2_score: int = Field(..., description="party2_strength * party2_roll")
party2_strength: float
party2_score: float = Field(..., description="party2_strength * party2_roll")
winner_name: Optional[str] = None
@ -222,8 +223,8 @@ class BattleResult(BaseModel):
party2_name: str
party1_bouts_won: int
party2_bouts_won: int
party1_total_score: int
party2_total_score: int
party1_total_score: float
party2_total_score: float
winner_party_id: str
winner_party_name: 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",
)
new_party_size: int
new_party_strength: int
new_party_strength: float
class BattleRequest(BaseModel):
@ -267,7 +268,7 @@ class RadarTarget(BaseModel):
color: str
x: int
y: int
strength: int = 1
strength: float = 1.0
distance: int
party_id: Optional[str] = None
party_name: Optional[str] = None

View File

@ -215,7 +215,9 @@ class SmartBotAgent:
def dist(chk: Dict[str, Any]) -> int:
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]):
"""Move adjacent/towards the target while avoiding obstacles."""

View File

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