diff --git a/backend/app/game.py b/backend/app/game.py index 441a160..3658872 100644 --- a/backend/app/game.py +++ b/backend/app/game.py @@ -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} diff --git a/backend/app/models.py b/backend/app/models.py index bcfb34b..7ebf712 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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 diff --git a/botagent/bot_agent.py b/botagent/bot_agent.py index e63d25a..7f887a5 100644 --- a/botagent/bot_agent.py +++ b/botagent/bot_agent.py @@ -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.""" diff --git a/frontend/src/components/BoardCanvas.tsx b/frontend/src/components/BoardCanvas.tsx index b6793e8..4cb1045 100644 --- a/frontend/src/components/BoardCanvas.tsx +++ b/frontend/src/components/BoardCanvas.tsx @@ -485,7 +485,7 @@ export const BoardCanvas: React.FC = ({ 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;