import json import logging from datetime import datetime, timedelta log = logging.getLogger(__name__) # --------------------------------------------------------------------------- # STANDARD_BLUEPRINT — ordered list of phases. # Each phase is a dict of { building_name: required_level }. # The engine works through phases in order, queueing one building at a time. # --------------------------------------------------------------------------- STANDARD_BLUEPRINT = [ {"barracks": 1, "farm": 3, "lumber": 2, "stoner": 2, "ironer": 2, "storage": 2, "main": 2, "temple": 1}, {"barracks": 1, "farm": 3, "lumber": 3, "stoner": 3, "ironer": 3, "storage": 6, "main": 8}, {"farm": 8, "lumber": 8, "ironer": 8, "stoner": 8, "market": 5, "temple": 5, "barracks": 5}, {"academy": 13}, {"storage": 12, "farm": 12}, {"main": 25}, {"storage": 21, "farm": 15}, {"lumber": 15, "stoner": 10, "ironer": 12}, {"docks": 10}, {"academy": 30}, {"farm": 20, "storage": 25}, {"market": 15, "trade_office": 1, "hide": 10}, {"market": 30, "farm": 35, "thermal": 1, "academy": 36}, {"farm": 45, "storage": 35, "lumber": 40, "ironer": 40, "stoner": 40}, {"temple": 30} ] RESEARCH_LIST = [ "booty", "pottery", "architecture", "building_crane", "shipwright", "plow", "mathematics", "combat_experience", "strong_wine", "take_over", "colonize_ship" ] RESEARCH_LEVELS = { "booty": 7, "pottery": 7, "architecture": 10, "building_crane": 13, "shipwright": 13, "plow": 22, "mathematics": 25, "combat_experience": 34, "strong_wine": 34, "take_over": 28, "colonize_ship": 13 } MAX_LOOKAHEAD_PHASES = 2 # How many phases ahead to look if current phase is fully blocked def evaluate_blueprints(conn): blueprints = conn.execute('SELECT town_id, blueprint_name FROM town_blueprints WHERE is_active = 1').fetchall() log.warning(f"[blueprint] Active blueprints: {len(blueprints)}") if not blueprints: return for row in blueprints: town_id = str(row['town_id']) log.warning(f"[blueprint] Evaluating town_id={town_id}") town_row = conn.execute( 'SELECT data, player_id, town_name, world_id FROM town_state WHERE town_id = ?', (town_id,) ).fetchone() if not town_row: log.warning(f"[blueprint] No town_state row for town_id={town_id} — skipping") continue player_id = town_row['player_id'] town_name_db = town_row['town_name'] town_world_id = town_row['world_id'] log.warning(f"[blueprint] Town: {town_name_db}, player_id={player_id}, world_id={town_world_id!r}") try: town = json.loads(town_row['data']) except Exception as e: log.warning(f"[blueprint] Failed to parse town JSON: {e}") continue build_queue = town.get('buildingOrder', []) buildings = town.get('buildings', {}) build_data = town.get('buildData', {}) log.warning(f"[blueprint] buildingOrder length: {len(build_queue)}") # ── Guard: don't queue if there's already a pending/executing command ────── # Ghost detection: if a 'pending' command has sat untouched for >5 min, # it's stale — delete it so we can re-evaluate fresh. five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat() db_pending = conn.execute( "SELECT id, type, status, updated_at FROM commands " "WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')", (town_id,) ).fetchall() if db_pending: ghost_ids = [ r['id'] for r in db_pending if r['status'] == 'pending' and (r['updated_at'] is None or r['updated_at'] < five_min_ago) ] if ghost_ids: log.warning(f"[blueprint] Deleting {len(ghost_ids)} ghost commands {ghost_ids}") conn.execute( f"DELETE FROM commands WHERE id IN ({','.join('?' for _ in ghost_ids)})", ghost_ids ) conn.commit() db_pending = conn.execute( "SELECT id, type, status, updated_at FROM commands " "WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')", (town_id,) ).fetchall() if db_pending: details = [(r['id'], r['type'], r['status']) for r in db_pending] log.warning(f"[blueprint] Already has {len(db_pending)} queued commands — skipping. {details}") continue # ── Calculate future levels: current buildings + anything in the game build queue ── future_levels = {k: v for k, v in buildings.items()} for q_item in build_queue: b_type = q_item.get('building_type') or q_item.get('name') if b_type: future_levels[b_type] = future_levels.get(b_type, 0) + 1 log.warning(f"[blueprint] future_levels: {future_levels}") # ── Simple phase search ──────────────────────────────────────────────────── # Strategy: # 1. Find the first phase that has at least one building below its target. # 2. Queue the first incomplete building from that phase — no resource checks, # no dependency checks, exactly like adding it manually. # 3. If every incomplete building in that phase is flagged has_max_level=True # (meaning the game truly refuses to build it), look up to MAX_LOOKAHEAD_PHASES # phases ahead for something to queue instead. target_building = None lookahead_used = 0 first_incomplete_phase_idx = None for phase_idx, phase in enumerate(STANDARD_BLUEPRINT): # Collect buildings that still need work in this phase incomplete = [b for b, req in phase.items() if future_levels.get(b, 0) < req] if not incomplete: continue # Phase is fully complete, move on # Track the very first incomplete phase we encounter if first_incomplete_phase_idx is None: first_incomplete_phase_idx = phase_idx log.warning(f"[blueprint] Phase {phase_idx} incomplete: {incomplete}") # Separate into: genuinely blocked (has_max=True) vs. queueable blocked = [b for b in incomplete if build_data.get(b, {}).get('has_max_level', False)] queueable = [b for b in incomplete if b not in blocked] if queueable: # We are in the current phase (or a valid lookahead phase) — queue it! target_building = queueable[0] log.warning(f"[blueprint] -> SELECTED '{target_building}' from phase {phase_idx}" + (f" (lookahead +{lookahead_used})" if lookahead_used else "")) break # Everything incomplete in this phase is has_max_level blocked. # Allow limited lookahead. if phase_idx == first_incomplete_phase_idx or lookahead_used < MAX_LOOKAHEAD_PHASES: if phase_idx != first_incomplete_phase_idx: lookahead_used += 1 log.warning(f"[blueprint] Phase {phase_idx}: all incomplete buildings are has_max — " f"looking ahead (lookahead_used={lookahead_used})") continue # Ran out of lookahead budget — stop log.warning(f"[blueprint] Lookahead exhausted after {lookahead_used} extra phases — giving up") break # ── Academy Research (fallback when no building target) ─────────────────── target_research = None if not target_building: academy_level = future_levels.get('academy', 0) researched = town.get('researches', {}) for r_name in RESEARCH_LIST: if not researched.get(r_name): req_level = RESEARCH_LEVELS.get(r_name, 99) if academy_level >= req_level: target_research = r_name log.warning(f"[blueprint] -> Research target: {r_name}") break log.warning(f"[blueprint] Final: target_building={target_building}, target_research={target_research}") # ── Insert command ───────────────────────────────────────────────────────── now = datetime.utcnow().isoformat() if target_building: pos_row = conn.execute( "SELECT MAX(position) as max_pos FROM commands" " WHERE player_id = ? AND town_id = ? AND type = 'build'" " AND status IN ('pending', 'executing')", (str(player_id), str(town_id)) ).fetchone() position = (pos_row['max_pos'] or 0) + 1 payload_str = json.dumps({"building_id": target_building}) conn.execute(''' INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?) ''', (str(town_id), town_name_db, 'build', payload_str, position, now, now, str(player_id))) log.warning(f"[blueprint] Inserted build command: {target_building} for {town_name_db}") elif target_research: payload_str = json.dumps({"research_id": target_research}) conn.execute(''' INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?) ''', (str(town_id), town_name_db, 'research', payload_str, now, now, str(player_id))) log.warning(f"[blueprint] Inserted research command: {target_research} for {town_name_db}") else: log.warning(f"[blueprint] Nothing to do for {town_name_db}")