Skip to content

Comments

Add multiple save file support with save slot management#52

Draft
Copilot wants to merge 6 commits intomainfrom
copilot/add-multiple-save-files-support
Draft

Add multiple save file support with save slot management#52
Copilot wants to merge 6 commits intomainfrom
copilot/add-multiple-save-files-support

Conversation

Copy link
Contributor

Copilot AI commented Feb 5, 2026

Multiple Save Files Implementation - COMPLETE ✅

  • Understand current codebase and save system
  • Run existing tests to establish baseline (100 tests passing)
  • Design multiple save file architecture
    • Create SaveFileManager class to manage multiple save files
    • Add save file selection menu at startup
    • Update FishE initialization to use SaveFileManager
  • Implement save file listing and selection
    • List available save files in data directory
    • Allow user to select existing save or create new one
    • Display save file metadata (day, money, fish, etc.)
  • Update save/load logic
    • Modify save() to use selected save file
    • Modify load methods to use selected save file
    • Support multiple save directories (slot_1, slot_2, etc.)
  • Create tests for new functionality
    • Test save file discovery
    • Test save file selection
    • Test multiple save files don't conflict
    • All 118 tests passing (18 new tests added)
  • Manual testing and verification
    • Run application and test save file creation ✓
    • Verify save file switching works ✓
    • Test edge cases (no saves, multiple saves, delete) ✓
    • Update .gitignore to exclude data directory
  • Code review - All feedback addressed ✓
  • Security review with CodeQL - No vulnerabilities found ✓
  • Documentation updated in README
  • PR review comments addressed:
    • Added error handling to all file operations
    • Switched to context managers for file operations
    • Fixed slot 100 edge case (returns None when full)
    • Added backward compatibility with migration
    • Fixed hardcoded data directory
    • Replaced recursion with loop to avoid stack overflow
    • Improved exception handling with specific exceptions
    • Optimized list_save_files to use os.listdir()
    • Added exist_ok=True to makedirs

Security Summary

✅ No security vulnerabilities detected by CodeQL analysis.

Original prompt

This section details on the original issue you should resolve

<issue_title>Multiple save files</issue_title>
<issue_description>As a user, I would appreciate the ability to have multiple save files and be able to detect/manage these saves when the application starts.</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 4 commits February 5, 2026 09:58
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for multiple save files management Add multiple save file support with save slot management Feb 5, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds multiple save file support to the FishE game, allowing players to maintain independent save files in separate slots. The implementation introduces a SaveFileManager class to handle slot operations and updates the FishE initialization to display an interactive save selection menu at startup.

Changes:

  • New SaveFileManager class manages save slots with metadata display, creation, deletion, and gap-filling
  • FishE initialization updated to show save selection menu and route all save/load operations through selected slot paths
  • Added 15 comprehensive tests for SaveFileManager functionality and updated existing FishE test to mock save selection

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/saveFileManager.py New class managing save slots in slot_N directories with metadata reading and slot operations
src/fishE.py Added save selection menu, integrated SaveFileManager for all save/load operations, added file close calls
tests/test_saveFileManager.py Comprehensive test suite covering slot management, metadata reading, edge cases, and error conditions
tests/test_fishE.py Updated to mock SaveFileManager and _selectSaveFile method to avoid stdin interaction
README.md Documented the new multiple save files feature with usage details
.gitignore Changed from data/*.json to data/ to ignore entire data directory including slot subdirectories

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

src/fishE.py Outdated
Comment on lines 202 to 216
playerSaveFile = open(self.saveFileManager.get_save_path("player.json"), "w")
self.playerJsonReaderWriter.writePlayerToFile(self.player, playerSaveFile)
playerSaveFile.close()

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

statsSaveFile = open("data/stats.json", "w")
statsSaveFile = open(self.saveFileManager.get_save_path("stats.json"), "w")
self.statsJsonReaderWriter.writeStatsToFile(self.stats, statsSaveFile)
statsSaveFile.close()
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.

File operations in save(), loadPlayer(), loadStats(), and loadTimeService() methods lack error handling. If any file operation fails (e.g., disk full, permission denied, corrupted file), the game will crash without a helpful error message. Consider adding try-except blocks to catch and handle IOError or OSError exceptions gracefully, possibly notifying the user of the issue.

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 comprehensive error handling with try-except blocks for IOError, OSError, and json.JSONDecodeError in all file operations. The game now gracefully handles disk full, permission denied, and corrupted file scenarios with user-friendly warnings. (6c58c97)

src/fishE.py Outdated
Comment on lines 202 to 216
playerSaveFile = open(self.saveFileManager.get_save_path("player.json"), "w")
self.playerJsonReaderWriter.writePlayerToFile(self.player, playerSaveFile)
playerSaveFile.close()

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

statsSaveFile = open("data/stats.json", "w")
statsSaveFile = open(self.saveFileManager.get_save_path("stats.json"), "w")
self.statsJsonReaderWriter.writeStatsToFile(self.stats, statsSaveFile)
statsSaveFile.close()
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.

While adding .close() calls is good, consider using context managers (with statements) for file operations to ensure files are closed even if an exception occurs. This pattern is already used in SaveFileManager (lines 51, 59) and would make the code more robust and consistent. For example: with open(self.saveFileManager.get_save_path("player.json"), "w") as playerSaveFile:

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.

Updated all file operations to use context managers (with statements). This ensures files are properly closed even if exceptions occur, making the code more robust and consistent with SaveFileManager's existing patterns. (6c58c97)

Comment on lines 73 to 84
def get_next_available_slot(self):
"""Returns the next available save slot number"""
save_files = self.list_save_files()
if not save_files:
return 1

# Find gaps in slot numbers
existing_slots = sorted([save["slot"] for save in save_files])
for i in range(1, 100):
if i not in existing_slots:
return i
return len(existing_slots) + 1
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.

If all 99 save slots are filled, get_next_available_slot() returns 100 (line 84), but list_save_files() only iterates through range(1, 100) which stops at 99 (line 22). This means slot 100 would be created but never listed. Consider either: 1) returning None when all slots are full and handling this in the UI, or 2) extending the range to match in both functions, or 3) adding a check to prevent creating slot 100.

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.

Fixed by returning None when all 99 slots are full instead of 100. The UI now checks for None and displays "All save slots are full. Please delete a save first." Added test coverage for this edge case. (6c58c97)

Comment on lines +25 to +26
# Show save file selection menu
self._selectSaveFile()
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)

src/fishE.py Outdated
Comment on lines 199 to 200
if not os.path.exists("data"):
os.makedirs("data")
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.

The hardcoded "data" directory check is inconsistent with the SaveFileManager's configurable data_directory parameter. This creates a directory that might not match the SaveFileManager's configuration, potentially causing issues if a custom data directory is used. Consider using self.saveFileManager.data_directory instead of the hardcoded "data" string, or remove this check entirely since get_save_path() already creates the necessary directories (lines 99-100 in saveFileManager.py).

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.

Fixed! Changed hardcoded "data" to use self.saveFileManager.data_directory for consistency. Since get_save_path() already creates necessary directories, the redundant check is now properly aligned with SaveFileManager's configuration. (6c58c97)

src/fishE.py Outdated
Comment on lines 131 to 134
self._deleteSaveFile(save_files)
# Recursively call to show updated menu
self._selectSaveFile()
return
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.

The recursive call to _selectSaveFile() after deleting a save file could lead to a stack overflow if a user repeatedly deletes and cancels many times. Consider using a loop-based approach instead of recursion to handle menu refreshes, or at least add a depth limit.

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.

Replaced recursion with a loop-based approach. The _selectSaveFile() method now uses while True at the top level, and _deleteSaveFile() returns True/False to control flow. This eliminates any risk of stack overflow regardless of user behavior. (6c58c97)

Comment on lines 70 to 71
except Exception:
return None
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.

The bare except clause catches all exceptions silently, making debugging difficult. While returning None is appropriate for corrupted save files, consider logging the exception or being more specific about which exceptions to catch (e.g., json.JSONDecodeError, IOError). This would help identify issues during development and provide better diagnostics.

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.

Changed from bare except to catch specific exceptions: (json.JSONDecodeError, IOError, OSError). This makes debugging easier while still returning None for corrupted/inaccessible save files as intended. (6c58c97)

Comment on lines 21 to 35
# Look for save slots (slot_1, slot_2, etc.)
for i in range(1, 100): # Support up to 99 save slots
slot_name = f"slot_{i}"
slot_path = os.path.join(self.data_directory, slot_name)
if os.path.exists(slot_path):
metadata = self._read_save_metadata(slot_path)
if metadata:
save_files.append(
{
"slot": i,
"slot_name": slot_name,
"path": slot_path,
"metadata": metadata,
}
)
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.

The loop iterating through range(1, 100) checks for the existence of 99 directories on every call to list_save_files(). This could be inefficient if called frequently. Consider using os.listdir() to only check directories that actually exist, filtering for those matching the "slot_N" pattern. This would scale better with the number of actual save files rather than the maximum possible slots.

Suggested change
# Look for save slots (slot_1, slot_2, etc.)
for i in range(1, 100): # Support up to 99 save slots
slot_name = f"slot_{i}"
slot_path = os.path.join(self.data_directory, slot_name)
if os.path.exists(slot_path):
metadata = self._read_save_metadata(slot_path)
if metadata:
save_files.append(
{
"slot": i,
"slot_name": slot_name,
"path": slot_path,
"metadata": metadata,
}
)
# Look for save slots (slot_1, slot_2, etc.) by inspecting existing directories
for entry in os.listdir(self.data_directory):
if not entry.startswith("slot_"):
continue
# Extract the numeric slot index from the directory name
_, _, suffix = entry.partition("_")
if not suffix.isdigit():
continue
slot_index = int(suffix)
if slot_index < 1 or slot_index >= 100:
# Preserve the upper bound of 99 save slots
continue
slot_name = entry
slot_path = os.path.join(self.data_directory, slot_name)
if not os.path.isdir(slot_path):
continue
metadata = self._read_save_metadata(slot_path)
if metadata:
save_files.append(
{
"slot": slot_index,
"slot_name": slot_name,
"path": slot_path,
"metadata": metadata,
}
)

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.

Optimized using os.listdir() as suggested! Now only checks directories that actually exist and filters by the "slot_N" pattern. This scales with actual save files (typically 1-5) rather than always checking 99 slots. Much more efficient. (6c58c97)


# Create slot directory if it doesn't exist
if not os.path.exists(slot_path):
os.makedirs(slot_path)
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.

Consider adding exist_ok=True parameter to os.makedirs() for robustness. While unlikely in single-player context, this prevents potential FileExistsError if the directory is created between the existence check and the makedirs call. This is a defensive programming practice that makes the code more resilient.

Suggested change
os.makedirs(slot_path)
os.makedirs(slot_path, exist_ok=True)

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 exist_ok=True to os.makedirs() for defensive programming. While unlikely in single-player context, this prevents potential FileExistsError race conditions. (6c58c97)

@dmccoystephenson
Copy link
Member

@copilot address comments

…dd migration

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple save files

2 participants