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.
129 changes: 114 additions & 15 deletions src/fishE.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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 +20,28 @@ def __init__(self):
self.playerJsonReaderWriter = PlayerJsonReaderWriter()
self.timeServiceJsonReaderWriter = TimeServiceJsonReaderWriter()
self.statsJsonReaderWriter = StatsJsonReaderWriter()
self.saveFileManager = SaveFileManager()

# Show save file selection menu
self._selectSaveFile()

# 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 +90,96 @@ def __init__(self):

self.currentLocation = LocationType.HOME

def _selectSaveFile(self):
"""Display save file selection menu and let user choose"""
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()
print(f" [N] Create New Save (Slot {next_slot})")
if save_files:
print(" [D] Delete a Save File")
print(" [Q] Quit")
print("-" * 75)

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

if choice == "Q":
print("\n Goodbye!")
exit(0)
elif choice == "N":
self.saveFileManager.select_save_slot(next_slot)
print(f"\n Creating new save in Slot {next_slot}...")
break
elif choice == "D" and save_files:
self._deleteSaveFile(save_files)
# Recursively call to show updated menu
self._selectSaveFile()
return
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}...")
break
else:
print(" Invalid slot number. Try again.")
else:
print(" Invalid choice. Try again.")

def _deleteSaveFile(self, save_files):
"""Delete a save file"""
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
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
else:
print(f"\n Failed to delete Slot {slot_num}.")
else:
return
else:
print(" Invalid slot number. Try again.")
else:
print(" Invalid choice. Try again.")

def play(self):
while self.running:
# change location
Expand All @@ -107,29 +199,36 @@ def save(self):
if not os.path.exists("data"):
os.makedirs("data")

playerSaveFile = open("data/player.json", "w")
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()

def loadPlayer(self):
playerSaveFile = open("data/player.json", "r")
playerSaveFile = open(self.saveFileManager.get_save_path("player.json"), "r")
self.player = self.playerJsonReaderWriter.readPlayerFromFile(playerSaveFile)
playerSaveFile.close()

def loadStats(self):
statsSaveFile = open("data/stats.json", "r")
statsSaveFile = open(self.saveFileManager.get_save_path("stats.json"), "r")
self.stats = self.statsJsonReaderWriter.readStatsFromFile(statsSaveFile)
statsSaveFile.close()

def loadTimeService(self):
timeServiceSaveFile = open("data/timeService.json", "r")
timeServiceSaveFile = open(
self.saveFileManager.get_save_path("timeService.json"), "r"
)
self.timeService = self.timeServiceJsonReaderWriter.readTimeServiceFromFile(
timeServiceSaveFile, self.player, self.stats
)
Expand Down
112 changes: 112 additions & 0 deletions src/saveFileManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import os
import json
import shutil
from datetime import datetime


# @author Daniel McCoy Stephenson
class SaveFileManager:
"""Manages multiple save files for the game"""

def __init__(self, data_directory="data"):
self.data_directory = data_directory
self.selected_save_slot = None

def list_save_files(self):
"""Returns a list of available save file slots with their metadata"""
if not os.path.exists(self.data_directory):
return []

save_files = []
# 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,
}
)
return save_files

def _read_save_metadata(self, slot_path):
"""Read metadata from a save slot"""
try:
player_file = os.path.join(slot_path, "player.json")
time_file = os.path.join(slot_path, "timeService.json")

if not os.path.exists(player_file):
return None

metadata = {}

# Read player data
if os.path.exists(player_file) and os.path.getsize(player_file) > 0:
with open(player_file, "r") as f:
player_data = json.load(f)
metadata["money"] = player_data.get("money", 0)
metadata["fishCount"] = player_data.get("fishCount", 0)
metadata["energy"] = player_data.get("energy", 100)

# Read time data
if os.path.exists(time_file) and os.path.getsize(time_file) > 0:
with open(time_file, "r") as f:
time_data = json.load(f)
metadata["day"] = time_data.get("day", 1)
metadata["time"] = time_data.get("time", 0)

# Get last modified time
metadata["last_modified"] = datetime.fromtimestamp(
os.path.getmtime(player_file)
).strftime("%Y-%m-%d %H:%M:%S")

return metadata
except Exception:
return None

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

def select_save_slot(self, slot_number):
"""Select a save slot to use"""
self.selected_save_slot = slot_number

def get_save_path(self, filename):
"""Get the full path for a save file in the selected slot"""
if self.selected_save_slot is None:
raise ValueError("No save slot selected")

slot_name = f"slot_{self.selected_save_slot}"
slot_path = os.path.join(self.data_directory, slot_name)

# Create slot directory if it doesn't exist
if not os.path.exists(slot_path):
os.makedirs(slot_path)

return os.path.join(slot_path, filename)

def delete_save_slot(self, slot_number):
"""Delete a save slot"""
slot_name = f"slot_{slot_number}"
slot_path = os.path.join(self.data_directory, slot_name)

if os.path.exists(slot_path):
shutil.rmtree(slot_path)
return True
return False
16 changes: 14 additions & 2 deletions tests/test_fishE.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from src import fishE


Expand All @@ -16,10 +16,21 @@ def createFishE():
fishE.PlayerJsonReaderWriter = MagicMock()
fishE.TimeServiceJsonReaderWriter = MagicMock()
fishE.StatsJsonReaderWriter = MagicMock()
fishE.SaveFileManager = MagicMock()
fishE.loadPlayer = MagicMock()
fishE.loadStats = MagicMock()
fishE.loadTimeService = MagicMock()
return fishE.FishE()

# Mock the save file manager instance methods
mock_save_manager = MagicMock()
mock_save_manager.get_save_path.return_value = "data/player.json"
mock_save_manager.list_save_files.return_value = []
mock_save_manager.get_next_available_slot.return_value = 1
fishE.SaveFileManager.return_value = mock_save_manager

# Mock the _selectSaveFile method to avoid stdin interaction
with patch.object(fishE.FishE, '_selectSaveFile', return_value=None):
return fishE.FishE()


def test_initialization():
Expand Down Expand Up @@ -51,3 +62,4 @@ def test_initialization():
fishE.PlayerJsonReaderWriter.assert_called_once()
fishE.TimeServiceJsonReaderWriter.assert_called_once()
fishE.StatsJsonReaderWriter.assert_called_once()
fishE.SaveFileManager.assert_called_once()
Loading