Skip to content
Merged
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: 0 additions & 2 deletions PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ You are a fisherman living in a home by the docks. There’s a store nearby that


GOALS
- Make it possible to open a fishing business
- Make hiring workers a thing
- FISH LOADING BAIT PRICE BUG

Maybes
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ The entire game — save-file manager, fishing, shop, bank, tavern, and NPC dial
### Your Goal
Build a fortune of **$10,000** in total wealth (cash on hand plus savings in the bank). Your progress toward the goal is shown in the status header, and reaching it earns a one-time victory — after which you're free to keep fishing or retire from the Home menu.

### Fishing Business
Once you can afford it, buy a **boat** at the docks ("Manage Boat & Crew") and **hire workers**. Each day your crew brings in a passive catch in exchange for a daily wage — turning saved-up money into ongoing production. If you over-hire and can't make payroll, the workers you can't pay quit, so keep enough cash on hand to cover wages.

### 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:

Expand Down
7 changes: 7 additions & 0 deletions schemas/player.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
"type": "integer",
"minimum": 0
}
},
"hasBoat": {
"type": "boolean"
},
"workers": {
"type": "integer",
"minimum": 0
}
},
"required": [
Expand Down
Empty file added src/business/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions src/business/business.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# @author Daniel McCoy Stephenson
#
# The fishing business: once the player owns a boat they can hire workers, who
# bring in a passive catch each day in exchange for a daily wage. This turns
# accumulated money into ongoing production rather than just a number that grows.

BOAT_PRICE = 500
MAX_WORKERS = 5
WORKER_DAILY_WAGE = 10
WORKER_FISH_PER_DAY = 6
WORKER_CATCH_SPECIES = "Minnow" # workers bring in the common catch


def runDailyProduction(player, stats=None):
"""Apply one day of the fishing business and return a summary.

Each worker catches WORKER_FISH_PER_DAY fish for WORKER_DAILY_WAGE in wages.
If the player can't cover the full payroll, the workers they can't pay quit
(so an over-hired, broke business shrinks instead of going into debt)."""
summary = {
"workers": player.workers,
"fishCaught": 0,
"wagesPaid": 0,
"quit": 0,
}
if not player.hasBoat or player.workers <= 0:
return summary

affordable = min(player.workers, int(player.money // WORKER_DAILY_WAGE))
if affordable < player.workers:
summary["quit"] = player.workers - affordable
player.workers = affordable
summary["workers"] = player.workers

if affordable <= 0:
return summary

wages = affordable * WORKER_DAILY_WAGE
caught = affordable * WORKER_FISH_PER_DAY
player.money -= wages
player.addFish(WORKER_CATCH_SPECIES, caught)
summary["wagesPaid"] = wages
summary["fishCaught"] = caught
if stats is not None:
stats.totalFishCaught += caught
return summary
76 changes: 75 additions & 1 deletion src/location/docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ui.userInterface import UserInterface
from npc.npc import NPC
from fish import fish
from business import business


# The catch reaction window widens with the player's rod level, so a better rod
Expand Down Expand Up @@ -84,7 +85,15 @@ def __init__(
)

def run(self):
li = ["Fish", "Talk to %s" % self.npc.name, "Go Home", "Go to Shop", "Go to Tavern", "Go to Bank"]
li = [
"Fish",
"Talk to %s" % self.npc.name,
"Go Home",
"Go to Shop",
"Go to Tavern",
"Go to Bank",
"Manage Boat & Crew",
]
input = self.userInterface.showOptions(
"You breathe in the fresh air. Salty.", li
)
Expand Down Expand Up @@ -120,6 +129,71 @@ def run(self):
)
return LocationType.BANK

elif input == "7":
self.manageBusiness()
return LocationType.DOCKS

def _businessStatus(self):
if not self.player.hasBoat:
return (
"You have no boat. A boat lets you hire a crew that brings in a "
"passive catch each day. A boat costs $%d." % business.BOAT_PRICE
)
return (
"Your crew: %d/%d workers. Each catches %d fish per day for $%d in "
"wages, paid automatically every new day."
% (
self.player.workers,
business.MAX_WORKERS,
business.WORKER_FISH_PER_DAY,
business.WORKER_DAILY_WAGE,
)
)

def manageBusiness(self):
while True:
options = []
actions = []
if not self.player.hasBoat:
options.append("Buy a Boat ($%d)" % business.BOAT_PRICE)
actions.append("buy_boat")
else:
if self.player.workers < business.MAX_WORKERS:
options.append(
"Hire a Worker (+%d fish/day for $%d/day)"
% (business.WORKER_FISH_PER_DAY, business.WORKER_DAILY_WAGE)
)
actions.append("hire")
if self.player.workers > 0:
options.append("Dismiss a Worker")
actions.append("dismiss")
options.append("Back")
actions.append("back")

choice = int(
self.userInterface.showOptions(self._businessStatus(), options)
)
action = actions[choice - 1]

if action == "buy_boat":
if self.player.money >= business.BOAT_PRICE:
self.player.money -= business.BOAT_PRICE
self.player.hasBoat = True
self.currentPrompt.text = "You bought a boat! Now hire a crew."
else:
self.currentPrompt.text = "You can't afford a boat yet."
elif action == "hire":
self.player.workers += 1
self.currentPrompt.text = (
"You hired a worker. They'll fish each day for their wage."
)
elif action == "dismiss":
self.player.workers -= 1
self.currentPrompt.text = "You let a worker go."
elif action == "back":
self.currentPrompt.text = "What would you like to do?"
return

def getTimeOfDayModifier(self, hour):
"""Return (yield factor, flavour label) for fishing at the given hour.

Expand Down
4 changes: 4 additions & 0 deletions src/player/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def __init__(self):
# Per-species breakdown of the fish currently held. fishCount remains the
# aggregate total; addFish/clearFish keep the two in sync.
self.fishByType = {}
# Fishing business: a boat unlocks hiring workers who bring in a passive
# daily catch for a daily wage (see src/business).
self.hasBoat = False
self.workers = 0

def addFish(self, fishTypeName, amount):
self.fishByType[fishTypeName] = self.fishByType.get(fishTypeName, 0) + amount
Expand Down
4 changes: 4 additions & 0 deletions src/player/playerJsonReaderWriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def createJsonFromPlayer(self, player):
"energy": player.energy,
"rodLevel": player.rodLevel,
"fishByType": player.fishByType,
"hasBoat": player.hasBoat,
"workers": player.workers,
}

def createPlayerFromJson(self, playerJson):
Expand All @@ -28,6 +30,8 @@ def createPlayerFromJson(self, playerJson):
player.energy = playerJson.get("energy", player.energy)
player.rodLevel = playerJson.get("rodLevel", player.rodLevel)
player.fishByType = playerJson.get("fishByType", player.fishByType)
player.hasBoat = playerJson.get("hasBoat", player.hasBoat)
player.workers = playerJson.get("workers", player.workers)
return player

def writePlayerToFile(self, player, jsonFile):
Expand Down
5 changes: 5 additions & 0 deletions src/world/timeService.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import math

from business import business


# Daily bank interest is deliberately modest and capped so that saving is a
# minor convenience rather than a way to bypass fishing entirely: sleeping is
Expand Down Expand Up @@ -36,3 +38,6 @@ def increaseDay(self):
self.player.moneyInBank += moneyToAdd
self.stats.moneyMadeFromInterest += moneyToAdd
self.stats.totalMoneyMade += moneyToAdd

# The fishing business (if any) produces its daily catch and pays wages.
business.runDailyProduction(self.player, self.stats)
75 changes: 75 additions & 0 deletions tests/business/test_business.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from src.business import business
from src.player.player import Player
from src.stats.stats import Stats


def test_no_production_without_a_boat():
# prepare - workers but no boat
player = Player()
player.workers = 3
player.money = 1000

# call
summary = business.runDailyProduction(player)

# check - nothing happens until there's a boat
assert summary["fishCaught"] == 0
assert summary["wagesPaid"] == 0
assert player.money == 1000
assert player.fishCount == 0


def test_workers_catch_fish_and_draw_wages():
# prepare - a boat and two workers, plenty of money
player = Player()
player.hasBoat = True
player.workers = 2
player.money = 1000
stats = Stats()

# call
summary = business.runDailyProduction(player, stats)

# check
expectedFish = 2 * business.WORKER_FISH_PER_DAY
expectedWages = 2 * business.WORKER_DAILY_WAGE
assert summary["fishCaught"] == expectedFish
assert summary["wagesPaid"] == expectedWages
assert player.fishCount == expectedFish
assert player.money == 1000 - expectedWages
assert stats.totalFishCaught == expectedFish


def test_unaffordable_workers_quit():
# prepare - 3 workers but only enough money for one day's wage of one worker
player = Player()
player.hasBoat = True
player.workers = 3
player.money = business.WORKER_DAILY_WAGE # covers exactly one worker

# call
summary = business.runDailyProduction(player)

# check - the two unpayable workers quit; the remaining one is paid and fishes
assert summary["quit"] == 2
assert player.workers == 1
assert summary["wagesPaid"] == business.WORKER_DAILY_WAGE
assert summary["fishCaught"] == business.WORKER_FISH_PER_DAY
assert player.money == 0


def test_all_workers_quit_when_broke():
# prepare - a boat and workers but no money
player = Player()
player.hasBoat = True
player.workers = 2
player.money = 0

# call
summary = business.runDailyProduction(player)

# check - everyone quits, nothing caught
assert player.workers == 0
assert summary["quit"] == 2
assert summary["fishCaught"] == 0
assert player.fishCount == 0
79 changes: 79 additions & 0 deletions tests/location/test_docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,82 @@ def make_docks_with_rod(rodLevel):

# check - the wider window of the better rod lands more catches
assert results["highRod"] > results["lowRod"]


def test_run_manage_business_action():
# prepare
docksInstance = createDocks()
docksInstance.userInterface.showOptions = MagicMock(return_value="7")
docksInstance.manageBusiness = MagicMock()

# call
nextLocation = docksInstance.run()

# check
assert nextLocation == LocationType.DOCKS
docksInstance.manageBusiness.assert_called_once()


def test_manageBusiness_buy_boat():
# prepare - enough money for a boat; buy it, then go Back
from src.business import business

docksInstance = createDocks()
docksInstance.player.money = business.BOAT_PRICE + 50
docksInstance.player.hasBoat = False
# "1" = Buy a Boat; then in the post-purchase menu "2" = Back
docksInstance.userInterface.showOptions = MagicMock(side_effect=["1", "2"])

# call
docksInstance.manageBusiness()

# check
assert docksInstance.player.hasBoat is True
assert docksInstance.player.money == 50


def test_manageBusiness_buy_boat_insufficient_funds():
# prepare - can't afford a boat
from src.business import business

docksInstance = createDocks()
docksInstance.player.money = business.BOAT_PRICE - 1
docksInstance.player.hasBoat = False
docksInstance.userInterface.showOptions = MagicMock(side_effect=["1", "2"])

# call
docksInstance.manageBusiness()

# check - no boat, no money spent
assert docksInstance.player.hasBoat is False
assert docksInstance.player.money == business.BOAT_PRICE - 1


def test_manageBusiness_hire_worker():
# prepare - own a boat, no crew yet
docksInstance = createDocks()
docksInstance.player.hasBoat = True
docksInstance.player.workers = 0
# "1" = Hire; then in the menu with a worker "3" = Back (Hire/Dismiss/Back)
docksInstance.userInterface.showOptions = MagicMock(side_effect=["1", "3"])

# call
docksInstance.manageBusiness()

# check
assert docksInstance.player.workers == 1


def test_manageBusiness_dismiss_worker():
# prepare - own a boat with two workers
docksInstance = createDocks()
docksInstance.player.hasBoat = True
docksInstance.player.workers = 2
# "2" = Dismiss (Hire/Dismiss/Back); then "3" = Back
docksInstance.userInterface.showOptions = MagicMock(side_effect=["2", "3"])

# call
docksInstance.manageBusiness()

# check
assert docksInstance.player.workers == 1
2 changes: 2 additions & 0 deletions tests/player/test_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def test_initialization():
assert player.energy == 100
assert player.rodLevel == 1
assert player.fishByType == {}
assert player.hasBoat is False
assert player.workers == 0


def test_addFish_and_clearFish_keep_count_in_sync():
Expand Down
Loading
Loading