File size: 19,047 Bytes
5ede5f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4666d9
 
 
 
 
5ede5f5
 
 
 
 
 
 
e4666d9
 
 
 
 
5ede5f5
 
f72fde9
 
5ede5f5
 
 
 
f72fde9
5ede5f5
 
 
 
 
e4666d9
 
 
 
 
 
5ede5f5
 
f72fde9
 
 
 
 
 
 
 
 
 
e4666d9
 
 
f72fde9
 
 
 
 
 
 
 
 
 
 
3c7b068
 
5ede5f5
 
773fb23
 
 
 
5ede5f5
 
 
 
f72fde9
5ede5f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773fb23
 
 
 
5ede5f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773fb23
5ede5f5
 
 
 
 
773fb23
 
 
 
 
5ede5f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4666d9
 
 
5ede5f5
 
 
 
 
 
 
 
 
e4666d9
 
 
5ede5f5
 
 
 
 
 
 
 
 
 
e4666d9
 
 
 
 
 
 
 
5ede5f5
 
 
 
 
 
 
 
 
f72fde9
 
5ede5f5
 
 
 
 
3c7b068
 
 
 
e4666d9
 
 
3c7b068
 
 
 
 
5ede5f5
 
 
f72fde9
5ede5f5
 
 
 
 
 
 
 
 
 
 
e4666d9
 
 
5ede5f5
 
 
 
 
 
 
 
 
 
f72fde9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ede5f5
 
f72fde9
5ede5f5
 
 
 
 
 
 
f72fde9
 
 
5ede5f5
 
f72fde9
 
5ede5f5
 
f72fde9
5ede5f5
 
f72fde9
 
5ede5f5
f72fde9
5ede5f5
f72fde9
 
 
 
 
5ede5f5
 
f72fde9
 
3c7b068
 
f72fde9
3c7b068
 
 
f72fde9
3c7b068
f72fde9
 
3c7b068
 
 
 
 
f72fde9
773fb23
f72fde9
773fb23
f72fde9
 
3c7b068
f72fde9
3c7b068
 
 
 
 
5ede5f5
 
f72fde9
5ede5f5
 
 
 
 
 
f72fde9
 
 
5ede5f5
 
f72fde9
3c7b068
e4666d9
 
 
 
f72fde9
 
3c7b068
 
5ede5f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4666d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbc4b4d
e4666d9
 
 
 
 
bbc4b4d
 
 
e4666d9
 
 
 
 
 
 
 
bbc4b4d
e4666d9
 
 
 
 
bbc4b4d
 
 
e4666d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbc4b4d
e4666d9
 
bbc4b4d
 
 
 
e4666d9
 
 
 
 
 
bbc4b4d
e4666d9
 
 
 
 
bbc4b4d
 
 
 
 
 
 
 
e4666d9
 
5ede5f5
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
from dataclasses import replace

import pytest

from core.config import GameConfig
from core.game import (
    PROPOSAL_STATUS_APPROVED,
    PROPOSAL_STATUS_REJECTED,
    PROPOSAL_STATUS_REJECTED_INVALID,
    NationGame,
)
from schemas.actions import ActionType, VoteChoice
from schemas.phases import Phase


DEPARTMENTS = ("Social", "Agriculture", "Health", "Education", "Defense", "Commerce")


def make_game(seed: int = 7, **config_overrides) -> NationGame:
    config = (
        replace(GameConfig.from_json(), **config_overrides)
        if config_overrides
        else GameConfig.from_json()
    )
    game = NationGame(config=config, seed=seed)
    game.event_engine._distribution = {"no_event": 1.0}
    game.reset()
    return game


def debate(agent_id: str = "Health") -> dict:
    return {
        "type": ActionType.DEBATE,
        "agent_id": agent_id,
        "message": "Public priority signal",
    }


def proposal(department: str, amount: float = 10.0) -> dict:
    """Discretionary request (≥ 0) above the auto-funded critical floor."""
    return {
        "type": ActionType.PROPOSE_BUDGET,
        "agent_id": department,
        "department": department,
        "amount": amount,
        "justification": "Fund expected demand",
    }


def vote(agent_id: str, proposal_id: str, choice: VoteChoice = VoteChoice.YES) -> dict:
    return {
        "type": ActionType.VOTE,
        "agent_id": agent_id,
        "proposal_id": proposal_id,
        "vote": choice,
    }


def every_eligible_vote(game: NationGame) -> list[dict]:
    """One YES vote from each non-proposer, for every pending proposal (full parliament)."""
    return [
        vote(department, p.proposal_id, VoteChoice.YES)
        for p in game.proposals
        for department in DEPARTMENTS
        if department != p.department
    ]


def every_eligible_no_on_target_else_yes(
    game: NationGame, target_department: str
) -> list[dict]:
    """NO on ``target_department``'s proposal; YES on all other proposals (from each eligible voter)."""
    return [
        vote(
            department,
            p.proposal_id,
            VoteChoice.NO if p.department == target_department else VoteChoice.YES,
        )
        for p in game.proposals
        for department in DEPARTMENTS
        if department != p.department
    ]


def advance_to_phase(game: NationGame, phase: Phase) -> None:
    while game.phase < phase and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()


def submit_safe_proposals(game: NationGame) -> None:
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal(department, amount=10.0) for department in DEPARTMENTS])


def approve_all_pending_proposals(game: NationGame) -> None:
    advance_to_phase(game, Phase.VOTING)
    actions = []
    for item in game.proposals:
        for department in DEPARTMENTS:
            if department != item.department:
                actions.append(vote(department, item.proposal_id))
    game.step(actions)


def run_phase_loop_round(game: NationGame) -> None:
    game.step()
    submit_safe_proposals(game)
    approve_all_pending_proposals(game)
    while game.phase != Phase.EVENT_REVELATION and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()


def test_reset_creates_round_1_phase_1_state() -> None:
    game = make_game()

    state = game.state()

    assert state["round"] == 1
    assert state["phase"] == Phase.EVENT_REVELATION
    assert state["treasury"] == 1000


def test_phase_transitions_follow_1_through_9() -> None:
    game = make_game()
    seen = [game.phase]

    game.step()
    seen.append(game.phase)
    game.force_advance_phase()
    seen.append(game.phase)
    game.step([proposal(department) for department in DEPARTMENTS])
    seen.append(game.phase)
    approve_all_pending_proposals(game)
    seen.append(game.phase)
    while game.phase != Phase.EVENT_REVELATION and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()
        seen.append(game.phase)

    assert seen[:9] == [Phase(index) for index in range(1, 10)]
    assert game.round == 2


def test_debate_action_only_accepted_in_phase_2() -> None:
    game = make_game()

    phase_1_result = game.step(debate())
    phase_2_result = game.step(debate())

    assert phase_1_result.info["ignored_actions"][0]["reason"] == "wrong_phase"
    assert phase_2_result.info["accepted_actions"][0]["type"] == ActionType.DEBATE
    assert len(game.debate_messages) == 1


def test_proposal_action_only_accepted_in_phase_3() -> None:
    game = make_game()

    wrong_phase_result = game.step(proposal("Health"))
    advance_to_phase(game, Phase.PROPOSAL)
    accepted_result = game.step(proposal("Health"))

    assert wrong_phase_result.info["ignored_actions"][0]["reason"] == "wrong_phase"
    assert (
        accepted_result.info["accepted_actions"][0]["type"] == ActionType.PROPOSE_BUDGET
    )


def test_vote_action_only_accepted_in_phase_4() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    wrong_phase_result = game.step(vote("Health", "missing"))

    game = make_game()
    submit_safe_proposals(game)
    proposal_id = next(
        item.proposal_id for item in game.proposals if item.department != "Health"
    )

    accepted_result = game.step(vote("Health", proposal_id))

    assert wrong_phase_result.info["ignored_actions"][0]["reason"] == "wrong_phase"
    assert accepted_result.info["accepted_actions"][0]["type"] == ActionType.VOTE


def test_wrong_phase_action_is_ignored() -> None:
    game = make_game()

    result = game.step(
        {
            "type": ActionType.VOTE,
            "agent_id": "Health",
            "proposal_id": "missing",
            "vote": VoteChoice.YES,
        }
    )

    assert result.info["ignored_actions"][0]["reason"] == "wrong_phase"
    assert game.votes == []


def test_invalid_proposal_over_treasury_is_rejected() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)

    rem = max(0.0, game.treasury.balance - game._total_critical_funding())
    result = game.step(proposal("Health", amount=rem + 1.0))

    assert result.info["rejected_actions"][0]["reason"] == "exceeds_treasury"
    assert game.proposals[0].status == PROPOSAL_STATUS_REJECTED_INVALID


def test_duplicate_proposal_is_ignored() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)

    result = game.step(
        [proposal("Health", amount=20.0), proposal("Health", amount=30.0)]
    )

    assert len(game.proposals) == 1
    assert result.info["ignored_actions"][0]["reason"] == "duplicate_proposal"


def test_agents_cannot_propose_for_another_department() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    action = proposal("Health", amount=20.0)
    action["agent_id"] = "Defense"

    result = game.step(action)

    assert result.info["rejected_actions"][0]["reason"] == "wrong_department"
    assert game.proposals[0].status == PROPOSAL_STATUS_REJECTED_INVALID


def test_self_vote_is_rejected() -> None:
    game = make_game()
    submit_safe_proposals(game)
    health_proposal = next(
        item for item in game.proposals if item.department == "Health"
    )

    result = game.step(vote("Health", health_proposal.proposal_id))

    assert result.info["rejected_actions"][0]["reason"] == "self_vote"
    assert health_proposal.votes == {}


def test_tie_vote_rejects_proposal() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    game.step(proposal("Health", amount=90.0))
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS if d != "Health"])
    health_proposal = next(p for p in game.proposals if p.department == "Health")
    custom = {
        "Defense": VoteChoice.YES,
        "Commerce": VoteChoice.NO,
        "Social": VoteChoice.ABSTAIN,
        "Agriculture": VoteChoice.YES,
        "Education": VoteChoice.NO,
    }
    actions: list[dict] = []
    for p in game.proposals:
        for d in DEPARTMENTS:
            if d == p.department:
                continue
            if p.proposal_id == health_proposal.proposal_id:
                actions.append(vote(d, p.proposal_id, custom.get(d, VoteChoice.YES)))
            else:
                actions.append(vote(d, p.proposal_id, VoteChoice.YES))
    game.step(actions)
    game.step()
    assert health_proposal.status == PROPOSAL_STATUS_REJECTED
    assert not game.done


def test_approved_proposal_debits_treasury() -> None:
    game = make_game()
    submit_safe_proposals(game)
    approve_all_pending_proposals(game)

    before = game.treasury.balance
    total_c = game._total_critical_funding()
    approved = sum(p.amount for p in game.proposals)
    game.step()

    assert all(p.status == PROPOSAL_STATUS_APPROVED for p in game.proposals)
    assert game.treasury.balance == pytest.approx(before - total_c - approved)


def test_rejected_proposal_keeps_only_critical() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    game.step(proposal("Health", amount=90.0))
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS if d != "Health"])
    health_proposal = game.proposals[0]
    game.step(every_eligible_no_on_target_else_yes(game, "Health"))

    while game.phase != Phase.CONSUMPTION_AND_EVENT_IMPACT and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()

    assert health_proposal.status == PROPOSAL_STATUS_REJECTED
    crit = float(game.sectors["Health"].critical)
    assert game.sectors["Health"].allocation == pytest.approx(crit)


def test_zero_discretionary_proposal_is_accepted() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)

    result = game.step(proposal("Health", amount=0.0))

    assert result.info["accepted_actions"][0]["type"] == ActionType.PROPOSE_BUDGET
    assert any(p.department == "Health" and p.amount == 0.0 for p in game.proposals)


def test_sequential_treasury_depletion_rejects_later_proposal() -> None:
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal("Social", amount=400.0), proposal("Health", amount=500.0)])
    game.step(
        [proposal(d, amount=0.0) for d in DEPARTMENTS if d not in {"Social", "Health"}]
    )
    social_proposal = next(p for p in game.proposals if p.department == "Social")
    health_proposal = next(p for p in game.proposals if p.department == "Health")

    game.step(every_eligible_vote(game))
    game.step()

    assert social_proposal.status == PROPOSAL_STATUS_APPROVED
    assert health_proposal.status == PROPOSAL_STATUS_REJECTED
    assert health_proposal.rejection_reason == "exceeds_remaining_treasury"


def test_dictator_direct_runs_economy_round_without_immediate_termination() -> None:
    game = make_game()
    allocations = dict(game.config.SECTOR_BASELINES)
    allocations["Defense"] = 30

    result = game.step(allocations)

    assert not result.done
    assert result.termination_reason != "CRITICAL_FAILURE"
    assert result.total_revenue > 0.0


def test_revenue_succeeds_after_direct_allocation() -> None:
    game = make_game()
    safe = {
        department: baseline * 1.3
        for department, baseline in game.config.SECTOR_BASELINES.items()
    }
    first = game.step(safe)
    assert first.total_revenue > 0


def test_bankruptcy_terminates_episode() -> None:
    game = make_game()
    game.treasury.balance = 1
    critical_allocations = {
        department: baseline * game.config.CRITICAL_RATIO
        for department, baseline in game.config.SECTOR_BASELINES.items()
    }

    result = game.step(critical_allocations)

    assert result.done
    assert result.termination_reason == "BANKRUPTCY"


def test_max_rounds_terminates_episode() -> None:
    game = make_game(MAX_ROUNDS=1)

    run_phase_loop_round(game)

    assert game.done
    assert game.termination_reason == "MAX_ROUNDS"


def test_auto_critical_is_funded_before_voted_proposals() -> None:
    """Auto-critical is applied BEFORE approved discretionary proposals are added."""
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal(department, amount=20.0) for department in DEPARTMENTS])
    approve_all_pending_proposals(game)
    game.step()  # Phase 5: Budget Execution

    total_c = game._total_critical_funding()
    # Every department gets critical funded automatically
    for dept in DEPARTMENTS:
        sector = game.sectors[dept]
        assert sector.allocation >= float(sector.critical(game.population.value)), (
            f"{dept} allocation {sector.allocation} < critical {sector.critical(game.population.value)}"
        )

    # Treasury was debited for total critical first
    # Then debited for approved discretionary on top
    approved_disc = sum(
        p.amount for p in game.proposals if p.status == PROPOSAL_STATUS_APPROVED
    )
    expected_allocation = total_c + approved_disc
    assert game.last_total_allocation == pytest.approx(expected_allocation)


def test_discretionary_zero_proposal_is_valid() -> None:
    """A proposal of amount=0 means 'I only need auto-critical, no extra'."""
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    # All departments propose 0 discretionary
    game.step([proposal(department, amount=0.0) for department in DEPARTMENTS])
    approve_all_pending_proposals(game)
    game.step()  # Phase 5

    # All proposals accepted (0 is valid)
    assert all(p.status == PROPOSAL_STATUS_APPROVED for p in game.proposals)
    # Every department still gets auto-critical
    total_c = game._total_critical_funding()
    assert game.last_total_allocation == pytest.approx(total_c)


def test_rejected_proposal_allocates_critical_not_zero() -> None:
    """Rejection means department gets ONLY auto-critical (not 0)."""
    game = make_game()
    advance_to_phase(game, Phase.PROPOSAL)
    # Health proposes 90, everyone else 0
    game.step(proposal("Health", amount=90.0))
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS if d != "Health"])
    # NO vote on Health's proposal
    health_proposal = next(p for p in game.proposals if p.department == "Health")
    game.step(every_eligible_no_on_target_else_yes(game, "Health"))

    while game.phase != Phase.CONSUMPTION_AND_EVENT_IMPACT and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()

    assert health_proposal.status == PROPOSAL_STATUS_REJECTED
    crit = float(game.sectors["Health"].critical(game.population.value))
    # Health gets auto-critical (not 0) even though proposal was rejected
    assert game.sectors["Health"].allocation == pytest.approx(crit)


def test_shutdown_triggered_by_zero_discretionary_two_rounds() -> None:
    """Shutdown = sum(Discretionary_d) = 0 for 2 consecutive rounds."""
    game = make_game()
    run_phase_loop_round(game)  # Round 1: all propose and approve

    # Round 2: all propose 0 discretionary (all get auto-critical only)
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS])
    approve_all_pending_proposals(game)
<<<<<<< Updated upstream
    while game.phase != Phase.EVENT_REVELATION and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()
=======
    game.step()  # Phase 5: first round with zero discretionary
>>>>>>> Stashed changes

    assert game.shutdown_counter == 1
    assert not game.done

    # Round 3: again all propose 0
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS])
    approve_all_pending_proposals(game)
<<<<<<< Updated upstream
    while game.phase != Phase.EVENT_REVELATION and not game.done:
        if game.phase == Phase.DEBATE:
            game.force_advance_phase()
        else:
            game.step()
=======
    game.step()  # Phase 5: second consecutive zero discretionary
>>>>>>> Stashed changes

    assert game.shutdown_counter == 2
    assert game.done
    assert game.termination_reason == "SHUTDOWN"


def test_bankruptcy_when_treasury_less_than_total_critical() -> None:
    """If treasury < sum(Critical_d) at budget execution, BANKRUPTCY triggers."""
    game = make_game()
    # Set treasury to be less than total critical
    game.treasury.balance = 50.0  # Total critical is ~190
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS])
    approve_all_pending_proposals(game)

    result = game.step()

    assert result.done
    assert result.termination_reason == "BANKRUPTCY"


def test_no_critical_failure_after_revenue_round() -> None:
    """Auto-critical ensures critical failure is impossible. Treasury checks apply instead."""
    game = make_game()
    # Simulate under-funding scenario: treasury too low for all critical
    game.treasury.balance = 100.0  # Below ~190 total critical
    advance_to_phase(game, Phase.PROPOSAL)
    game.step([proposal(d, amount=0.0) for d in DEPARTMENTS])
    approve_all_pending_proposals(game)

    result = game.step()

    # Should be BANKRUPTCY (treasury < critical), NOT CRITICAL_FAILURE
    assert result.done
    assert result.termination_reason == "BANKRUPTCY"


def test_direct_allocation_applies_auto_critical_first() -> None:
    """Direct allocation shortcut auto-funds critical, then applies input as discretionary."""
    game = make_game()
<<<<<<< Updated upstream
    # Input mapping is legacy total desired allocation per department; engine splits
    # into critical + discretionary after auto-funding critical.
=======
    # Direct allocation input: {Dept: total_requested}
    # Should be interpreted as: {Dept: discretionary_above_critical}
>>>>>>> Stashed changes
    safe = {
        dept: baseline * 1.3 for dept, baseline in game.config.SECTOR_BASELINES.items()
    }
    result = game.step(safe)

    assert not result.done
<<<<<<< Updated upstream
    pop = int(result.observation["population"])
    for dept in DEPARTMENTS:
        alloc = result.observation["sectors"][dept]["allocation"]
        crit = game.sectors[dept].critical(pop)
        assert alloc >= float(crit)
=======
    # Verify auto-critical was applied
    total_c = game._total_critical_funding()
    # Verify each department got at least critical
    for dept in DEPARTMENTS:
        sector = game.sectors[dept]
        assert sector.allocation >= float(sector.critical(game.population.value))
>>>>>>> Stashed changes


def test_seeded_reset_is_deterministic() -> None:
    first = NationGame(seed=123).reset(seed=123)
    second = NationGame(seed=123).reset(seed=123)

    assert first["event_ledger"] == second["event_ledger"]
    assert first["sectors"] == second["sectors"]