Skip to content
Draft
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
*.pyc
__pycache__/
data/*.json
data/
.coverage
cov.xml
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@
[![Run Unit Tests](https://github.com/Stephenson-Software/FishE/actions/workflows/test.yml/badge.svg)](https://github.com/Stephenson-Software/FishE/actions/workflows/test.yml)

This game allows you to explore a fishing village and perform actions in it.

## Features

### Multiple Save Files
FishE supports multiple save files, allowing you to maintain different game progressions simultaneously. When you start the game, you'll see a save file manager that displays:

- **Existing Saves**: View all your saved games with their progress (Day, Money, Fish count, Last Modified)
- **Create New Save**: Start a fresh game in a new save slot
- **Delete Save**: Remove unwanted save files
- **Quick Load**: Load any existing save file to continue your adventure

Each save file is stored in its own slot (slot_1, slot_2, etc.) in the `data/` directory, ensuring your saves never conflict with each other.
180 changes: 149 additions & 31 deletions src/fishE.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import json
from location import bank, docks, home, shop, tavern
from location.enum.locationType import LocationType
from player.player import Player
Expand All @@ -9,6 +10,7 @@
from world.timeService import TimeService
from stats.stats import Stats
from ui.userInterface import UserInterface
from saveFileManager import SaveFileManager


# @author Daniel McCoy Stephenson
Expand All @@ -19,27 +21,31 @@ def __init__(self):
self.playerJsonReaderWriter = PlayerJsonReaderWriter()
self.timeServiceJsonReaderWriter = TimeServiceJsonReaderWriter()
self.statsJsonReaderWriter = StatsJsonReaderWriter()
self.saveFileManager = SaveFileManager()

# Migrate old save files to new format if they exist
self.saveFileManager.migrate_old_save_files()

# Show save file selection menu
self._selectSaveFile()
Comment on lines +29 to +30
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change breaks backward compatibility with existing save files. Users with saves in the old format (data/player.json, data/stats.json, data/timeService.json) will lose access to them since the code now only looks for slot_N directories. Consider adding migration logic to automatically move existing saves to slot_1 on first run, or at minimum document this breaking change clearly in release notes so users can manually migrate their saves before updating.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added automatic migration! The migrate_old_save_files() method now runs on startup and automatically moves existing saves from the old format (data/*.json) to slot_1 in the new format. Tested with actual old save files - migration completes silently and users can continue their game without data loss. (6c58c97)


# if save file exists, load it
if (
os.path.exists("data/player.json")
and os.path.getsize("data/player.json") > 0
):
player_path = self.saveFileManager.get_save_path("player.json")
if os.path.exists(player_path) and os.path.getsize(player_path) > 0:
self.loadPlayer()
else:
self.player = Player()

# if save file exists, load it
if os.path.exists("data/stats.json") and os.path.getsize("data/stats.json") > 0:
stats_path = self.saveFileManager.get_save_path("stats.json")
if os.path.exists(stats_path) and os.path.getsize(stats_path) > 0:
self.loadStats()
else:
self.stats = Stats()

# if save file exists, load it
if (
os.path.exists("data/timeService.json")
and os.path.getsize("data/timeService.json") > 0
):
time_path = self.saveFileManager.get_save_path("timeService.json")
if os.path.exists(time_path) and os.path.getsize(time_path) > 0:
self.loadTimeService()
else:
self.timeService = TimeService(self.player, self.stats)
Expand Down Expand Up @@ -88,6 +94,102 @@ def __init__(self):

self.currentLocation = LocationType.HOME

def _selectSaveFile(self):
"""Display save file selection menu and let user choose"""
while True: # Use loop instead of recursion to avoid stack overflow
save_files = self.saveFileManager.list_save_files()

print("\n" * 20)
print("-" * 75)
print("\n FISHE - SAVE FILE MANAGER")
print("-" * 75)

if save_files:
print("\n Available Save Files:\n")
for save in save_files:
metadata = save["metadata"]
print(f" [{save['slot']}] Save Slot {save['slot']}")
print(f" Day: {metadata.get('day', 1)}")
print(f" Money: ${metadata.get('money', 0)}")
print(f" Fish: {metadata.get('fishCount', 0)}")
print(f" Last Modified: {metadata.get('last_modified', 'Unknown')}")
print()

next_slot = self.saveFileManager.get_next_available_slot()
if next_slot is not None:
print(f" [N] Create New Save (Slot {next_slot})")
if save_files:
print(" [D] Delete a Save File")
print(" [Q] Quit")
print("-" * 75)

choice = input("\n Select an option: ").strip().upper()

if choice == "Q":
print("\n Goodbye!")
exit(0)
elif choice == "N" and next_slot is not None:
self.saveFileManager.select_save_slot(next_slot)
print(f"\n Creating new save in Slot {next_slot}...")
return
elif choice == "N" and next_slot is None:
print(" All save slots are full. Please delete a save first.")
elif choice == "D" and save_files:
if self._deleteSaveFile(save_files):
# Continue loop to show updated menu
continue
else:
# User cancelled, continue loop
continue
elif choice.isdigit():
slot_num = int(choice)
if any(save["slot"] == slot_num for save in save_files):
self.saveFileManager.select_save_slot(slot_num)
print(f"\n Loading Save Slot {slot_num}...")
return
else:
print(" Invalid slot number. Try again.")
else:
print(" Invalid choice. Try again.")

def _deleteSaveFile(self, save_files):
"""Delete a save file. Returns True if a file was deleted, False if cancelled."""
print("\n" * 20)
print("-" * 75)
print("\n DELETE SAVE FILE")
print("-" * 75)
print("\n Which save file would you like to delete?\n")

for save in save_files:
print(f" [{save['slot']}] Save Slot {save['slot']}")

print(" [C] Cancel")
print("-" * 75)

while True:
choice = input("\n Select a slot to delete: ").strip().upper()

if choice == "C":
return False
elif choice.isdigit():
slot_num = int(choice)
if any(save["slot"] == slot_num for save in save_files):
confirm = input(f"\n Are you sure you want to delete Slot {slot_num}? (Y/N): ").strip().upper()
if confirm == "Y":
if self.saveFileManager.delete_save_slot(slot_num):
print(f"\n Slot {slot_num} deleted successfully.")
input("\n [ CONTINUE ]")
return True
else:
print(f"\n Failed to delete Slot {slot_num}.")
return False
else:
return False
else:
print(" Invalid slot number. Try again.")
else:
print(" Invalid choice. Try again.")

def play(self):
while self.running:
# change location
Expand All @@ -103,37 +205,53 @@ def play(self):
self.save()

def save(self):
# create data directory
if not os.path.exists("data"):
os.makedirs("data")
# create data directory - use SaveFileManager's directory
if not os.path.exists(self.saveFileManager.data_directory):
os.makedirs(self.saveFileManager.data_directory, exist_ok=True)

playerSaveFile = open("data/player.json", "w")
self.playerJsonReaderWriter.writePlayerToFile(self.player, playerSaveFile)
try:
with open(self.saveFileManager.get_save_path("player.json"), "w") as playerSaveFile:
self.playerJsonReaderWriter.writePlayerToFile(self.player, playerSaveFile)

timeServiceSaveFile = open("data/timeService.json", "w")
self.timeServiceJsonReaderWriter.writeTimeServiceToFile(
self.timeService, timeServiceSaveFile
)
with open(self.saveFileManager.get_save_path("timeService.json"), "w") as timeServiceSaveFile:
self.timeServiceJsonReaderWriter.writeTimeServiceToFile(
self.timeService, timeServiceSaveFile
)

statsSaveFile = open("data/stats.json", "w")
self.statsJsonReaderWriter.writeStatsToFile(self.stats, statsSaveFile)
with open(self.saveFileManager.get_save_path("stats.json"), "w") as statsSaveFile:
self.statsJsonReaderWriter.writeStatsToFile(self.stats, statsSaveFile)
except (IOError, OSError) as e:
print(f"\n Warning: Failed to save game: {e}")
# Game continues even if save fails

def loadPlayer(self):
playerSaveFile = open("data/player.json", "r")
self.player = self.playerJsonReaderWriter.readPlayerFromFile(playerSaveFile)
playerSaveFile.close()
try:
with open(self.saveFileManager.get_save_path("player.json"), "r") as playerSaveFile:
self.player = self.playerJsonReaderWriter.readPlayerFromFile(playerSaveFile)
except (IOError, OSError, json.JSONDecodeError) as e:
print(f"\n Warning: Failed to load player data: {e}")
print(" Creating new player...")
self.player = Player()

def loadStats(self):
statsSaveFile = open("data/stats.json", "r")
self.stats = self.statsJsonReaderWriter.readStatsFromFile(statsSaveFile)
statsSaveFile.close()
try:
with open(self.saveFileManager.get_save_path("stats.json"), "r") as statsSaveFile:
self.stats = self.statsJsonReaderWriter.readStatsFromFile(statsSaveFile)
except (IOError, OSError, json.JSONDecodeError) as e:
print(f"\n Warning: Failed to load stats data: {e}")
print(" Creating new stats...")
self.stats = Stats()

def loadTimeService(self):
timeServiceSaveFile = open("data/timeService.json", "r")
self.timeService = self.timeServiceJsonReaderWriter.readTimeServiceFromFile(
timeServiceSaveFile, self.player, self.stats
)
timeServiceSaveFile.close()
try:
with open(self.saveFileManager.get_save_path("timeService.json"), "r") as timeServiceSaveFile:
self.timeService = self.timeServiceJsonReaderWriter.readTimeServiceFromFile(
timeServiceSaveFile, self.player, self.stats
)
except (IOError, OSError, json.JSONDecodeError) as e:
print(f"\n Warning: Failed to load time service data: {e}")
print(" Creating new time service...")
self.timeService = TimeService(self.player, self.stats)


if __name__ == "__main__":
Expand Down
Loading