[upd] rebalance professions + add game controls to the stats page

This commit is contained in:
Снесарев Максим 2026-01-19 21:03:30 +03:00
parent 308f738c37
commit 25bd13e001
7 changed files with 350 additions and 167 deletions

View File

@ -202,10 +202,28 @@ def create_gather_action(
if success_chance < 1.0: if success_chance < 1.0:
base_cost *= 1.0 + (1.0 - success_chance) * 0.3 base_cost *= 1.0 + (1.0 - success_chance) * 0.3
# Mild personality adjustments (shouldn't dominate the cost) # STRONG profession specialization effect for gathering
if action_type == ActionType.GATHER: if action_type == ActionType.GATHER:
# Cautious agents slightly prefer gathering # Compare gather_preference to other preferences
base_cost *= (0.9 + state.risk_tolerance * 0.2) # Specialists get big discounts, generalists pay penalty
other_prefs = (state.hunt_preference + state.trade_preference) / 2
relative_strength = state.gather_preference / max(0.1, other_prefs)
# relative_strength > 1.0 means gathering is your specialty
# relative_strength < 1.0 means you're NOT a gatherer
if relative_strength >= 1.0:
# Specialist discount: up to 50% off
preference_modifier = 1.0 / relative_strength
else:
# Non-specialist penalty: up to 3x cost
preference_modifier = 1.0 + (1.0 - relative_strength) * 2.0
base_cost *= preference_modifier
# Skill reduces cost further (experienced = efficient)
# skill 0: no bonus, skill 1.0: 40% discount
skill_modifier = 1.0 - state.gathering_skill * 0.4
base_cost *= skill_modifier
return base_cost return base_cost
@ -280,8 +298,28 @@ def create_buy_action(resource_type: ResourceType) -> GOAPAction:
# Trading cost is low (1 energy) # Trading cost is low (1 energy)
base_cost = 0.5 base_cost = 0.5
# Market-oriented agents prefer buying # MILD profession effect for trading (everyone should be able to trade)
base_cost *= (1.5 - state.market_affinity) # Traders get a bonus, but non-traders shouldn't be heavily penalized
# (trading benefits the whole economy)
other_prefs = (state.hunt_preference + state.gather_preference) / 2
relative_strength = state.trade_preference / max(0.1, other_prefs)
if relative_strength >= 1.0:
# Specialist discount: up to 40% off for dedicated traders
preference_modifier = max(0.6, 1.0 / relative_strength)
else:
# Mild non-specialist penalty: up to 50% cost increase
preference_modifier = 1.0 + (1.0 - relative_strength) * 0.5
base_cost *= preference_modifier
# Skill reduces cost (experienced traders are efficient)
# skill 0: no bonus, skill 1.0: 40% discount
skill_modifier = 1.0 - state.trading_skill * 0.4
base_cost *= skill_modifier
# Market affinity still has mild effect
base_cost *= (1.2 - state.market_affinity * 0.4)
# Check if it's a good deal # Check if it's a good deal
if resource_type == ResourceType.MEAT: if resource_type == ResourceType.MEAT:

View File

@ -159,9 +159,28 @@ def _create_hunt() -> GOAPAction:
if config.success_chance < 1.0: if config.success_chance < 1.0:
base_cost *= 1.0 + (1.0 - config.success_chance) * 0.2 base_cost *= 1.0 + (1.0 - config.success_chance) * 0.2
# Risk-tolerant agents prefer hunting # STRONG profession specialization effect for hunting
# Range: 0.85 (high risk tolerance) to 1.15 (low risk tolerance) # Compare hunt_preference to other preferences
risk_modifier = 1.0 + (0.5 - state.risk_tolerance) * 0.3 other_prefs = (state.gather_preference + state.trade_preference) / 2
relative_strength = state.hunt_preference / max(0.1, other_prefs)
# relative_strength > 1.0 means hunting is your specialty
if relative_strength >= 1.0:
# Specialist discount: up to 50% off
preference_modifier = 1.0 / relative_strength
else:
# Non-specialist penalty: up to 3x cost
preference_modifier = 1.0 + (1.0 - relative_strength) * 2.0
base_cost *= preference_modifier
# Skill reduces cost further (experienced hunters are efficient)
# skill 0: no bonus, skill 1.0: 40% discount
skill_modifier = 1.0 - state.hunting_skill * 0.4
base_cost *= skill_modifier
# Risk tolerance still has mild effect
risk_modifier = 1.0 + (0.5 - state.risk_tolerance) * 0.15
base_cost *= risk_modifier base_cost *= risk_modifier
# Big bonus if we have no meat - prioritize getting some # Big bonus if we have no meat - prioritize getting some
@ -295,11 +314,29 @@ def _create_sell_action(resource_type: ResourceType, min_keep: int = 1) -> GOAPA
return result return result
def cost(state: WorldState) -> float: def cost(state: WorldState) -> float:
# Selling has low cost # Selling has low cost - everyone should be able to sell excess
base_cost = 1.0 base_cost = 1.0
# Hoarders reluctant to sell # MILD profession effect for selling (everyone should be able to trade)
base_cost *= (0.5 + state.hoarding_rate) other_prefs = (state.hunt_preference + state.gather_preference) / 2
relative_strength = state.trade_preference / max(0.1, other_prefs)
if relative_strength >= 1.0:
# Specialist discount: up to 40% off for dedicated traders
preference_modifier = max(0.6, 1.0 / relative_strength)
else:
# Mild non-specialist penalty: up to 50% cost increase
preference_modifier = 1.0 + (1.0 - relative_strength) * 0.5
base_cost *= preference_modifier
# Skill reduces cost (experienced traders know the market)
# skill 0: no bonus, skill 1.0: 40% discount
skill_modifier = 1.0 - state.trading_skill * 0.4
base_cost *= skill_modifier
# Hoarders reluctant to sell (mild effect)
base_cost *= (0.8 + state.hoarding_rate * 0.4)
return base_cost return base_cost

View File

@ -73,6 +73,16 @@ class WorldState:
market_affinity: float = 0.5 market_affinity: float = 0.5
is_trader: bool = False is_trader: bool = False
# Profession preferences (0.5-1.5 range, higher = more preferred)
gather_preference: float = 1.0
hunt_preference: float = 1.0
trade_preference: float = 1.0
# Skill levels (0.0-1.0, higher = more skilled)
hunting_skill: float = 0.0
gathering_skill: float = 0.0
trading_skill: float = 0.0
# Critical thresholds (from config) # Critical thresholds (from config)
critical_threshold: float = 0.25 critical_threshold: float = 0.25
low_threshold: float = 0.45 low_threshold: float = 0.45
@ -297,6 +307,12 @@ def create_world_state(
risk_tolerance=agent.personality.risk_tolerance, risk_tolerance=agent.personality.risk_tolerance,
market_affinity=agent.personality.market_affinity, market_affinity=agent.personality.market_affinity,
is_trader=is_trader, is_trader=is_trader,
gather_preference=agent.personality.gather_preference,
hunt_preference=agent.personality.hunt_preference,
trade_preference=agent.personality.trade_preference,
hunting_skill=agent.skills.hunting,
gathering_skill=agent.skills.gathering,
trading_skill=agent.skills.trading,
critical_threshold=agent_config.critical_threshold, critical_threshold=agent_config.critical_threshold,
low_threshold=0.45, # Could also be in config low_threshold=0.45, # Could also be in config
) )

View File

@ -278,6 +278,17 @@
</div> </div>
</div> </div>
<div class="stats-footer"> <div class="stats-footer">
<div class="controls">
<button id="btn-initialize-stats" class="btn btn-secondary" title="Reset Simulation">
<span class="btn-icon"></span> Reset
</button>
<button id="btn-step-stats" class="btn btn-primary" title="Advance one turn">
<span class="btn-icon"></span> Step
</button>
<button id="btn-auto-stats" class="btn btn-toggle" title="Toggle auto mode">
<span class="btn-icon"></span> Auto
</button>
</div>
<div class="stats-summary-bar"> <div class="stats-summary-bar">
<div class="summary-item"> <div class="summary-item">
<span class="summary-label">Turn</span> <span class="summary-label">Turn</span>
@ -304,6 +315,11 @@
<span class="summary-value" id="stats-gini">0.00</span> <span class="summary-value" id="stats-gini">0.00</span>
</div> </div>
</div> </div>
<div class="speed-control">
<label for="speed-slider-stats">Speed</label>
<input type="range" id="speed-slider-stats" min="50" max="1000" value="150" step="50">
<span id="speed-display-stats">150ms</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -132,6 +132,12 @@ export default class GameScene extends Phaser.Scene {
goapPlanView: document.getElementById('goap-plan-view'), goapPlanView: document.getElementById('goap-plan-view'),
goapActionsList: document.getElementById('goap-actions-list'), goapActionsList: document.getElementById('goap-actions-list'),
chartGoapGoals: document.getElementById('chart-goap-goals'), chartGoapGoals: document.getElementById('chart-goap-goals'),
// Stats screen controls (duplicated for stats page)
btnStepStats: document.getElementById('btn-step-stats'),
btnAutoStats: document.getElementById('btn-auto-stats'),
btnInitializeStats: document.getElementById('btn-initialize-stats'),
speedSliderStats: document.getElementById('speed-slider-stats'),
speedDisplayStats: document.getElementById('speed-display-stats'),
}; };
// GOAP state // GOAP state
@ -172,6 +178,21 @@ export default class GameScene extends Phaser.Scene {
btnCloseStats.removeEventListener('click', this.boundHandlers.closeStats); btnCloseStats.removeEventListener('click', this.boundHandlers.closeStats);
} }
// Stats screen controls cleanup
const { btnStepStats, btnAutoStats, btnInitializeStats, speedSliderStats } = this.domCache;
if (btnStepStats && this.boundHandlers.step) {
btnStepStats.removeEventListener('click', this.boundHandlers.step);
}
if (btnAutoStats && this.boundHandlers.auto) {
btnAutoStats.removeEventListener('click', this.boundHandlers.auto);
}
if (btnInitializeStats && this.boundHandlers.init) {
btnInitializeStats.removeEventListener('click', this.boundHandlers.init);
}
if (speedSliderStats && this.boundHandlers.speedStats) {
speedSliderStats.removeEventListener('input', this.boundHandlers.speedStats);
}
// Destroy charts // Destroy charts
Object.values(this.charts).forEach(chart => chart?.destroy()); Object.values(this.charts).forEach(chart => chart?.destroy());
this.charts = {}; this.charts = {};
@ -286,19 +307,34 @@ export default class GameScene extends Phaser.Scene {
setupUIControls() { setupUIControls() {
const { btnStep, btnAuto, btnInitialize, btnStats, btnCloseStats, speedSlider, speedDisplay, tabButtons } = this.domCache; const { btnStep, btnAuto, btnInitialize, btnStats, btnCloseStats, speedSlider, speedDisplay, tabButtons } = this.domCache;
const { btnStepStats, btnAutoStats, btnInitializeStats, speedSliderStats, speedDisplayStats } = this.domCache;
// Create bound handlers for later cleanup // Create bound handlers for later cleanup
this.boundHandlers.step = () => this.handleStep(); this.boundHandlers.step = () => this.handleStep();
this.boundHandlers.auto = () => this.toggleAutoMode(); this.boundHandlers.auto = () => this.toggleAutoMode();
this.boundHandlers.init = () => this.handleInitialize(); this.boundHandlers.init = () => this.handleInitialize();
// Speed handler that syncs both sliders
this.boundHandlers.speed = (e) => { this.boundHandlers.speed = (e) => {
this.autoSpeed = parseInt(e.target.value); this.autoSpeed = parseInt(e.target.value);
if (speedDisplay) speedDisplay.textContent = `${this.autoSpeed}ms`; if (speedDisplay) speedDisplay.textContent = `${this.autoSpeed}ms`;
if (speedDisplayStats) speedDisplayStats.textContent = `${this.autoSpeed}ms`;
if (speedSliderStats) speedSliderStats.value = this.autoSpeed;
if (this.isAutoMode) this.restartAutoMode(); if (this.isAutoMode) this.restartAutoMode();
}; };
this.boundHandlers.speedStats = (e) => {
this.autoSpeed = parseInt(e.target.value);
if (speedDisplay) speedDisplay.textContent = `${this.autoSpeed}ms`;
if (speedDisplayStats) speedDisplayStats.textContent = `${this.autoSpeed}ms`;
if (speedSlider) speedSlider.value = this.autoSpeed;
if (this.isAutoMode) this.restartAutoMode();
};
this.boundHandlers.openStats = () => this.showStatsScreen(); this.boundHandlers.openStats = () => this.showStatsScreen();
this.boundHandlers.closeStats = () => this.hideStatsScreen(); this.boundHandlers.closeStats = () => this.hideStatsScreen();
// Main controls
if (btnStep) btnStep.addEventListener('click', this.boundHandlers.step); if (btnStep) btnStep.addEventListener('click', this.boundHandlers.step);
if (btnAuto) btnAuto.addEventListener('click', this.boundHandlers.auto); if (btnAuto) btnAuto.addEventListener('click', this.boundHandlers.auto);
if (btnInitialize) btnInitialize.addEventListener('click', this.boundHandlers.init); if (btnInitialize) btnInitialize.addEventListener('click', this.boundHandlers.init);
@ -306,6 +342,12 @@ export default class GameScene extends Phaser.Scene {
if (btnStats) btnStats.addEventListener('click', this.boundHandlers.openStats); if (btnStats) btnStats.addEventListener('click', this.boundHandlers.openStats);
if (btnCloseStats) btnCloseStats.addEventListener('click', this.boundHandlers.closeStats); if (btnCloseStats) btnCloseStats.addEventListener('click', this.boundHandlers.closeStats);
// Stats screen controls (same handlers)
if (btnStepStats) btnStepStats.addEventListener('click', this.boundHandlers.step);
if (btnAutoStats) btnAutoStats.addEventListener('click', this.boundHandlers.auto);
if (btnInitializeStats) btnInitializeStats.addEventListener('click', this.boundHandlers.init);
if (speedSliderStats) speedSliderStats.addEventListener('input', this.boundHandlers.speedStats);
// Tab switching // Tab switching
tabButtons?.forEach(btn => { tabButtons?.forEach(btn => {
btn.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab)); btn.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
@ -380,15 +422,19 @@ export default class GameScene extends Phaser.Scene {
toggleAutoMode() { toggleAutoMode() {
this.isAutoMode = !this.isAutoMode; this.isAutoMode = !this.isAutoMode;
const { btnAuto, btnStep } = this.domCache; const { btnAuto, btnStep, btnAutoStats, btnStepStats } = this.domCache;
if (this.isAutoMode) { if (this.isAutoMode) {
btnAuto?.classList.add('active'); btnAuto?.classList.add('active');
btnAutoStats?.classList.add('active');
btnStep?.setAttribute('disabled', 'true'); btnStep?.setAttribute('disabled', 'true');
btnStepStats?.setAttribute('disabled', 'true');
this.startAutoMode(); this.startAutoMode();
} else { } else {
btnAuto?.classList.remove('active'); btnAuto?.classList.remove('active');
btnAutoStats?.classList.remove('active');
btnStep?.removeAttribute('disabled'); btnStep?.removeAttribute('disabled');
btnStepStats?.removeAttribute('disabled');
this.stopAutoMode(); this.stopAutoMode();
} }
} }

View File

@ -613,7 +613,8 @@ body {
font-weight: 500; font-weight: 500;
} }
#speed-slider { #speed-slider,
#speed-slider-stats {
width: 120px; width: 120px;
height: 4px; height: 4px;
-webkit-appearance: none; -webkit-appearance: none;
@ -623,7 +624,8 @@ body {
cursor: pointer; cursor: pointer;
} }
#speed-slider::-webkit-slider-thumb { #speed-slider::-webkit-slider-thumb,
#speed-slider-stats::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 14px; width: 14px;
@ -633,7 +635,8 @@ body {
cursor: pointer; cursor: pointer;
} }
#speed-display { #speed-display,
#speed-display-stats {
font-family: var(--font-mono); font-family: var(--font-mono);
min-width: 50px; min-width: 50px;
} }
@ -939,16 +942,21 @@ body {
/* Stats Footer */ /* Stats Footer */
.stats-footer { .stats-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-sm) var(--space-lg); padding: var(--space-sm) var(--space-lg);
background: var(--bg-primary); background: var(--bg-primary);
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
flex-shrink: 0; flex-shrink: 0;
height: 56px;
} }
.stats-summary-bar { .stats-summary-bar {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: var(--space-xl); gap: var(--space-xl);
flex: 1;
} }
.summary-item { .summary-item {
@ -1049,6 +1057,28 @@ body {
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-md); gap: var(--space-md);
} }
.stats-footer {
flex-wrap: wrap;
height: auto;
gap: var(--space-sm);
padding: var(--space-sm);
}
.stats-footer .controls {
order: 1;
width: auto;
}
.stats-footer .stats-summary-bar {
order: 2;
width: 100%;
}
.stats-footer .speed-control {
order: 3;
width: auto;
}
} }
/* ================================= /* =================================