From 35d3213d81b74c1ed100bacd7587934851a79739 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Sat, 13 Jun 2026 15:23:17 -0600 Subject: [PATCH] Give the shop a daily money budget that recharges (closes #20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the long-standing "make the shop's money recharge over time" goal. The shop now holds a daily budget (SHOP_DAILY_BUDGET, $750) that refills at the start of each new day. When you sell, it buys your fish most-valuable-first until the budget runs out; any unaffordable fish stay in your inventory to sell another day, and a clear message tells you the shop is tapped out for today. Design call: the budget gates selling (otherwise shop money is meaningless flavor), but it's tuned to cover a normal day's catch and only bite on very large hauls — so it adds "sell regularly / store wealth in the bank" texture without being a daily wall. Verified with the playtest simulation: the optimal reinvest strategy slows only mildly (~20 -> ~24 days to the goal) and the steady-income business strategy stays competitive (~25). No persistence/schema change — the shop isn't saved, so its budget simply starts full and refills daily. Removed the now-unused `random` import. Closes #20 Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 ++ src/location/shop.py | 72 ++++++++++++++++++++++++++++++------- tests/location/test_shop.py | 51 ++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9c8f87b..c760c8c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ Build a fortune of **$10,000** in total wealth (cash on hand plus savings in the ### 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. +### Selling Fish +Sell your catch at the shop. The shop has a limited amount of money each day that refills overnight, so a very large haul may sell out the shop and need to be finished the next day — sell regularly, and park your earnings in the bank or reinvest them in gear and your crew. + ### 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: diff --git a/src/location/shop.py b/src/location/shop.py index 42757d5..059eb72 100644 --- a/src/location/shop.py +++ b/src/location/shop.py @@ -1,4 +1,3 @@ -import random from location.enum.locationType import LocationType from player.player import Player from prompt.prompt import Prompt @@ -20,6 +19,12 @@ ROD_BASE_PRICE = 75 MAX_ROD_LEVEL = 10 +# The shop has a limited pool of money for buying fish that refills each new day. +# It comfortably covers a normal day's catch but can be exhausted by a very large +# haul, so massive hoards must be sold over several days (and the bank/business +# give somewhere to put wealth in the meantime). +SHOP_DAILY_BUDGET = 750 + def rodUpgradeCost(rodLevel): return ROD_BASE_PRICE * rodLevel @@ -84,6 +89,9 @@ def __init__( } ] ) + # Daily budget for buying fish; refills when a new day begins. + self.money = SHOP_DAILY_BUDGET + self.lastRefillDay = self.timeService.day def run(self): li = [ @@ -114,21 +122,59 @@ def run(self): self.currentPrompt.text = "What would you like to do?" return LocationType.DOCKS + def _refillIfNewDay(self): + if self.timeService.day > self.lastRefillDay: + self.money = SHOP_DAILY_BUDGET + self.lastRefillDay = self.timeService.day + def sellFish(self): - if self.player.fishByType: - # Price each species individually (rarer fish are worth more). - moneyToAdd = 0 - for fishTypeName, count in self.player.fishByType.items(): - moneyToAdd += count * fish.fishValue(fishTypeName) - else: - # Legacy save with only an aggregate count: flat $3-5 per fish. - moneyToAdd = self.player.fishCount * random.randint(3, 5) + self._refillIfNewDay() - self.player.money += moneyToAdd - self.stats.totalMoneyMade += moneyToAdd - self.player.clearFish() + if self.player.fishCount == 0: + self.currentPrompt.text = "You have no fish to sell." + return - self.currentPrompt.text = "You sold all of your fish!" + # One entry per held fish. Sell the most valuable species first so the + # best fish are cashed in before the shop's daily budget runs out; any + # unaffordable leftovers stay in the inventory for another day. + if self.player.fishByType: + queue = [] + for species, count in self.player.fishByType.items(): + queue.extend([species] * count) + queue.sort( + key=lambda s: (fish.getFishType(s) or {}).get("maxValue", 0), + reverse=True, + ) + else: + # Legacy save with only an aggregate count (no species breakdown). + queue = [None] * self.player.fishCount + + earned = 0.0 + for species in queue: + value = fish.fishValue(species) + if self.money < value: + break # shop is out of money for today + self.money -= value + self.player.money += value + self.stats.totalMoneyMade += value + earned += value + self._removeOneFish(species) + + if self.player.fishCount > 0: + self.currentPrompt.text = ( + "Sold fish for $%.2f, but the shop is out of money for today. " + "Come back tomorrow for the rest." % earned + ) + else: + self.currentPrompt.text = "You sold your fish for $%.2f!" % earned + + def _removeOneFish(self, species): + self.player.fishCount -= 1 + if species is None: + return + self.player.fishByType[species] -= 1 + if self.player.fishByType[species] == 0: + del self.player.fishByType[species] def buyBetterBait(self): if self.player.fishMultiplier >= MAX_FISH_MULTIPLIER: diff --git a/tests/location/test_shop.py b/tests/location/test_shop.py index e50f16e..60b1d7a 100644 --- a/tests/location/test_shop.py +++ b/tests/location/test_shop.py @@ -229,3 +229,54 @@ def test_buyBetterRod_refused_at_cap(): assert shopInstance.player.rodLevel == MAX_ROD_LEVEL assert shopInstance.player.money == 1000000 assert shopInstance.currentPrompt.text == "Your rod is already the finest in the village!" + + +def test_sellFish_limited_by_shop_budget(): + # prepare - a haul worth far more than the shop's daily budget + from src.fish import fish + from src.location.shop import SHOP_DAILY_BUDGET + + shopInstance = createShop() + shopInstance.player.money = 0 + # 100 Marlins ($15-25 each) >> the budget, so the shop can't buy them all + shopInstance.player.addFish("Marlin", 100) + + # call + shopInstance.sellFish() + + # check - the shop spent (about) its whole budget and some fish remain unsold + assert shopInstance.player.money <= SHOP_DAILY_BUDGET + assert shopInstance.player.money > SHOP_DAILY_BUDGET - 25 # within one fish of the cap + assert shopInstance.money < 25 # budget nearly exhausted + assert shopInstance.player.fishCount > 0 # leftovers carried over + assert "out of money for today" in shopInstance.currentPrompt.text + + +def test_shop_budget_refills_next_day(): + # prepare - exhaust the shop's budget + shopInstance = createShop() + shopInstance.player.addFish("Marlin", 100) + shopInstance.sellFish() + assert shopInstance.money < 25 # drained + + # a new day begins + shopInstance.timeService.day += 1 + + # call - selling again first refills the budget for the new day + leftover_before = shopInstance.player.fishCount + shopInstance.sellFish() + + # check - more fish sold (budget refilled), inventory shrank further + assert shopInstance.player.fishCount < leftover_before + + +def test_sellFish_no_fish_message(): + # prepare + shopInstance = createShop() + shopInstance.player.clearFish() + + # call + shopInstance.sellFish() + + # check + assert shopInstance.currentPrompt.text == "You have no fish to sell."