222 lines
10 KiB
Python
222 lines
10 KiB
Python
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}")
|