Concurrency.
This commit is contained in:
parent
6a2f94337e
commit
3c783056cf
@ -18,38 +18,50 @@ ws_manager = WebSocketManager(game_state)
|
|||||||
economy_engine = EconomyEngine(game_state)
|
economy_engine = EconomyEngine(game_state)
|
||||||
database = Database()
|
database = Database()
|
||||||
|
|
||||||
|
# --- HYBRID MODEL RE-IMPLEMENTED ---
|
||||||
|
last_economy_tick_time = time.time()
|
||||||
TICK_INTERVAL = 10 # seconds
|
TICK_INTERVAL = 10 # seconds
|
||||||
|
|
||||||
# --- FIX: Reverting to a simple, reliable 10-second loop ---
|
async def trigger_economy_update_and_save():
|
||||||
|
"""Triggers an economy tick, broadcasts updates concurrently, saves, and resets the timer."""
|
||||||
|
global last_economy_tick_time
|
||||||
|
|
||||||
|
logger.debug("Triggering full economy update and save cycle...")
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
economy_engine.tick()
|
||||||
|
|
||||||
|
update_tasks = []
|
||||||
|
for player_id, player in game_state.players.items():
|
||||||
|
if player_id in ws_manager.active_connections:
|
||||||
|
task = ws_manager.send_to_player(player_id, {
|
||||||
|
"type": "player_stats_update",
|
||||||
|
"player": player.to_dict()
|
||||||
|
})
|
||||||
|
update_tasks.append(task)
|
||||||
|
|
||||||
|
if update_tasks:
|
||||||
|
await asyncio.gather(*update_tasks)
|
||||||
|
|
||||||
|
database.save_game_state(game_state)
|
||||||
|
|
||||||
|
# Reset the global tick timer after any update
|
||||||
|
last_economy_tick_time = time.time()
|
||||||
|
|
||||||
|
duration = time.perf_counter() - start_time
|
||||||
|
logger.info(f"Full economy update cycle completed in {duration:.4f} seconds.")
|
||||||
|
|
||||||
async def economy_loop():
|
async def economy_loop():
|
||||||
"""A simple loop that runs the economy tick every 10 seconds."""
|
"""Runs periodically to check if a passive economy update is needed."""
|
||||||
|
global last_economy_tick_time
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(TICK_INTERVAL)
|
# Check if 10 seconds have passed since the last tick (from any source)
|
||||||
|
if time.time() - last_economy_tick_time > TICK_INTERVAL:
|
||||||
|
logger.info("Triggering timed economy update for idle players.")
|
||||||
|
await trigger_economy_update_and_save()
|
||||||
|
|
||||||
logger.info("Triggering scheduled economy tick.")
|
# Check frequently for responsiveness
|
||||||
start_time = time.perf_counter()
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# 1. Process one economy tick
|
|
||||||
economy_engine.tick()
|
|
||||||
|
|
||||||
# 2. Broadcast updates concurrently
|
|
||||||
update_tasks = []
|
|
||||||
for player_id, player in game_state.players.items():
|
|
||||||
if player_id in ws_manager.active_connections:
|
|
||||||
task = ws_manager.send_to_player(player_id, {
|
|
||||||
"type": "player_stats_update",
|
|
||||||
"player": player.to_dict()
|
|
||||||
})
|
|
||||||
update_tasks.append(task)
|
|
||||||
|
|
||||||
if update_tasks:
|
|
||||||
await asyncio.gather(*update_tasks)
|
|
||||||
|
|
||||||
# 3. Save the new state
|
|
||||||
database.save_game_state(game_state)
|
|
||||||
|
|
||||||
duration = time.perf_counter() - start_time
|
|
||||||
logger.info(f"Economy tick cycle completed in {duration:.4f} seconds.")
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
@ -58,9 +70,9 @@ async def lifespan(app: FastAPI):
|
|||||||
database.init_db()
|
database.init_db()
|
||||||
game_state.load_state(database.load_game_state())
|
game_state.load_state(database.load_game_state())
|
||||||
|
|
||||||
# Start the simple economy loop
|
# Start the hybrid economy loop
|
||||||
task = asyncio.create_task(economy_loop())
|
task = asyncio.create_task(economy_loop())
|
||||||
logger.info(f"Economy loop started with a {TICK_INTERVAL}-second interval.")
|
logger.info("Hybrid economy loop started.")
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@ -117,8 +129,9 @@ async def handle_message(websocket: WebSocket, data: dict):
|
|||||||
result = game_state.place_building(player_id, data["building_type"], data["x"], data["y"])
|
result = game_state.place_building(player_id, data["building_type"], data["x"], data["y"])
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
await ws_manager.broadcast({"type": "building_placed", "building": result["building"]})
|
await ws_manager.broadcast({"type": "building_placed", "building": result["building"]})
|
||||||
# --- CHANGE: Action now only saves, economy is handled by the loop ---
|
# --- CHANGE: Trigger an INSTANT economy update on financial action ---
|
||||||
database.save_game_state(game_state)
|
logger.info(f"Player {player_id} action triggered immediate economy update.")
|
||||||
|
await trigger_economy_update_and_save()
|
||||||
else:
|
else:
|
||||||
await websocket.send_json({"type": "error", "message": result["error"]})
|
await websocket.send_json({"type": "error", "message": result["error"]})
|
||||||
|
|
||||||
@ -126,8 +139,9 @@ async def handle_message(websocket: WebSocket, data: dict):
|
|||||||
result = game_state.remove_building(player_id, data["x"], data["y"])
|
result = game_state.remove_building(player_id, data["x"], data["y"])
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
await ws_manager.broadcast({"type": "building_removed", "x": data["x"], "y": data["y"]})
|
await ws_manager.broadcast({"type": "building_removed", "x": data["x"], "y": data["y"]})
|
||||||
# --- CHANGE: Action now only saves, economy is handled by the loop ---
|
# --- CHANGE: Trigger an INSTANT economy update on financial action ---
|
||||||
database.save_game_state(game_state)
|
logger.info(f"Player {player_id} action triggered immediate economy update.")
|
||||||
|
await trigger_economy_update_and_save()
|
||||||
else:
|
else:
|
||||||
await websocket.send_json({"type": "error", "message": result["error"]})
|
await websocket.send_json({"type": "error", "message": result["error"]})
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ stats-display {
|
|||||||
/* Building Toolbox */
|
/* Building Toolbox */
|
||||||
building-toolbox {
|
building-toolbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
right: 10px;
|
||||||
top: 120px;
|
top: 120px;
|
||||||
background: var(--bg-medium);
|
background: var(--bg-medium);
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ class BuildingToolbox extends HTMLElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.app = null;
|
this.app = null;
|
||||||
|
this.buildingItems = []; // A cache for the building DOM elements
|
||||||
this.buildings = [
|
this.buildings = [
|
||||||
{ type: 'small_house', name: 'Small House', cost: 5000, income: -50, pop: 10 },
|
{ type: 'small_house', name: 'Small House', cost: 5000, income: -50, pop: 10 },
|
||||||
{ type: 'medium_house', name: 'Medium House', cost: 12000, income: -120, pop: 25 },
|
{ type: 'medium_house', name: 'Medium House', cost: 12000, income: -120, pop: 25 },
|
||||||
@ -24,18 +25,30 @@ class BuildingToolbox extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.render();
|
// 1. Initial full render happens only once
|
||||||
|
this.innerHTML = this.renderHTML();
|
||||||
|
|
||||||
|
// 2. Cache the DOM elements for efficient future updates
|
||||||
|
this.buildingItems = this.querySelectorAll('.building-item');
|
||||||
|
|
||||||
|
// 3. Add click handlers only once
|
||||||
|
this.addClickHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback() {
|
attributeChangedCallback() {
|
||||||
this.render();
|
// When player stats change, run the lightweight update function
|
||||||
|
// instead of a full re-render.
|
||||||
|
if (this.buildingItems.length > 0) {
|
||||||
|
this.updateItemStates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderHTML() {
|
||||||
|
// This function generates the initial HTML string.
|
||||||
const money = parseInt(this.getAttribute('player-money') || '0');
|
const money = parseInt(this.getAttribute('player-money') || '0');
|
||||||
const population = parseInt(this.getAttribute('player-population') || '0');
|
const population = parseInt(this.getAttribute('player-population') || '0');
|
||||||
|
|
||||||
this.innerHTML = `
|
return `
|
||||||
<div style="font-weight: bold; margin-bottom: 10px; font-size: 16px; border-bottom: 1px solid var(--border-color); padding-bottom: 5px;">
|
<div style="font-weight: bold; margin-bottom: 10px; font-size: 16px; border-bottom: 1px solid var(--border-color); padding-bottom: 5px;">
|
||||||
Buildings
|
Buildings
|
||||||
</div>
|
</div>
|
||||||
@ -44,7 +57,6 @@ class BuildingToolbox extends HTMLElement {
|
|||||||
const canAfford = money >= building.cost;
|
const canAfford = money >= building.cost;
|
||||||
const meetsReq = !building.req || population >= building.req;
|
const meetsReq = !building.req || population >= building.req;
|
||||||
const enabled = canAfford && meetsReq;
|
const enabled = canAfford && meetsReq;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div
|
<div
|
||||||
class="building-item"
|
class="building-item"
|
||||||
@ -55,8 +67,7 @@ class BuildingToolbox extends HTMLElement {
|
|||||||
background: var(--bg-light);
|
background: var(--bg-light);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
cursor: ${enabled ? 'pointer' : 'not-allowed'};
|
cursor: ${enabled ? 'pointer' : 'not-allowed'};
|
||||||
opacity: ${enabled ? '1' : '0.5'};
|
opacity: ${enabled ? '1' : '0.5'};"
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div style="font-weight: bold; margin-bottom: 4px;">
|
<div style="font-weight: bold; margin-bottom: 4px;">
|
||||||
${building.name}
|
${building.name}
|
||||||
@ -74,12 +85,15 @@ class BuildingToolbox extends HTMLElement {
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add click handlers
|
addClickHandlers() {
|
||||||
this.querySelectorAll('.building-item').forEach(item => {
|
this.buildingItems.forEach(item => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
const type = item.dataset.type;
|
const type = item.dataset.type;
|
||||||
const building = this.buildings.find(b => b.type === type);
|
const building = this.buildings.find(b => b.type === type);
|
||||||
|
const money = parseInt(this.getAttribute('player-money') || '0');
|
||||||
|
const population = parseInt(this.getAttribute('player-population') || '0');
|
||||||
|
|
||||||
if (money >= building.cost && (!building.req || population >= building.req)) {
|
if (money >= building.cost && (!building.req || population >= building.req)) {
|
||||||
if (this.app) {
|
if (this.app) {
|
||||||
@ -89,6 +103,26 @@ class BuildingToolbox extends HTMLElement {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateItemStates() {
|
||||||
|
// This lightweight function only updates styles, preserving the DOM and scroll position.
|
||||||
|
const money = parseInt(this.getAttribute('player-money') || '0');
|
||||||
|
const population = parseInt(this.getAttribute('player-population') || '0');
|
||||||
|
|
||||||
|
this.buildingItems.forEach(item => {
|
||||||
|
const type = item.dataset.type;
|
||||||
|
const building = this.buildings.find(b => b.type === type);
|
||||||
|
if (!building) return;
|
||||||
|
|
||||||
|
const canAfford = money >= building.cost;
|
||||||
|
const meetsReq = !building.req || population >= building.req;
|
||||||
|
const enabled = canAfford && meetsReq;
|
||||||
|
|
||||||
|
// Directly update only the styles that change
|
||||||
|
item.style.opacity = enabled ? '1' : '0.5';
|
||||||
|
item.style.cursor = enabled ? 'pointer' : 'not-allowed';
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('building-toolbox', BuildingToolbox);
|
customElements.define('building-toolbox', BuildingToolbox);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user