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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Sell that basket and you're shown where you sleep; put in a few days and the vil

Nothing is made harder by this; it's pacing, not difficulty. A save file from a player who already owns half the village opens with all of it available, exactly as before.

An option you've earned but can't use right now stays on the menu and says why, rather than being picked and refused: fishing with an empty energy bar, buying gear you can't afford or have already maxed out, withdrawing from an empty account, guessing a dice face before you've staked anything. In the browser those are greyed-out buttons carrying the reason; the console and pygame front-ends tag the same rows and skip over them.

### Your Goal
Build a fortune of **$10,000** in total wealth (cash on hand plus savings in the bank). You're told about it once you've got your first $1,000 to your name, after which your progress toward the goal is shown in the status header. Reaching it earns a one-time victory — after which you're free to keep fishing or retire from the Home menu.

Expand Down
17 changes: 16 additions & 1 deletion src/location/bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ def run(self):
# a fixed number.
li = ["Make a Deposit", "Make a Withdrawal"]
actions = ["deposit", "withdraw"]
# An empty purse or an empty account makes one side of the counter
# pointless; both are visible in the header, so say which one is out.
unavailable = {}
if not (self.player.operatorMode or self.player.money > 0):
unavailable[1] = "no money on you"
if self.player.moneyInBank <= 0:
unavailable[2] = "nothing in the bank"
if progression.isUnlocked(self.stats, progression.TALK):
li.append("Talk to %s" % self.npc.name)
actions.append("talk")
Expand All @@ -131,6 +138,7 @@ def run(self):
input = self.userInterface.showOptions(
"You're at the front of the line and the teller asks you what you want to do.",
li,
unavailable,
)
action = actions[int(input) - 1]

Expand Down Expand Up @@ -188,6 +196,7 @@ def manageInvestments(self):
while True:
options = []
actions = []
unavailable = {}
owned = investments.ownedCounts(self.player)
for typeId in range(1, len(investments.PROPERTY_TYPES) + 1):
info = investments.typeInfo(typeId)
Expand All @@ -196,6 +205,10 @@ def manageInvestments(self):
% (info["name"], info["cost"], info["dailyIncome"])
)
actions.append(("buy", typeId))
# The whole list is always shown, so the affordable rungs of the
# ladder read against the ones still out of reach.
if not self.player.canAfford(info["cost"]):
unavailable[len(options)] = "not enough money"
if owned.get(typeId, 0) > 0:
options.append(
"Sell a %s (+$%d)" % (info["name"], info["resaleValue"])
Expand All @@ -205,7 +218,9 @@ def manageInvestments(self):
actions.append(("back", None))

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

Expand Down
56 changes: 46 additions & 10 deletions src/location/docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
REACTION_BASE_WINDOW = 2.0
ROD_WINDOW_STEP = 0.5

# Energy spent per hour of fishing, and so the minimum needed to cast at all.
FISHING_ENERGY_COST = 10


# @author Daniel McCoy Stephenson
class Docks:
Expand Down Expand Up @@ -103,6 +106,14 @@ def run(self):
# a new one shows up. Quit stays last.
li = ["Fish"]
actions = ["fish"]
# Fishing is the one option here the player can be blocked from, and
# running out of energy is the commonest way a day ends - so say so on
# the option itself rather than only after they've picked it.
unavailable = {}
if not self.player.hasEnergy(FISHING_ENERGY_COST):
unavailable[len(li)] = (
"needs %d energy - sleep at home" % FISHING_ENERGY_COST
)
for feature, label, action in (
(progression.SHOP, "Go to Shop", "shop"),
(progression.HOME, "Go Home", "home"),
Expand Down Expand Up @@ -142,13 +153,13 @@ def run(self):
notices = boats.needsAttention(self.player)
if notices:
descriptor += "\n\nNeeds attention: " + "; ".join(notices) + "."
input = self.userInterface.showOptions(descriptor, li)
input = self.userInterface.showOptions(descriptor, li, unavailable)

choice = int(input)
action = actions[choice - 1]

if action == "fish":
if self.player.hasEnergy(10):
if self.player.hasEnergy(FISHING_ENERGY_COST):
self.fish()
return LocationType.DOCKS
else:
Expand Down Expand Up @@ -372,9 +383,13 @@ def manageFleet(self):
options = []
actions = []

unavailable = {}

starter = business.tierInfo(1)
options.append("Buy a %s ($%d)" % (starter["name"], starter["cost"]))
actions.append("buy_boat")
if not self.player.canAfford(starter["cost"]):
unavailable[len(options)] = "not enough money"

if self.player.boats:
berths = boats.totalCrewBerths(self.player)
Expand Down Expand Up @@ -408,7 +423,11 @@ def manageFleet(self):
options.append("Back")
actions.append("back")

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

if action == "buy_boat":
Expand All @@ -435,15 +454,20 @@ def manageFleet(self):
self.currentPrompt.text = "What would you like to do?"
return

def _pickBoat(self, prompt, candidates=None):
def _pickBoat(self, prompt, candidates=None, unavailable=None):
"""Shared boat chooser. Returns the boat, or None if the player backed
out - every fleet action needs one and they should all read the same."""
out - every fleet action needs one and they should all read the same.

unavailable maps a 1-based position in candidates to the reason that
boat can't be chosen (an unaffordable repair bill, say), so the ones
the player can't act on are greyed out rather than picked and refused.
"Back" is appended last and is never marked."""
candidates = self.player.boats if candidates is None else candidates
if not candidates:
return None
options = [boats.describeBoat(boat) for boat in candidates]
options.append("Back")
choice = int(self.userInterface.showOptions(prompt, options))
choice = int(self.userInterface.showOptions(prompt, options, unavailable))
if choice == len(options):
return None
return candidates[choice - 1]
Expand Down Expand Up @@ -556,7 +580,14 @@ def _assignCrew(self):

def _repairBoat(self):
damaged = [boat for boat in self.player.boats if boat["damage"] > 0]
boat = self._pickBoat("Which boat needs work?", damaged)
# Each hull has its own bill, so which ones are out of reach depends on
# the boat rather than on the menu as a whole.
unaffordable = {
index + 1: "repair costs $%d" % boats.repairCost(boat)
for index, boat in enumerate(damaged)
if not self.player.canAfford(boats.repairCost(boat))
}
boat = self._pickBoat("Which boat needs work?", damaged, unaffordable)
if boat is None:
return
cost = boats.repairCost(boat)
Expand All @@ -579,7 +610,12 @@ def _upgradeBoat(self):
for boat in self.player.boats
if boat["tier"] < len(business.BOAT_TIERS)
]
boat = self._pickBoat("Which boat are you upgrading?", upgradable)
unaffordable = {
index + 1: "upgrade costs $%d" % business.tierInfo(boat["tier"] + 1)["cost"]
for index, boat in enumerate(upgradable)
if not self.player.canAfford(business.tierInfo(boat["tier"] + 1)["cost"])
}
boat = self._pickBoat("Which boat are you upgrading?", upgradable, unaffordable)
if boat is None:
return
nextInfo = business.tierInfo(boat["tier"] + 1)
Expand Down Expand Up @@ -1117,10 +1153,10 @@ def fish(self):
hours = random.randint(1, 10)

# Check if player has enough energy for all hours
energy_needed = hours * 10
energy_needed = hours * FISHING_ENERGY_COST
if not self.player.hasEnergy(energy_needed):
# Fish for as many hours as energy allows
hours = self.player.energy // 10
hours = self.player.energy // FISHING_ENERGY_COST
if hours == 0:
self.currentPrompt.text = "You're too tired to fish! Go home and sleep."
return
Expand Down
11 changes: 10 additions & 1 deletion src/location/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,22 @@ def manageHome(self):
tier = housing.currentTier(self.player)
options = []
actions = []
unavailable = {}
for targetTier, targetInfo, netCost in self._availableMoves():
options.append(self._moveLabel(tier, targetTier, targetInfo, netCost))
actions.append(("move", targetTier))
# Moving down pays out and is always possible; only a move that
# costs money can be out of reach (see housing.moveHome).
if netCost > 0 and not self.player.canAfford(netCost):
unavailable[len(options)] = "not enough money"
options.append("Back")
actions.append(("back", None))

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

if action == "move":
Expand Down
18 changes: 17 additions & 1 deletion src/location/shop.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,27 @@ def run(self):
# actions are built as a pair rather than dispatched on a fixed number.
li = ["Sell Fish"]
actions = ["sell"]
# Every purchase here has a gate the player can be on the wrong side of
# (an empty hold, an empty purse, gear already maxed out), so each row
# is marked as it is added - len(li) is always the number just used.
unavailable = {}
if self.player.fishCount == 0:
unavailable[len(li)] = "you have no fish"
if progression.isUnlocked(self.stats, progression.BAIT):
li.append("Buy Better Bait ( $%.2f )" % self.player.priceForBait)
actions.append("bait")
if self.player.fishMultiplier >= MAX_FISH_MULTIPLIER:
unavailable[len(li)] = "already the best bait"
elif not self.player.canAfford(self.player.priceForBait):
unavailable[len(li)] = "not enough money"
if progression.isUnlocked(self.stats, progression.ROD):
li.append("Buy Better Rod ( $%.2f )" % rodUpgradeCost(self.player.rodLevel))
rodCost = rodUpgradeCost(self.player.rodLevel)
li.append("Buy Better Rod ( $%.2f )" % rodCost)
actions.append("rod")
if self.player.rodLevel >= MAX_ROD_LEVEL:
unavailable[len(li)] = "already the finest rod"
elif not self.player.canAfford(rodCost):
unavailable[len(li)] = "not enough money"
if progression.isUnlocked(self.stats, progression.TALK):
li.append("Talk to %s" % self.npc.name)
actions.append("talk")
Expand All @@ -222,6 +237,7 @@ def run(self):
input = self.userInterface.showOptions(
"The shopkeeper winks at you as you behold his collection of fishing poles.",
li,
unavailable,
)
action = actions[int(input) - 1]

Expand Down
25 changes: 21 additions & 4 deletions src/location/tavern.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
DRUNK_LOSS_CHANCE = 0.3
DRUNK_TIP_CHANCE = 0.3

# What a night's drinking costs. Named so the menu label, the affordability
# check and the charge itself can't drift apart.
DRINK_COST = 10


# @author Daniel McCoy Stephenson
class Tavern:
Expand Down Expand Up @@ -145,21 +149,26 @@ def run(self):
# Options and actions are built as a pair rather than dispatched on a
# fixed number, because talking to Old Tom is revealed separately from
# the tavern itself (see src/progression).
li = ["Get drunk ( $10 )", "Gamble"]
li = ["Get drunk ( $%d )" % DRINK_COST, "Gamble"]
actions = ["drink", "gamble"]
unavailable = {}
if not self.player.canAfford(DRINK_COST):
unavailable[1] = "not enough money"
if progression.isUnlocked(self.stats, progression.TALK):
li.append("Talk to %s" % self.npc.name)
actions.append("talk")
li.append("Go to Docks")
actions.append("docks")

input = self.userInterface.showOptions(
"You sit at the bar, watching the barkeep clean a mug with a dirty rag.", li
"You sit at the bar, watching the barkeep clean a mug with a dirty rag.",
li,
unavailable,
)
action = actions[int(input) - 1]

if action == "drink":
if self.player.canAfford(10):
if self.player.canAfford(DRINK_COST):
self.getDrunk()
return LocationType.HOME
else:
Expand All @@ -182,7 +191,7 @@ def run(self):
return LocationType.DOCKS

def getDrunk(self):
self.player.spendMoney(10)
self.player.spendMoney(DRINK_COST)

self.userInterface.showBusy("You drink your way into the evening...", 3)

Expand Down Expand Up @@ -222,12 +231,20 @@ def getDrunk(self):
def gamble(self):
while True:
li = ["1", "2", "3", "4", "5", "6", "Change Bet", "Back"]
# Nothing rides on a face until money is on the table, so the six
# faces are greyed out until the player has set a bet - Change Bet
# and Back stay live, and they're the only way forward anyway.
unavailable = {}
if self.currentBet <= 0:
for face in range(1, 7):
unavailable[face] = "place a bet first"
input = int(
self.userInterface.showOptions(
"Once you place your bet, the burly man in front of you will throw the dice. "
"Guess the number right and he pays out %dx your bet."
% DICE_WIN_MULTIPLIER,
li,
unavailable,
)
)

Expand Down
Loading
Loading