Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions scripts/game_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ var _backup_counter := 0

func _ready() -> void:
save_supported = true
if OS.has_feature("web"):
use_local_storage = JavaScriptBridge.eval("typeof localStorage !== 'undefined'", true)
use_local_storage = OS.has_feature("web") and JavaScriptBridge.eval("typeof localStorage !== 'undefined'", true)

func save_game(data: Dictionary) -> void:
var payload := JSON.stringify(data)
Expand Down
2 changes: 1 addition & 1 deletion scripts/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2432,7 +2432,7 @@ func render_event_drawer() -> void:

func push_event(text: String) -> void:
state.events.push_front({"tick": tick, "text": text})
while state.events.size() > 8:
while state.events.size() > 20:
state.events.pop_back()
_mark_dirty()

Expand Down
6 changes: 6 additions & 0 deletions tests/test_dirty_state_tracking.gd
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func seed_tiles(state: Dictionary) -> void:
func flow_dirty_state_triggers_save() -> void:
print("\n=== Flow 1: Dirty state triggers save ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Simulate: _mark_dirty() was called (state changed)
Expand All @@ -104,6 +105,7 @@ func flow_dirty_state_triggers_save() -> void:
func flow_clean_state_skips_save() -> void:
print("\n=== Flow 2: Clean state skips save (persist returns early) ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Initial save with dirty state
Expand Down Expand Up @@ -136,6 +138,7 @@ func flow_clean_state_skips_save() -> void:
func flow_persist_resets_dirty_flag() -> void:
print("\n=== Flow 3: persist() resets _dirty flag ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Initial save (dirty=true)
Expand Down Expand Up @@ -164,6 +167,7 @@ func flow_persist_resets_dirty_flag() -> void:
func flow_multiple_mutations_single_persist() -> void:
print("\n=== Flow 4: Multiple mutations → single persist ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Initial state
Expand Down Expand Up @@ -194,6 +198,7 @@ func flow_multiple_mutations_single_persist() -> void:
func flow_idle_tick_skips_persist() -> void:
print("\n=== Flow 5: Idle tick (no mutations) skips persist ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Save initial state
Expand All @@ -220,6 +225,7 @@ func flow_idle_tick_skips_persist() -> void:
func flow_dirty_flag_covers_all_mutation_categories() -> void:
print("\n=== Flow 6: Dirty flag covers all mutation categories ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Initial state with workers and builds
Expand Down
7 changes: 7 additions & 0 deletions tests/test_e2e.gd
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func _assert_array_has(arr: Array, val, msg: String) -> void:
func flow_boot_and_bootstrap() -> void:
print("\n=== Flow 1: Boot and bootstrap ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Simulate bootstrap by constructing initial state directly
Expand Down Expand Up @@ -157,6 +158,7 @@ func flow_boot_and_bootstrap() -> void:
func flow_save_and_reload() -> void:
print("\n=== Flow 2: Save and reload ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

var state := {
Expand Down Expand Up @@ -225,6 +227,7 @@ func flow_save_and_reload() -> void:
func flow_tick_simulation() -> void:
print("\n=== Flow 3: Tick simulation ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Initial state with resources available for gathering
Expand Down Expand Up @@ -287,6 +290,7 @@ func flow_tick_simulation() -> void:
func flow_build_placement() -> void:
print("\n=== Flow 4: Build placement, queue, and completion ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

var state := {
Expand Down Expand Up @@ -386,6 +390,7 @@ func flow_build_placement() -> void:
func flow_anchor_switching() -> void:
print("\n=== Flow 5: Anchor switching ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Test that state is preserved across anchor changes
Expand Down Expand Up @@ -444,6 +449,7 @@ func flow_anchor_switching() -> void:
func flow_priority_order() -> void:
print("\n=== Flow 6: Priority order changes ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

var state := {
Expand Down Expand Up @@ -504,6 +510,7 @@ func flow_priority_order() -> void:
func flow_save_compatibility() -> void:
print("\n=== Flow 7: Save compatibility ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Test 1: Old save version (version 0) — rejected as invalid
Expand Down
6 changes: 6 additions & 0 deletions tests/test_reservations.gd
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func _assert_no_key(d: Dictionary, key: String, name: String) -> void:
func test_reservation_prevents_double_haul(gs: Node) -> void:
print("")
print("--- reservation: prevents double-haul ---")
gs.use_local_storage = false

# Build needs 1 stone, stockpile has 1 stone
var state := {
Expand Down Expand Up @@ -128,6 +129,7 @@ func test_reservation_prevents_double_haul(gs: Node) -> void:
func test_reservation_clamps_delivery(gs: Node) -> void:
print("")
print("--- reservation: clamps delivery ---")
gs.use_local_storage = false

# Hut costs 2 stone. Build starts with 0 delivered, 0 reserved.
# Worker picks up 1 stone → reserves it (reserved=1).
Expand Down Expand Up @@ -219,6 +221,7 @@ func test_reservation_clamps_delivery(gs: Node) -> void:
func test_reservation_released_on_build_complete(gs: Node) -> void:
print("")
print("--- reservation: released on completion ---")
gs.use_local_storage = false

# Build is complete — reservations should be cleaned up by _clean_stale_reservations
var state := {
Expand Down Expand Up @@ -260,6 +263,7 @@ func test_reservation_released_on_build_complete(gs: Node) -> void:
func test_stale_reservations_cleaned_up(gs: Node) -> void:
print("")
print("--- reservation: stale cleanup ---")
gs.use_local_storage = false

# Build has 1 stone reserved but no worker is hauling to it (worker broke)
var state := {
Expand Down Expand Up @@ -317,6 +321,7 @@ func test_stale_reservations_cleaned_up(gs: Node) -> void:
func test_two_workers_one_need_only_one_succeeds(gs: Node) -> void:
print("")
print("--- reservation: two workers one need ---")
gs.use_local_storage = false

# Hut needs 2 stone. Stockpile has 1 stone.
# Build has 0 delivered, 0 reserved initially.
Expand Down Expand Up @@ -401,6 +406,7 @@ func test_two_workers_one_need_only_one_succeeds(gs: Node) -> void:
func test_reserve_field_added_to_new_builds(gs: Node) -> void:
print("")
print("--- reservation: reserved field on new builds ---")
gs.use_local_storage = false

# Simulate a newly queued build — should have reserved field
var new_build := {
Expand Down
21 changes: 11 additions & 10 deletions tests/test_runner.gd
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func test_persistence_roundtrip(gs: Node) -> void:
"events": [{"tick": 42, "text": "test event"}],
}

gs.use_local_storage = false
gs.save_game(payload)
var loaded = gs.load_game()
_assert_not_empty(loaded, "persistence_roundtrip: load returned data")
Expand Down Expand Up @@ -381,23 +382,23 @@ func test_bounded_event_log(gs: Node) -> void:
print("")
print("--- bounded event log ---")

# Simulate push_event bounded behavior: max 8 events, LIFO eviction
# Simulate push_event bounded behavior: max 20 events, LIFO eviction
var events := []
const MAX_EVENTS := 8
const MAX_EVENTS := 20

for i in range(12):
for i in range(25):
events.push_front({"tick": i, "text": "Event %d" % i})
while events.size() > MAX_EVENTS:
events.pop_back()

_assert_eq(events.size(), MAX_EVENTS, "bounded_event_log: capped at 8")
# First event should be the most recent (11), last should be oldest kept (4)
_assert_eq(int(events[0].get("tick", -1)), 11, "bounded_event_log: first is newest (11)")
_assert_eq(int(events[MAX_EVENTS - 1].get("tick", -1)), 4, "bounded_event_log: last is oldest kept (4)")
_assert_eq(events.size(), MAX_EVENTS, "bounded_event_log: capped at 20")
# First event should be the most recent (24), last should be oldest kept (5)
_assert_eq(int(events[0].get("tick", -1)), 24, "bounded_event_log: first is newest (24)")
_assert_eq(int(events[MAX_EVENTS - 1].get("tick", -1)), 5, "bounded_event_log: last is oldest kept (5)")

# Verify eviction count: 12 pushed - 8 kept = 4 evicted
var evicted_count := 12 - MAX_EVENTS
_assert_eq(evicted_count, 4, "bounded_event_log: 4 events evicted")
# Verify eviction count: 25 pushed - 20 kept = 5 evicted
var evicted_count := 25 - MAX_EVENTS
_assert_eq(evicted_count, 5, "bounded_event_log: 5 events evicted")

# Empty log stays empty
var empty_events := []
Expand Down
3 changes: 3 additions & 0 deletions tests/test_save_backup.gd
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func _assert_eq(actual, expected, msg: String) -> void:
func flow_backup_creates_file() -> void:
print("\n=== Flow 1: backup_save creates a timestamped file ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Save some state first
Expand Down Expand Up @@ -69,6 +70,7 @@ func flow_backup_creates_file() -> void:
func flow_restore_from_backup() -> void:
print("\n=== Flow 2: restore_backup restores from the latest backup ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Save original state
Expand Down Expand Up @@ -119,6 +121,7 @@ func flow_restore_from_backup() -> void:
func flow_list_backups_sorted() -> void:
print("\n=== Flow 3: list_backups returns newest-first sorted list ===")
var gs := load_game_state()
gs.use_local_storage = false
gs.clear_game()

# Create multiple backups
Expand Down
Loading