ChatCraft / backend /game /bot.py
gabraken's picture
Tweaking
f187b8b
"""
BotPlayer — simple rule-based AI opponent.
Strategy phases (evaluated every BOT_TICK_INTERVAL ticks ≈ 2 s):
1. SCVs gather minerals immediately
2. Build Supply Depot when minerals >= 100
3. Build Barracks when Supply Depot active & minerals >= 150
4. Train Marines continuously when Barracks active
5. Attack enemy base when military count >= 4
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from .buildings import BuildingStatus, BuildingType
from .commands import ActionType, GameAction, ParsedCommand
from .units import UnitType, UNIT_DEFS
if TYPE_CHECKING:
from .engine import GameEngine
log = logging.getLogger(__name__)
BOT_PLAYER_ID = "__bot__"
BOT_PLAYER_NAME = "Bot IA"
# Ticks between each bot decision (~4 ticks/s → 10 ticks ≈ 2.5 s)
BOT_TICK_INTERVAL = 10
def _cmd(*actions: GameAction) -> ParsedCommand:
return ParsedCommand(actions=list(actions), feedback_template="")
class BotPlayer:
"""Rule-based bot that drives one player slot in the game engine."""
def __init__(self, engine: "GameEngine", player_id: str) -> None:
self.engine = engine
self.player_id = player_id
self._initial_gather_done = False
# ------------------------------------------------------------------
# Main entry point — called by engine every BOT_TICK_INTERVAL ticks
# ------------------------------------------------------------------
def act(self) -> None:
state = self.engine.state
player = state.players.get(self.player_id)
if not player:
return
# 1. Send all SCVs to gather minerals (once)
if not self._initial_gather_done:
self.engine.apply_command(
self.player_id,
_cmd(GameAction(
type=ActionType.GATHER,
unit_selector="all_scv",
resource_type="minerals",
)),
)
self._initial_gather_done = True
# 2. Supply Depot
if (
player.minerals >= 100
and not player.has_active(BuildingType.SUPPLY_DEPOT)
and not _building_in_progress(player, BuildingType.SUPPLY_DEPOT)
):
self.engine.apply_command(
self.player_id,
_cmd(GameAction(
type=ActionType.BUILD,
building_type=BuildingType.SUPPLY_DEPOT.value,
)),
)
# 3. Barracks
if (
player.minerals >= 150
and player.has_active(BuildingType.SUPPLY_DEPOT)
and not player.has_active(BuildingType.BARRACKS)
and not _building_in_progress(player, BuildingType.BARRACKS)
):
self.engine.apply_command(
self.player_id,
_cmd(GameAction(
type=ActionType.BUILD,
building_type=BuildingType.BARRACKS.value,
)),
)
# 4. Train Marines (only if supply and minerals allow)
marine_cost = UNIT_DEFS[UnitType.MARINE].mineral_cost
marine_supply = UNIT_DEFS[UnitType.MARINE].supply_cost
queued_supply = _queued_supply(player)
free_supply = player.supply_max - player.supply_used - queued_supply
can_train_count = max(0, min(2, free_supply // marine_supply, player.minerals // marine_cost))
if (
player.has_active(BuildingType.BARRACKS)
and can_train_count >= 1
):
self.engine.apply_command(
self.player_id,
_cmd(GameAction(
type=ActionType.TRAIN,
unit_type=UnitType.MARINE.value,
count=can_train_count,
)),
)
# 5. Attack with military when strong enough
military = [u for u in player.units.values() if u.unit_type != UnitType.SCV]
if len(military) >= 4:
self.engine.apply_command(
self.player_id,
_cmd(GameAction(
type=ActionType.ATTACK,
unit_selector="all_military",
target_zone="enemy_base",
)),
)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _building_in_progress(player, bt: BuildingType) -> bool:
return any(
b.building_type == bt and b.status == BuildingStatus.CONSTRUCTING
for b in player.buildings.values()
)
def _queued_supply(player) -> int:
"""Supply that will be used by units already in production queues."""
total = 0
for b in player.buildings.values():
if b.status in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED):
continue
for item in b.production_queue:
try:
ut = UnitType(item.unit_type)
total += UNIT_DEFS[ut].supply_cost
except ValueError:
pass
return total