StavanKhobare's picture
Update documentation, add blog, and simplify inference script
312c390
# BoardSim — Full Mechanics Reference
> Authoritative math and design reference for the BoardSim environment
> (organisation-agnostic boardroom simulation).
> Target audience: hackathon judges who want internals, and future contributors.
> See `README.md` for the submission overview.
---
## 1. State variables
State lives in `BoardState.state_dict`, initialised in `BoardSimEnvironment.reset()` at `envs/board_sim_env/server/board_sim_env_environment.py:536`.
### Core company state (mutated each round by event consequences)
| Field | Initial | Range | Unit | Meaning |
|---|---|---|---|---|
| `revenue` | 2,000,000 | [0, 1e12] | USD/year | Annual recurring revenue |
| `burn_rate` | 1,200,000 | [0, 1e10] | USD/month | Monthly cash expenditure |
| `runway_months` | 14.0 | [0, 120] | months | Time until cash = 0 |
| `product_readiness` | 0.45 | [0, 1] | fraction | Shippability / quality of the product |
| `market_share` | 0.08 | [0, 1] | fraction | % of total addressable market |
| `team_morale` | 0.70 | [0, 1] | fraction | Team retention / engagement signal |
| `investor_confidence` | 0.65 | [0, 1] | fraction | Board investors' belief in success |
| `regulatory_risk` | 0.20 | [0, 1] | fraction | Legal / compliance exposure |
### Coalition state
| Field | Initial | Range | Update rule |
|---|---|---|---|
| `trust[CTO]` | 0.5 | [0.1, 1.0] | ±0.08 per round depending on alignment with the *winning* decision |
| `trust[CFO]` | 0.5 | [0.1, 1.0] | same |
| `trust[Investor Rep]` | 0.5 | [0.1, 1.0] | same |
| `trust[Independent]` | 0.5 | [0.1, 1.0] | same |
Trust feeds back via two channels:
- **NPC confidence**: `confidence += (trust − 0.5) × 0.30`, clipped.
- **Vote weight multiplier**: `trust_mult = clamp(trust × 2.0, 0.5, 1.5)` applied to that NPC's tally contribution next round.
### Bookkeeping
| Field | Purpose |
|---|---|
| `round` | 1..10, increments each step |
| `profitability_score` | Composite recomputed at end of each step |
| `history` | Per-round log: agent_decision, winning_decision, vote_tally, pitch_scores, pitch_used |
| `trust_history` | Per-round snapshot of all 4 trust values |
| `done_reason` | `"runway_exhausted"` / `"acquisition"` / `"finished_10"` / `None` |
| `winning_decision` | Last round's vote winner |
---
## 2. Profitability score
```
profitability_score = clamp(raw, 0, 100)
raw =
min(revenue / 8_000_000, 1.0) × 22 # revenue term (max 22)
+ max(0, 1 − burn_rate / 1_400_000) × 18 # burn efficiency (max 18)
+ min(runway_months / 18.0, 1.0) × 18 # runway term (max 18)
− max(0, (6 − runway_months) / 6) × 10 # low-runway penalty (bites < 6 mo)
+ min(market_share, 0.50) / 0.50 × 14 # market share (max 14)
+ product_readiness × 10 # product readiness (max 10)
+ team_morale × 7 # team morale (max 7)
+ investor_confidence × 11 # investor confidence (max 11)
− regulatory_risk × 18 # regulatory drag (max −18)
```
Initial state ≈ 37.3/100. Theoretical max = 100.
---
## 3. Transition
```
next_state = current_state + consequences[winning_decision] × (1 + ε)
where ε ~ N(0, 0.15) per consequence value, fixed at episode reset (seeded)
runway_months -= _advance_runway() # depends on net cash flow
trust[role] ±= 0.08 per NPC # based on alignment with winning_decision
profitability_score = compute_profitability_score(next_state)
```
### Runway decrement
```python
monthly_revenue = revenue / 12.0
net = monthly_revenue - burn_rate
if net >= 0:
runway_months -= 0.5 # profitable: slow burn
else:
burn_months = min(2.0, max(1.0, abs(net) / burn_rate + 1.0))
runway_months -= burn_months # unprofitable: faster bleed
```
### Three layers of variability (no trajectory memorisation)
1. **Event order shuffled per episode** — same 10 events, different sequence per seed.
2. **Consequence magnitudes ±15% Gaussian noise** — sampled at `reset()`, fixed for the episode.
3. **NPC agendas ±25% sign-preserving jitter**`_jitter_agendas(seed)` perturbs base NPC priorities each episode.
---
## 4. Vote resolution
### Vote weights
```
CEO: 2.5 CTO: 1.2 CFO: 1.0 Investor Rep: 1.3 Independent: 0.8
```
CEO weight 2.5 ensures a decisive CEO call usually wins — the agent's actions visibly move outcomes round-to-round. NPCs still matter via persuasion shifts and trust dynamics.
### NPC option scoring (per NPC, per round)
```
for each option opt:
score[opt] = Σ over (metric, weight) in NPC_agenda:
consequences[opt][metric] × weight (with unit normalisation)
score[opt] += N(0, 0.20) # personality noise
NPC votes for argmax(score)
margin = top_two_score_difference
confidence = clamp(0.5 + 0.5 × margin + (trust − 0.5)×0.30, 0.05, 1.0)
```
Unit normalisation in scoring: `revenue /= 1e6`, `burn_rate /= 1e5`, `runway_months /= 6`. `revenue_mult` consequences are scored against the current `revenue` × the agenda weight on `revenue`.
### Pitch persuasion — semantic similarity, not keyword matching
```
ps_role = pitch_score(pitch, role) ∈ [0, 1]
# Persuasion redirects up to 55% of the NPC's vote weight to CEO's pick:
shift_frac = 0.55 × ps_role
tally[NPC_vote] += base_weight × (1 − shift_frac)
tally[CEO_decision] += base_weight × shift_frac
```
Where `base_weight = ROLE_WEIGHT[role] × confidence × clamp(trust[role] × 2, 0.5, 1.5)`.
The pitch scorer (`_PitchScorer` in `board_sim_env_environment.py`) has two backends:
1. **Sentence-transformer (primary)**: `all-MiniLM-L6-v2`, normalised cosine. `score = clamp((cosine + 0.05) × 1.2, 0, 1)`. Genuine sentence embeddings — semantically aligned arguments score high even with no shared tokens.
2. **TF-IDF fallback**: `(1,2)`-grams, English stop-words removed, IDF-weighted bag-of-bigrams cosine vs the role's manifesto. `score = clamp(cosine × 1.4, 0, 1)`. Token-based but properly stop-worded and IDF-weighted — already much more robust than a literal keyword count.
Set `BOARDSIM_PITCH_BACKEND=tfidf` to force the fallback (e.g. for CI without the embedding model).
### NPC manifestos (the hidden objective the CEO must infer)
| Role | Manifesto (paraphrased) |
|---|---|
| CTO | Operational excellence, engineering quality, team morale, technical risk reduction. |
| CFO | Capital discipline, runway, balance-sheet protection, regulatory caution. |
| Investor Rep | Growth, market share, ambitious returns, decisive bold bets. |
| Independent | Long-term reputation, governance, stakeholder trust, ethical responsibility. |
The full text lives in `NPC_MANIFESTOS` in the environment file.
### Tie-breaking
If two options tie in the tally, the CEO's pick wins (implementation: insert `agent_decision` first into the ordered tally before `max()`).
---
## 5. Reward formula
Applied at the end of each `step()` call:
```
# Primary signal — normalised profitability delta
reward = (new_score − old_score) / 100.0
# Coalition bonus / penalty (magnitudes raised so CEO impact is visible)
reward += 1.0 if winning_decision == agent_decision else −0.4
# Trust delta term
reward += 0.5 × (Σ trust_after − Σ trust_before)
# Pitch bootstrap + semantic persuasion
if pitch is non-empty:
reward += 0.05
if any NPC opposed the CEO's pick:
reward += 0.6 × mean(pitch_score over opposing NPCs)
# Format penalty
if action.decision not in current_round.options:
reward −= 0.5
# Terminal
if runway_months <= 0:
reward −= 2.0 # bankruptcy
if terminal:
reward += event._terminal_bonus # acquisition +30, IPO +25, stay-private +5, etc.
reward += {+10 if final ≥ 60, +5 if ≥ 40, −5 if < 20}
```
| Term | Purpose |
|---|---|
| Δ score / 100 | Primary learning signal: profitability improvement per decision |
| Coalition ±1.0 / −0.4 | Teaches the agent to actually win votes, not pick "good-looking" options |
| Trust × 0.5 | Rewards long-arc coalition building across rounds |
| Pitch bootstrap +0.05 | Ensures the pitch channel is exercised before the model is good enough to earn semantic bonuses |
| Pitch persuasion × 0.6 | Rewards pitches semantically aligned with opposing NPC manifestos (ToM signal) |
| Invalid −0.5 | Format-compliance signal (DECISION: / PITCH: two-line structure) |
| Bankruptcy −2.0 | Episode-ending failure signal |
| Terminal tiered | Long-horizon incentive toward high profitability, acquisition, or IPO |
---
## 6. Step ordering
```
1. old_score = compute_profitability_score(state) # snapshot BEFORE
2. NPC votes computed from current state + trust
3. CEO decision + pitch → _resolve_vote() → winning_decision
4. consequences[winning_decision] × noise → applied to state
5. _advance_runway()
6. trust updated per NPC (±0.08)
7. new_score = compute_profitability_score(state) # AFTER consequences
8. reward = (new_score − old_score)/100 + coalition + trust + pitch + ...
9. next observation returned with new_score in obs.state
```
The CEO **never consults profitability to make its decision** — it sees the previous round's score in the observation, emits a decision, and then the score updates. Profitability is the *outcome metric*, not a planning input.
---
## 7. Training pipeline
### Per-round gradient flow
The training loop samples one completion per round, per group member. Every one of the 10 decisions in a trajectory contributes gradient signal — not just the opening decision.
```
For each training step:
Create GROUP_SIZE independent envs (different seeds)
For each round r in 0..9:
For each group member g:
prompt = build_prompt(obs_g)
completion = model.generate(prompt, do_sample=True) # gradient-connected
obs_g = env_g.step(parse(completion))
ep_reward[g] += obs_g.reward
advantages = GRPO(ep_rewards) # group-relative normalisation
For each (g, r) completion:
loss = advantage[g] × NLL(completion) / (GROUP_SIZE × n_rounds)
+ β_KL × KL(π_θ || π_ref)
optimizer.step()
```
### KL penalty
A frozen reference model computes reference log-probs. KL ≈ `current_loss − ref_loss` per completion, clamped at 0. Coefficient β = 0.04. Prevents drift into degenerate text patterns (always emitting the same decision, empty pitches).
### Reward normalisation
Three normalisations in the reward function so terms are commensurate:
1. **Δ score ÷ 100** — brings profitability delta into the same scale as the coalition term.
2. **Bankruptcy penalty −2** (was −5) — one bad arc no longer drowns 9 rounds of positive signal.
3. **Pitch bootstrap +0.05** — kickstarts the pitch channel before the model is good enough to earn semantic bonuses.
---
## 8. The baseline — same Qwen3-0.6B with LoRA disabled
Earlier revisions compared the trained policy against a uniform-random policy. A coin flip is not a meaningful opponent for a 4 B language model picking among 3 well-formed strings — it can only highlight that the LM ≠ noise, which is not the relevant question.
The current baseline runs **the same Qwen3-0.6B**, on the **same paired seeds**, with the LoRA adapter context-managed off. Implementation (see `Training.py` / `notebooks/train_grpo_v2.ipynb`):
```python
# Fine-tuned (LoRA active)
trained_finals = run_episodes(model, seeds=HELDOUT)
# Same model, LoRA disabled — apples-to-apples base reference.
with model.disable_adapter():
base_finals = run_episodes(model, seeds=HELDOUT)
```
Statistical comparison on the per-seed paired delta `trained − base`:
- Paired t-test
- Wilcoxon signed-rank
- Cohen's d
- Bootstrap 95% CI on the mean delta
- Win-rate (fraction of seeds where trained > base)
---
## 9. Theory-of-Mind — what's actually measured
ToM in this environment has a specific, narrow meaning: **can the agent infer what each NPC privately values**, given only their statements and prior votes?
It is graded two ways:
1. **Pitch persuasion score**: `cosine(SBERT(pitch), SBERT(role_manifesto))`. A pitch that genuinely articulates the role's priorities scores above ~0.4; a pitch that is merely topically adjacent scores ~0.1; off-topic pitches score ~0.0. This replaces the earlier keyword-overlap metric, which the agent could trivially game.
2. **ToM probe**: ask the model to name the SINGLE board member most likely to *oppose* its chosen decision. Random baseline = 25% (1 of 4). The probe is run for both the fine-tuned policy and the disable-adapter base — the delta isolates what fine-tuning taught the model about its boardroom.
Trust trajectory across 10 rounds is a secondary diagnostic: rising trust for 3+ NPCs indicates the agent is consistently picking decisions aligned with their private preferences, which requires implicit modelling of those preferences.