Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
be77da5
Initial plan
Copilot Sep 21, 2025
4171ed5
Fix recursive gambling issue in tavern - resolves #22
Copilot Sep 21, 2025
6e7e587
Initial plan
Copilot Sep 29, 2025
a5e7108
Implement energy system - player starts with 100 energy, fishing cons…
Copilot Sep 29, 2025
3b3ba32
Merge pull request #36 from Stephenson-Software/copilot/fix-f5c0831f-…
dmccoystephenson Sep 29, 2025
397f937
Merge pull request #29 from Stephenson-Software/copilot/fix-ed1b91bc-…
dmccoystephenson Oct 2, 2025
0ab9e85
Initial plan
Copilot Oct 2, 2025
8ce8ecf
Initial plan
Copilot Oct 2, 2025
a2c4b83
Initial plan
Copilot Oct 2, 2025
06c4f8b
Allow decimal values for bank deposits and withdrawals
Copilot Oct 2, 2025
6d2e3d8
Fix tavern gambling win message showing $0 instead of actual bet amount
Copilot Oct 2, 2025
f2f98e6
Add Config class to hold configuration options
Copilot Oct 2, 2025
35e2803
Remove trailing newline from test file
Copilot Oct 2, 2025
5f76162
Merge pull request #37 from Stephenson-Software/copilot/fix-7a41e107-…
dmccoystephenson Oct 13, 2025
c2f0f55
Merge pull request #38 from Stephenson-Software/copilot/fix-44c7da9f-…
dmccoystephenson Oct 13, 2025
0f90eb4
Merge pull request #39 from Stephenson-Software/copilot/fix-107ad07a-…
dmccoystephenson Oct 13, 2025
99d90b3
Initial plan
Copilot Oct 13, 2025
ad69dfb
Add comprehensive unit tests for JSON I/O, prompt, and tavern
Copilot Oct 13, 2025
ff272e1
Add GitHub Actions CI workflow to run unit tests
Copilot Feb 1, 2026
6da77f1
Merge pull request #41 from Stephenson-Software/copilot/expand-unit-t…
dmccoystephenson Feb 1, 2026
3e45276
Initial plan
Copilot Feb 1, 2026
8d9fd63
Add NPC system with names and backstories for all locations
Copilot Feb 1, 2026
d0ce3a0
Fix trailing whitespace in test files
Copilot Feb 1, 2026
bbf82ef
Address PR comments: fix Stats import, extract dialogue to UserInterf…
Copilot Feb 1, 2026
e4774d1
Merge pull request #45 from Stephenson-Software/copilot/expand-npc-ba…
dmccoystephenson Feb 1, 2026
6d2a243
Initial plan
Copilot Feb 1, 2026
9c7a6a8
Implement interactive fishing minigame with timing-based catches
Copilot Feb 1, 2026
003baa5
Address code review feedback: fix naming conventions
Copilot Feb 1, 2026
60b2e74
Fix test isolation issue causing test_sellFish to fail
Copilot Feb 1, 2026
b9d8228
Fix CI failure by using infinite generators for time.time mocks
Copilot Feb 1, 2026
667c703
Merge pull request #47 from Stephenson-Software/copilot/make-fishing-…
dmccoystephenson Feb 2, 2026
84955b4
Initial plan
Copilot Feb 2, 2026
113eb2e
Add interactive dialogue system with tutorial content for NPCs
Copilot Feb 2, 2026
c4d40d8
Address PR feedback: fix dialogue_options initialization and add test…
Copilot Feb 2, 2026
b4ba8e9
Improve test to avoid side effects with cleanup
Copilot Feb 2, 2026
bcbe8cc
Add 'Tell me about yourself' dialogue option for all NPCs
Copilot Feb 2, 2026
312dab7
Merge pull request #49 from Stephenson-Software/copilot/expand-dialog…
dmccoystephenson Feb 2, 2026
8d92f12
Initial plan
Copilot Feb 5, 2026
e5cb186
Implement multiple save files with SaveFileManager
Copilot Feb 5, 2026
710045c
Update .gitignore to exclude data directory
Copilot Feb 5, 2026
102ceac
Address code review feedback - improve imports and test structure
Copilot Feb 5, 2026
1e5ad90
Add multiple save files documentation to README
Copilot Feb 5, 2026
6c58c97
Address PR review comments - improve error handling, fix recursion, a…
Copilot Feb 15, 2026
39ec6fb
Expand unit tests with 14 additional test cases for SaveFileManager
Copilot Mar 4, 2026
fed01f6
Merge pull request #52 from Stephenson-Software/copilot/add-multiple-…
dmccoystephenson Mar 4, 2026
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
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Run Unit Tests

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov jsonschema

- name: Run tests with coverage
run: |
python -m pytest --verbose -vv --cov=src --cov-report=term-missing --cov-report=xml:cov.xml

- name: Upload coverage reports
uses: codecov/codecov-action@v4
with:
file: ./cov.xml
fail_ci_if_error: false
continue-on-error: true
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.pyc
data/*.json
__pycache__/
data/
.coverage
cov.xml
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
# FishE

[![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.
8 changes: 7 additions & 1 deletion schemas/player.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@
"priceForBait": {
"type": "number",
"minimum": 0
},
"energy": {
"type": "integer",
"minimum": 0,
"maximum": 100
}
},
"required": [
"fishCount",
"money",
"moneyInBank",
"fishMultiplier",
"priceForBait"
"priceForBait",
"energy"
]
}
Empty file added src/config/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions src/config/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# @author Daniel McCoy Stephenson
class Config:
def __init__(self):
# Save file paths
self.dataDirectory = "data"
self.playerSaveFile = "data/player.json"
self.statsSaveFile = "data/stats.json"
self.timeServiceSaveFile = "data/timeService.json"

# Initial player values
self.initialMoney = 20
self.initialEnergy = 100
self.initialFishCount = 0
self.initialMoneyInBank = 0.01
self.initialFishMultiplier = 1
self.initialPriceForBait = 50
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()

# 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