blueprint function

This commit is contained in:
2026-05-02 00:08:43 +03:00
parent 05785c294e
commit 5f6855ec69
9 changed files with 295 additions and 10 deletions

114
blueprint_engine.py Normal file
View File

@@ -0,0 +1,114 @@
import json
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
}
def evaluate_blueprints(conn):
blueprints = conn.execute('SELECT town_id, blueprint_name FROM town_blueprints WHERE is_active = 1').fetchall()
if not blueprints:
return
for row in blueprints:
town_id = str(row['town_id'])
town_row = conn.execute('SELECT data FROM town_state WHERE town_id = ?', (town_id,)).fetchone()
if not town_row:
continue
try:
town = json.loads(town_row['data'])
except Exception:
continue
build_queue = town.get('build_queue', [])
buildings = town.get('buildings', {})
build_data = town.get('build_data', {})
# Don't queue anything if there's already a pending/executing command in DB
db_pending = conn.execute("SELECT id FROM commands WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')", (town_id,)).fetchall()
if db_pending:
continue
# Calculate Future Levels based on current + game 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
# Find next required building
target_building = None
for phase in STANDARD_BLUEPRINT:
incomplete = False
for b_name, req_level in phase.items():
if future_levels.get(b_name, 0) < req_level:
incomplete = True
b_info = build_data.get(b_name)
if b_info and not b_info.get('has_max_level'):
if b_info.get('enough_resources') != False:
target_building = b_name
break
if incomplete:
break
# Handle Academy Tech Research if no building was prioritized
target_research = None
if not target_building:
academy_level = future_levels.get('academy', 0)
researched = town.get('researches', {})
research_queue = town.get('research_queue', [])
for r_name in RESEARCH_LIST:
if not researched.get(r_name) and r_name not in [q.get('type') for q in research_queue]:
# Check if academy level is high enough
req_level = RESEARCH_LEVELS.get(r_name, 99)
if academy_level >= req_level:
target_research = r_name
break
if target_building:
payload_str = json.dumps({"building_id": target_building})
conn.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, player_id)
VALUES (?, ?, ?, ?, ?, ?)
''', (town_id, town.get('town_name'), 'build', payload_str, 'pending', town.get('player_id')))
elif target_research:
payload_str = json.dumps({"research_id": target_research})
conn.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, player_id)
VALUES (?, ?, ?, ?, ?, ?)
''', (town_id, town.get('town_name'), 'research', payload_str, 'pending', town.get('player_id')))

10
db.py
View File

@@ -92,6 +92,16 @@ def init_db():
''')
c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)')
# Blueprints - assigns a blueprint to a specific town
c.execute('''
CREATE TABLE IF NOT EXISTS town_blueprints (
town_id TEXT PRIMARY KEY,
blueprint_name TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
''')
# Migration: add new columns if upgrading an existing database
for _col in [
'ALTER TABLE town_state ADD COLUMN player_id TEXT',

View File

@@ -4,6 +4,7 @@ import json
from datetime import datetime, timedelta
import os
from flask import make_response
from blueprint_engine import evaluate_blueprints
api = Blueprint('api', __name__)
@@ -94,6 +95,13 @@ def receive_state():
datetime.utcnow().isoformat()
))
conn.commit()
try:
evaluate_blueprints(conn)
conn.commit()
except Exception as e:
print("Error evaluating blueprints:", e)
conn.close()
return jsonify({'ok': True, 'towns_updated': len(towns)})

View File

@@ -178,11 +178,13 @@ def get_towns():
player_id = request.args.get('player_id')
conn = get_db()
rows = conn.execute('''
SELECT town_id, town_name, player, player_id, alliance_id,
world_id, x, y, sea, data, updated_at
FROM town_state
WHERE player_id = ?
ORDER BY town_name ASC
SELECT ts.town_id, ts.town_name, ts.player, ts.player_id, ts.alliance_id,
ts.world_id, ts.x, ts.y, ts.sea, ts.data, ts.updated_at,
tb.blueprint_name, tb.is_active as blueprint_active
FROM town_state ts
LEFT JOIN town_blueprints tb ON ts.town_id = tb.town_id AND tb.is_active = 1
WHERE ts.player_id = ?
ORDER BY ts.town_name ASC
''', (player_id, )).fetchall()
conn.close()
@@ -220,11 +222,51 @@ def get_towns():
'bonuses': d.get('bonuses', {}),
'wonder_points': d.get('wonder_points', 0),
'total_points': d.get('total_points', 0),
'alliance_name': d.get('alliance_name', None)
'alliance_name': d.get('alliance_name', None),
'blueprint_name': row['blueprint_name'],
'blueprint_active': bool(row['blueprint_active'])
})
return jsonify(towns)
# ------------------------------------------------------------------
# POST /dashboard/blueprints
# Toggle a blueprint for a specific town
# ------------------------------------------------------------------
@dashboard.route('/dashboard/blueprints', methods=['POST'])
@login_required
def toggle_blueprint():
data = request.get_json(silent=True) or {}
town_id = data.get('town_id')
blueprint_name = data.get('blueprint_name', 'Standard Growth')
if not town_id:
return jsonify({'error': 'missing town_id'}), 400
conn = get_db()
# Check if currently active
row = conn.execute('SELECT is_active FROM town_blueprints WHERE town_id = ?', (town_id,)).fetchone()
new_state = 1
if row and row['is_active'] == 1:
new_state = 0 # Toggle off
conn.execute('''
INSERT INTO town_blueprints (town_id, blueprint_name, is_active, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(town_id) DO UPDATE SET
blueprint_name = excluded.blueprint_name,
is_active = excluded.is_active,
updated_at = excluded.updated_at
''', (town_id, blueprint_name, new_state, datetime.utcnow().isoformat()))
conn.commit()
conn.close()
return jsonify({'ok': True, 'is_active': bool(new_state), 'blueprint_name': blueprint_name})
# ------------------------------------------------------------------
# GET /dashboard/client-status
# Returns whether the Tampermonkey client is considered online.

View File

@@ -318,9 +318,9 @@ tr:hover td { background: #1e1e40; }
}
/* ==========================================================================
Building, Academy, Unit & Market Modals
Building, Academy, Unit, Market & Blueprint Modals
========================================================================== */
#building-modal-overlay, #academy-modal-overlay, #unit-modal-overlay, #market-modal-overlay {
#building-modal-overlay, #academy-modal-overlay, #unit-modal-overlay, #market-modal-overlay, #blueprints-modal-overlay {
display: none;
position: fixed;
inset: 0;
@@ -329,9 +329,9 @@ tr:hover td { background: #1e1e40; }
align-items: center;
justify-content: center;
}
#building-modal-overlay.open, #academy-modal-overlay.open, #unit-modal-overlay.open, #market-modal-overlay.open { display: flex; }
#building-modal-overlay.open, #academy-modal-overlay.open, #unit-modal-overlay.open, #market-modal-overlay.open, #blueprints-modal-overlay.open { display: flex; }
#building-modal, #academy-modal, #unit-modal, #market-modal {
#building-modal, #academy-modal, #unit-modal, #market-modal, #blueprints-modal {
background: #16213e;
border: 2px solid #c8a44a;
border-radius: 10px;

View File

@@ -182,6 +182,30 @@ window.sendCommand = async function() {
}
payload = { research_id };
} else if (type === 'blueprints') {
const blueprint_name = window.selectedBlueprintName;
if (!blueprint_name) return alert('Παρακαλώ επιλέξτε Blueprint.');
try {
const res = await fetch('/dashboard/blueprints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
town_id: town.town_id,
blueprint_name: blueprint_name
})
});
const data = await res.json();
if (data.ok) {
alert(data.is_active ? 'Blueprint ενεργοποιήθηκε!' : 'Blueprint απενεργοποιήθηκε!');
window.fetchTowns(); // refresh towns to update UI state
} else {
alert('Σφάλμα: ' + data.error);
}
} catch(e) {
alert('Failed to toggle blueprint: ' + e);
}
return; // Exit here as we don't send this to /dashboard/commands
}
try {

View File

@@ -49,6 +49,12 @@ window.updateSelectionDisplay = function() {
}
} else if (type === 'market_offer') {
labelEl.innerHTML = `🛒 Ρυθμίσεις Παζαριού`;
} else if (type === 'blueprints') {
if (window.selectedBlueprintName) {
labelEl.innerHTML = `📜 ${window.selectedBlueprintName}`;
} else {
labelEl.innerHTML = `-- Επιλέξτε Blueprint --`;
}
}
};
@@ -58,6 +64,22 @@ window.reopenActiveModal = function() {
else if (type === 'recruit') window.openUnitModal();
else if (type === 'research') window.openAcademyModal();
else if (type === 'market_offer') window.openMarketModal();
else if (type === 'blueprints') window.openBlueprintsModal();
};
window.selectedBlueprintName = null;
window.selectBlueprint = function(name) {
window.selectedBlueprintName = name;
window.updateSelectionDisplay();
window.closeBlueprintsModal();
};
window.openBlueprintsModal = function() {
document.getElementById('blueprints-modal-overlay').classList.add('open');
};
window.closeBlueprintsModal = function(e) {
if (e && e.target.id !== 'blueprints-modal-overlay' && !e.target.classList.contains('modal-close')) return;
document.getElementById('blueprints-modal-overlay').classList.remove('open');
};
window.openMarketModal = function() {

View File

@@ -212,4 +212,43 @@ window.renderTownDetails = function() {
if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>';
document.getElementById('td-units').innerHTML = unitsHtml;
// ---- Blueprint Lock & Indicator ----
const isBlueprintActive = !!t.blueprint_active;
const bpName = t.blueprint_name || 'Standard Growth';
if (isBlueprintActive) {
document.getElementById('td-general').innerHTML += `
<div style="margin-top:10px; padding: 4px 8px; background: rgba(200,164,74,0.15); border-left: 3px solid #c8a44a; border-radius:4px; font-size:0.8rem;">
🤖 Blueprint: <strong style="color:#c8a44a">${bpName}</strong> <span style="color:#2ecc71">(ΕΝΕΡΓΟ)</span>
</div>
`;
}
const btnBuild = document.getElementById('seg-build');
const btnResearch = document.getElementById('seg-research');
if (btnBuild && btnResearch) {
if (isBlueprintActive) {
btnBuild.style.opacity = '0.3';
btnBuild.style.pointerEvents = 'none';
btnBuild.title = 'Απενεργοποιημένο λόγω Blueprint';
btnResearch.style.opacity = '0.3';
btnResearch.style.pointerEvents = 'none';
btnResearch.title = 'Απενεργοποιημένο λόγω Blueprint';
if (window.currentCmdType === 'build' || window.currentCmdType === 'research') {
window.setCmdType('recruit');
}
} else {
btnBuild.style.opacity = '1';
btnBuild.style.pointerEvents = 'auto';
btnBuild.title = '';
btnResearch.style.opacity = '1';
btnResearch.style.pointerEvents = 'auto';
btnResearch.title = '';
}
}
};

View File

@@ -91,6 +91,7 @@
<button class="seg-btn" id="seg-recruit" onclick="window.setCmdType('recruit', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">⚔️ Στρατός</button>
<button class="seg-btn" id="seg-market" onclick="window.setCmdType('market_offer', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🛒 Παζάρι</button>
<button class="seg-btn" id="seg-research" onclick="window.setCmdType('research', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🦉 Έρευνα</button>
<button class="seg-btn" id="seg-blueprints" onclick="window.setCmdType('blueprints', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">📜 Blueprints</button>
</div>
<!-- Dynamic Selection Area -->
@@ -127,6 +128,31 @@
</div>
<!-- ====== Blueprints Modal ====== -->
<div class="modal-overlay" id="blueprints-modal-overlay" onclick="window.closeBlueprintsModal(event)">
<div class="custom-modal" id="blueprints-modal" style="max-width: 400px;">
<div class="modal-header">
<h3>📜 Επιλογή Blueprint</h3>
<button class="modal-close" onclick="window.closeBlueprintsModal()"></button>
</div>
<div style="padding: 15px;">
<p style="color:#ccc; font-size:0.85rem; margin-bottom:15px;">Επιλέξτε ένα Blueprint για να αναλάβει το Python την αυτόματη κατασκευή της πόλης.</p>
<div id="blueprint-list" style="display:flex; flex-direction:column; gap:10px; margin-bottom:20px;">
<!-- Currently just one blueprint -->
<div class="bld-card" id="bp-card-standard" onclick="window.selectBlueprint('Standard Growth')" style="width:100%; justify-content:flex-start; cursor:pointer;">
<span class="bld-icon" style="font-size:2rem;">🏙️</span>
<div style="display:flex; flex-direction:column; align-items:flex-start;">
<span class="bld-name" style="margin-top:0; font-size:1rem; font-weight:bold;">Standard Growth</span>
<span style="font-size:0.75rem; color:#888;">Αυτόματη ανάπτυξη κτιρίων & ακαδημίας</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ====== Market Modal ====== -->
<div class="modal-overlay" id="market-modal-overlay" onclick="window.closeMarketModal(event)">
<div class="custom-modal" id="market-modal" style="max-width: 500px;">