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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ WORKDIR /app
# front-end (examples/web_app.py) could not give them.
COPY requirements.txt ./
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& pip install --no-cache-dir "tak @ git+https://github.com/Stephenson-Software/tak@1bac386777d4b2468faeda00b31e960af8d57164" \
&& pip install --no-cache-dir "tak @ git+https://github.com/Stephenson-Software/tak@9b539d8760e2c02cc262a7dcff6d0c541572ab71" \
&& apt-get purge -y git && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*

COPY src/ ./src/
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
# tak is the text-adventure kit extracted from this game
# (https://github.com/Stephenson-Software/tak), which FishE now runs on.
# Installing it needs git on the PATH.
tak @ git+https://github.com/Stephenson-Software/tak@1bac386777d4b2468faeda00b31e960af8d57164
tak @ git+https://github.com/Stephenson-Software/tak@9b539d8760e2c02cc262a7dcff6d0c541572ab71
jsonschema>=4.0,<5.0
36 changes: 22 additions & 14 deletions src/saveFileManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,36 @@ def __init__(self, data_directory="data"):
)

def migrate_old_save_files(self):
"""Migrate old save files (data/*.json) to slot_1 if they exist"""
"""Move a pre-slot save (data/*.json) into a slot, if there is one.

The save goes into slot_1 when slot_1 is free, otherwise into the next
free slot: an existing slot is never written over. (Before cloud saves
that could only happen with a hand-copied file; now a save set brought
in from another device can hold both layouts, and the one already in
a slot must survive.) With every slot taken, nothing is moved and the
old files stay where they are."""
old_player = os.path.join(self.data_directory, "player.json")
old_stats = os.path.join(self.data_directory, "stats.json")
old_time = os.path.join(self.data_directory, "timeService.json")
old_files = ["player.json", "stats.json", "timeService.json"]

# Check if old save files exist
if not os.path.exists(old_player):
return False

# Create slot_1 directory
slot_1_path = os.path.join(self.data_directory, "slot_1")
if not os.path.exists(slot_1_path):
os.makedirs(slot_1_path, exist_ok=True)
# The kit's rule: the lowest slot whose directory holds no file.
slot_number = self.get_next_available_slot()
if slot_number is None:
return False
slot_path = os.path.join(self.data_directory, "slot_%d" % slot_number)
os.makedirs(slot_path, exist_ok=True)

# Move files to slot_1
# Move files into the slot. The slot held no file, so nothing in it is
# replaced; a name that is somehow there already is left alone.
try:
if os.path.exists(old_player):
shutil.move(old_player, os.path.join(slot_1_path, "player.json"))
if os.path.exists(old_stats):
shutil.move(old_stats, os.path.join(slot_1_path, "stats.json"))
if os.path.exists(old_time):
shutil.move(old_time, os.path.join(slot_1_path, "timeService.json"))
for name in old_files:
source = os.path.join(self.data_directory, name)
target = os.path.join(slot_path, name)
if os.path.exists(source) and not os.path.exists(target):
shutil.move(source, target)
# The migration rewrote the save directory's layout; flush it so a
# browser-storage player doesn't re-migrate on every page load.
syncBrowserSaves()
Expand Down
62 changes: 57 additions & 5 deletions tests/test_saveFileManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,8 @@ def test_migrate_old_save_files_partial():


def test_migrate_old_save_files_slot1_exists():
"""Test migration when slot_1 already exists"""
"""A pre-slot save never replaces a save already in slot_1: it goes into
the next free slot, and slot_1 is left exactly as it was."""
temp_dir = tempfile.mkdtemp()
try:
manager = SaveFileManager(temp_dir)
Expand All @@ -728,15 +729,66 @@ def test_migrate_old_save_files_slot1_exists():
# Create old format files
with open(os.path.join(temp_dir, "player.json"), "w") as f:
json.dump({"money": 100}, f)
with open(os.path.join(temp_dir, "stats.json"), "w") as f:
json.dump({"totalFishCaught": 3}, f)

# Migration should still succeed (will overwrite)
result = manager.migrate_old_save_files()
assert result is True

# Check that old file was moved and overwrote existing
with open(os.path.join(slot_1_path, "player.json"), "r") as f:
data = json.load(f)
assert data["money"] == 100 # Should be the migrated value
assert json.load(f)["money"] == 999 # untouched
assert not os.path.exists(os.path.join(slot_1_path, "stats.json"))
slot_2_path = os.path.join(temp_dir, "slot_2")
with open(os.path.join(slot_2_path, "player.json"), "r") as f:
assert json.load(f)["money"] == 100
with open(os.path.join(slot_2_path, "stats.json"), "r") as f:
assert json.load(f)["totalFishCaught"] == 3
assert not os.path.exists(os.path.join(temp_dir, "player.json"))
assert not os.path.exists(os.path.join(temp_dir, "stats.json"))
finally:
shutil.rmtree(temp_dir)


def test_migrate_old_save_files_skips_a_slot_holding_any_file():
"""A slot whose player.json is gone but whose other files survive is not
free either (the kit's rule): the old save goes past it."""
temp_dir = tempfile.mkdtemp()
try:
manager = SaveFileManager(temp_dir)
os.makedirs(os.path.join(temp_dir, "slot_1"))
with open(os.path.join(temp_dir, "slot_1", "stats.json"), "w") as f:
f.write('{"totalFishCaught": 7}')
with open(os.path.join(temp_dir, "player.json"), "w") as f:
json.dump({"money": 5}, f)

assert manager.migrate_old_save_files() is True

assert not os.path.exists(os.path.join(temp_dir, "slot_1", "player.json"))
with open(os.path.join(temp_dir, "slot_1", "stats.json")) as f:
assert f.read() == '{"totalFishCaught": 7}'
assert os.path.exists(os.path.join(temp_dir, "slot_2", "player.json"))
finally:
shutil.rmtree(temp_dir)


def test_migrate_old_save_files_every_slot_taken_moves_nothing():
temp_dir = tempfile.mkdtemp()
try:
manager = SaveFileManager(temp_dir)
for n in range(1, 100):
os.makedirs(os.path.join(temp_dir, "slot_%d" % n))
with open(os.path.join(temp_dir, "slot_%d" % n, "player.json"), "w") as f:
json.dump({"money": n}, f)
with open(os.path.join(temp_dir, "player.json"), "w") as f:
json.dump({"money": 100}, f)

with patch("src.saveFileManager.syncBrowserSaves") as sync:
assert manager.migrate_old_save_files() is False
sync.assert_not_called()

assert os.path.exists(os.path.join(temp_dir, "player.json"))
with open(os.path.join(temp_dir, "slot_1", "player.json")) as f:
assert json.load(f)["money"] == 1
finally:
shutil.rmtree(temp_dir)

Expand Down
2 changes: 2 additions & 0 deletions tests/web/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def test_the_page_keeps_the_saves_database_name():
html = page.read()
assert 'idbName: "fishe-saves"' in html
assert 'saveDirEnv: "FISHE_SAVE_DIR"' in html
# Cloud saves (RFC 0016), only on arcade and only once a player turns them on.
assert "cloudSaves: true," in html
assert 'entry: "web/pyodide_main.py"' in html


Expand Down
7 changes: 6 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<h2>FishE <span class="tagline">— fish a seaside village and build a fortune of $10,000</span></h2>
<div id="status" class="status">Starting&hellip;</div>
<div id="app"></div>
<p class="controls">Tip: click an option or press its number key (1-9). Enter or Space continues. Your saved games live in this browser.</p>
<p class="controls">Tip: click an option or press its number key (1-9). Enter or Space continues. Your saved games live in this browser; at fishe.play.danielstephenson.dev you can also back them up to your account once signed in.</p>
<footer class="credit">More by Daniel Stephenson &rarr; <a href="https://danielstephenson.dev">danielstephenson.dev</a></footer>
<script src="/tak/client.js"></script>
<script src="/tak/boot.js"></script>
Expand All @@ -33,6 +33,11 @@ <h2>FishE <span class="tagline">— fish a seaside village and build a fortune o
bundleUrl: "/web/game.zip",
entry: "web/pyodide_main.py",
logPrefix: "[fishe]",
// Cloud saves on arcade (RFC 0016): a signed-in player can back these
// saves up and get them on their other devices. Off until they turn it on,
// and only on fishe.play.danielstephenson.dev: the old alias host never
// loads cloud.js (tak's onArcadeHost), so its saves stay in its browser.
cloudSaves: true,
messages: { download: "Downloading the village…", start: "Casting off…" },
});
</script>
Expand Down
Loading