feat(game): damage bots for squeezing diagonally between impassable terrain pieces
This commit is contained in:
parent
cb732ad626
commit
f04b6c78aa
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue