[upd] rebalance professions + add game controls to the stats page
This commit is contained in:
parent
308f738c37
commit
25bd13e001
@ -202,10 +202,28 @@ def create_gather_action(
|
||||
if success_chance < 1.0:
|
||||
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:
|
||||
# Cautious agents slightly prefer gathering
|
||||
base_cost *= (0.9 + state.risk_tolerance * 0.2)
|
||||
# Compare gather_preference to other preferences
|
||||
# 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
|
||||
|
||||
@ -280,8 +298,28 @@ def create_buy_action(resource_type: ResourceType) -> GOAPAction:
|
||||
# Trading cost is low (1 energy)
|
||||
base_cost = 0.5
|
||||
|
||||
# Market-oriented agents prefer buying
|
||||
base_cost *= (1.5 - state.market_affinity)
|
||||
# MILD profession effect for trading (everyone should be able to trade)
|
||||
# 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
|
||||
if resource_type == ResourceType.MEAT:
|
||||
|
||||
@ -159,9 +159,28 @@ def _create_hunt() -> GOAPAction:
|
||||
if config.success_chance < 1.0:
|
||||
base_cost *= 1.0 + (1.0 - config.success_chance) * 0.2
|
||||
|
||||
# Risk-tolerant agents prefer hunting
|
||||
# Range: 0.85 (high risk tolerance) to 1.15 (low risk tolerance)
|
||||
risk_modifier = 1.0 + (0.5 - state.risk_tolerance) * 0.3
|
||||
# STRONG profession specialization effect for hunting
|
||||
# Compare hunt_preference to other preferences
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
def cost(state: WorldState) -> float:
|
||||
# Selling has low cost
|
||||
# Selling has low cost - everyone should be able to sell excess
|
||||
base_cost = 1.0
|
||||
|
||||
# Hoarders reluctant to sell
|
||||
base_cost *= (0.5 + state.hoarding_rate)
|
||||
# MILD profession effect for selling (everyone should be able to trade)
|
||||
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
|
||||
|
||||
|
||||
@ -73,6 +73,16 @@ class WorldState:
|
||||
market_affinity: float = 0.5
|
||||
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_threshold: float = 0.25
|
||||
low_threshold: float = 0.45
|
||||
@ -297,6 +307,12 @@ def create_world_state(
|
||||
risk_tolerance=agent.personality.risk_tolerance,
|
||||
market_affinity=agent.personality.market_affinity,
|
||||
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,
|
||||
low_threshold=0.45, # Could also be in config
|
||||
)
|
||||
|
||||
@ -278,6 +278,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<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="summary-item">
|
||||
<span class="summary-label">Turn</span>
|
||||
@ -304,6 +315,11 @@
|
||||
<span class="summary-value" id="stats-gini">0.00</span>
|
||||
</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>
|
||||
|
||||
@ -132,6 +132,12 @@ export default class GameScene extends Phaser.Scene {
|
||||
goapPlanView: document.getElementById('goap-plan-view'),
|
||||
goapActionsList: document.getElementById('goap-actions-list'),
|
||||
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
|
||||
@ -172,6 +178,21 @@ export default class GameScene extends Phaser.Scene {
|
||||
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
|
||||
Object.values(this.charts).forEach(chart => chart?.destroy());
|
||||
this.charts = {};
|
||||
@ -286,19 +307,34 @@ export default class GameScene extends Phaser.Scene {
|
||||
|
||||
setupUIControls() {
|
||||
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
|
||||
this.boundHandlers.step = () => this.handleStep();
|
||||
this.boundHandlers.auto = () => this.toggleAutoMode();
|
||||
this.boundHandlers.init = () => this.handleInitialize();
|
||||
|
||||
// Speed handler that syncs both sliders
|
||||
this.boundHandlers.speed = (e) => {
|
||||
this.autoSpeed = parseInt(e.target.value);
|
||||
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();
|
||||
};
|
||||
|
||||
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.closeStats = () => this.hideStatsScreen();
|
||||
|
||||
// Main controls
|
||||
if (btnStep) btnStep.addEventListener('click', this.boundHandlers.step);
|
||||
if (btnAuto) btnAuto.addEventListener('click', this.boundHandlers.auto);
|
||||
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 (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
|
||||
tabButtons?.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
|
||||
@ -380,15 +422,19 @@ export default class GameScene extends Phaser.Scene {
|
||||
|
||||
toggleAutoMode() {
|
||||
this.isAutoMode = !this.isAutoMode;
|
||||
const { btnAuto, btnStep } = this.domCache;
|
||||
const { btnAuto, btnStep, btnAutoStats, btnStepStats } = this.domCache;
|
||||
|
||||
if (this.isAutoMode) {
|
||||
btnAuto?.classList.add('active');
|
||||
btnAutoStats?.classList.add('active');
|
||||
btnStep?.setAttribute('disabled', 'true');
|
||||
btnStepStats?.setAttribute('disabled', 'true');
|
||||
this.startAutoMode();
|
||||
} else {
|
||||
btnAuto?.classList.remove('active');
|
||||
btnAutoStats?.classList.remove('active');
|
||||
btnStep?.removeAttribute('disabled');
|
||||
btnStepStats?.removeAttribute('disabled');
|
||||
this.stopAutoMode();
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,7 +613,8 @@ body {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#speed-slider {
|
||||
#speed-slider,
|
||||
#speed-slider-stats {
|
||||
width: 120px;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
@ -623,7 +624,8 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#speed-slider::-webkit-slider-thumb {
|
||||
#speed-slider::-webkit-slider-thumb,
|
||||
#speed-slider-stats::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
@ -633,7 +635,8 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#speed-display {
|
||||
#speed-display,
|
||||
#speed-display-stats {
|
||||
font-family: var(--font-mono);
|
||||
min-width: 50px;
|
||||
}
|
||||
@ -939,16 +942,21 @@ body {
|
||||
|
||||
/* Stats Footer */
|
||||
.stats-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.stats-summary-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-xl);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
@ -1049,6 +1057,28 @@ body {
|
||||
flex-wrap: wrap;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* =================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user