Skip to content

Add multiple save file support with save slot management#52

Merged
dmccoystephenson merged 7 commits intomainfrom
copilot/add-multiple-save-files-support
Mar 4, 2026
Merged

Add multiple save file support with save slot management#52
dmccoystephenson merged 7 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 132 tests passing (32 SaveFileManager tests)
  • 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
  • Comprehensive test coverage expansion
    • 14 additional test cases added
    • Edge case testing: invalid slot names, boundary values
    • Error resilience: OSError, empty files, corrupted JSON
    • Migration scenarios: partial migration, existing slot conflicts
    • Integration tests: full user workflow simulation

Test Coverage Summary

  • 32 SaveFileManager tests covering all functionality
  • 132 total tests in the project
  • Comprehensive coverage of edge cases, error scenarios, and integration workflows
  • All tests passing ✅

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)

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>
@dmccoystephenson
Copy link
Member

@copilot expand unit tests

Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot expand unit tests

Expanded unit tests from 18 to 32 tests for SaveFileManager (+14 new tests). Total test suite grew from 118 to 132 tests. New coverage includes:

Edge Cases: Invalid slot names (slot_abc, slot_0, slot_100), boundary values, non-directory entries
Error Handling: OSError resilience, empty files, corrupted JSON handling
Migration: Partial migration (only player.json), existing slot_1 overwrite scenarios
Metadata: Partial data with defaults, missing optional files (timeService.json)
Integration: Full user workflows - create/delete/list operations in sequence

All 132 tests passing ✅ (39ec6fb)

@dmccoystephenson dmccoystephenson marked this pull request as ready for review March 4, 2026 03:38
@dmccoystephenson dmccoystephenson merged commit fed01f6 into main Mar 4, 2026
1 check passed
@dmccoystephenson dmccoystephenson deleted the copilot/add-multiple-save-files-support branch March 4, 2026 04:55
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

3 participants