This commit is contained in:
2026-05-02 11:42:05 +03:00
parent f5231a2524
commit 76b991a62b

View File

@@ -4,6 +4,11 @@ from datetime import datetime, timedelta
log = logging.getLogger(__name__) 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 = [ STANDARD_BLUEPRINT = [
{"barracks": 1, "farm": 3, "lumber": 2, "stoner": 2, "ironer": 2, "storage": 2, "main": 2, "temple": 1}, {"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}, {"barracks": 1, "farm": 3, "lumber": 3, "stoner": 3, "ironer": 3, "storage": 6, "main": 8},
@@ -42,6 +47,9 @@ RESEARCH_LEVELS = {
"colonize_ship": 13 "colonize_ship": 13
} }
MAX_LOOKAHEAD_PHASES = 2 # How many phases ahead to look if current phase is fully blocked
def evaluate_blueprints(conn): def evaluate_blueprints(conn):
blueprints = conn.execute('SELECT town_id, blueprint_name FROM town_blueprints WHERE is_active = 1').fetchall() blueprints = conn.execute('SELECT town_id, blueprint_name FROM town_blueprints WHERE is_active = 1').fetchall()
log.warning(f"[blueprint] Active blueprints: {len(blueprints)}") log.warning(f"[blueprint] Active blueprints: {len(blueprints)}")
@@ -57,10 +65,10 @@ def evaluate_blueprints(conn):
).fetchone() ).fetchone()
if not town_row: if not town_row:
log.warning(f"[blueprint] No town_state row found for town_id={town_id} — skipping") log.warning(f"[blueprint] No town_state row for town_id={town_id} — skipping")
continue continue
player_id = town_row['player_id'] player_id = town_row['player_id']
town_name_db = town_row['town_name'] town_name_db = town_row['town_name']
town_world_id = town_row['world_id'] 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}") log.warning(f"[blueprint] Town: {town_name_db}, player_id={player_id}, world_id={town_world_id!r}")
@@ -68,43 +76,49 @@ def evaluate_blueprints(conn):
try: try:
town = json.loads(town_row['data']) town = json.loads(town_row['data'])
except Exception as e: except Exception as e:
log.warning(f"[blueprint] Failed to parse town data JSON: {e}") log.warning(f"[blueprint] Failed to parse town JSON: {e}")
continue continue
build_queue = town.get('buildingOrder', []) build_queue = town.get('buildingOrder', [])
buildings = town.get('buildings', {}) buildings = town.get('buildings', {})
build_data = town.get('buildData', {}) build_data = town.get('buildData', {})
log.warning(f"[blueprint] buildings keys: {list(buildings.keys())}")
log.warning(f"[blueprint] buildData keys: {list(build_data.keys())}")
log.warning(f"[blueprint] buildingOrder length: {len(build_queue)}") log.warning(f"[blueprint] buildingOrder length: {len(build_queue)}")
# Don't queue anything if there's already a valid pending/executing command in DB. # ── Guard: don't queue if there's already a pending/executing command ──────
# However, if a pending command has been sitting untouched for >5 minutes it's # Ghost detection: if a 'pending' command has sat untouched for >5 min,
# a ghost (inserted by an old broken blueprint run) — delete it and re-evaluate. # it's stale — delete it so we can re-evaluate fresh.
five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat() five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
db_pending = conn.execute( 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')", "SELECT id, type, status, updated_at FROM commands "
"WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')",
(town_id,) (town_id,)
).fetchall() ).fetchall()
if db_pending: 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)] 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: if ghost_ids:
log.warning(f"[blueprint] Deleting {len(ghost_ids)} ghost pending commands {ghost_ids} for town {town_id}") 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.execute(
f"DELETE FROM commands WHERE id IN ({','.join('?' for _ in ghost_ids)})",
ghost_ids
)
conn.commit() conn.commit()
db_pending = conn.execute( 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')", "SELECT id, type, status, updated_at FROM commands "
"WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')",
(town_id,) (town_id,)
).fetchall() ).fetchall()
if db_pending: if db_pending:
details = [(r['id'], r['type'], r['status']) for r in db_pending] details = [(r['id'], r['type'], r['status']) for r in db_pending]
log.warning(f"[blueprint] Already has {len(db_pending)} pending/executing commands — skipping. Commands: {details}") log.warning(f"[blueprint] Already has {len(db_pending)} queued commands — skipping. {details}")
continue continue
# Calculate Future Levels based on current + game queue # ── Calculate future levels: current buildings + anything in the game build queue ──
future_levels = {k: v for k, v in buildings.items()} future_levels = {k: v for k, v in buildings.items()}
for q_item in build_queue: for q_item in build_queue:
b_type = q_item.get('building_type') or q_item.get('name') b_type = q_item.get('building_type') or q_item.get('name')
@@ -113,76 +127,60 @@ def evaluate_blueprints(conn):
log.warning(f"[blueprint] future_levels: {future_levels}") log.warning(f"[blueprint] future_levels: {future_levels}")
# Find next required building # ── 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 target_building = None
phase_incomplete = False lookahead_used = 0
blocked_phases = 0 first_incomplete_phase_idx = None
for phase_idx, phase in enumerate(STANDARD_BLUEPRINT): for phase_idx, phase in enumerate(STANDARD_BLUEPRINT):
incomplete_buildings = [] # Collect buildings that still need work in this phase
for b_name, req_level in phase.items(): incomplete = [b for b, req in phase.items() if future_levels.get(b, 0) < req]
if future_levels.get(b_name, 0) < req_level:
incomplete_buildings.append(b_name)
if incomplete_buildings: if not incomplete:
phase_incomplete = True continue # Phase is fully complete, move on
log.warning(f"[blueprint] Phase {phase_idx} is incomplete. Missing: {incomplete_buildings}")
waiting_for_resources = False
for b_name in incomplete_buildings: # Track the very first incomplete phase we encounter
b_info = build_data.get(b_name) if first_incomplete_phase_idx is None:
if b_info is None: first_incomplete_phase_idx = phase_idx
log.warning(f"[blueprint] {b_name}: no buildData entry — skipping")
continue
has_max = b_info.get('has_max_level', False)
deps = b_info.get('missing_dependencies')
# Do not trust the frontend 'enough_resources' flag. Grepolis lazily loads this,
# so for background towns it is almost always stale (False). We calculate it manually!
enough = b_info.get('enough_resources')
res = town.get('resources')
if res:
b_wood = b_info.get('wood', 0)
b_stone = b_info.get('stone', 0)
b_iron = b_info.get('iron', 0)
b_pop = b_info.get('pop', 0)
enough = (
res.get('wood', 0) >= b_wood and
res.get('stone', 0) >= b_stone and
res.get('iron', 0) >= b_iron and
res.get('population', 0) >= b_pop
)
log.warning(f"[blueprint] {b_name}: has_max={has_max}, deps={deps}, enough_resources={enough}") log.warning(f"[blueprint] Phase {phase_idx} incomplete: {incomplete}")
if not has_max: # Separate into: genuinely blocked (has_max=True) vs. queueable
if not deps: blocked = [b for b in incomplete if build_data.get(b, {}).get('has_max_level', False)]
if enough != False: queueable = [b for b in incomplete if b not in blocked]
target_building = b_name
log.warning(f"[blueprint] -> SELECTED {b_name}")
break
else:
log.warning(f"[blueprint] -> waiting for resources for {b_name}")
waiting_for_resources = True
if target_building: if queueable:
break # We are in the current phase (or a valid lookahead phase) — queue it!
elif waiting_for_resources: target_building = queueable[0]
log.warning(f"[blueprint] Phase {phase_idx}: blocked by resources, stopping lookahead") log.warning(f"[blueprint] -> SELECTED '{target_building}' from phase {phase_idx}"
break + (f" (lookahead +{lookahead_used})" if lookahead_used else ""))
else: break
log.warning(f"[blueprint] Phase {phase_idx}: all blocked by deps, looking ahead (blocked_phases={blocked_phases+1})")
blocked_phases += 1
if blocked_phases > 2:
log.warning(f"[blueprint] Too many blocked phases, giving up")
break
# Handle Academy Tech Research # 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 target_research = None
if not target_building: if not target_building:
academy_level = future_levels.get('academy', 0) academy_level = future_levels.get('academy', 0)
researched = town.get('researches', {}) researched = town.get('researches', {})
for r_name in RESEARCH_LIST: for r_name in RESEARCH_LIST:
if not researched.get(r_name): if not researched.get(r_name):
req_level = RESEARCH_LEVELS.get(r_name, 99) req_level = RESEARCH_LEVELS.get(r_name, 99)
@@ -193,29 +191,31 @@ def evaluate_blueprints(conn):
log.warning(f"[blueprint] Final: target_building={target_building}, target_research={target_research}") log.warning(f"[blueprint] Final: target_building={target_building}, target_research={target_research}")
# ── Insert command ─────────────────────────────────────────────────────────
now = datetime.utcnow().isoformat()
if target_building: if target_building:
now = datetime.utcnow().isoformat()
# Get next position for this town's build queue
pos_row = conn.execute( pos_row = conn.execute(
"SELECT MAX(position) as max_pos FROM commands" "SELECT MAX(position) as max_pos FROM commands"
" WHERE player_id = ? AND town_id = ? AND type = 'build'" " WHERE player_id = ? AND town_id = ? AND type = 'build'"
" AND status IN ('pending', 'executing')", " AND status IN ('pending', 'executing')",
(str(player_id), str(town_id)) (str(player_id), str(town_id))
).fetchone() ).fetchone()
position = (pos_row['max_pos'] or 0) + 1 position = (pos_row['max_pos'] or 0) + 1
payload_str = json.dumps({"building_id": target_building}) payload_str = json.dumps({"building_id": target_building})
conn.execute(''' conn.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id) INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
''', (str(town_id), town_name_db, 'build', payload_str, position, now, now, str(player_id))) ''', (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}") log.warning(f"[blueprint] Inserted build command: {target_building} for {town_name_db}")
elif target_research: elif target_research:
now = datetime.utcnow().isoformat()
payload_str = json.dumps({"research_id": target_research}) payload_str = json.dumps({"research_id": target_research})
conn.execute(''' conn.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
''', (str(town_id), town_name_db, 'research', payload_str, now, now, str(player_id))) ''', (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}") log.warning(f"[blueprint] Inserted research command: {target_research} for {town_name_db}")
else: else:
log.warning(f"[blueprint] Nothing to do for {town_name_db}") log.warning(f"[blueprint] Nothing to do for {town_name_db}")