Skip to content

Add Home Ownership (trade-up) and Investment Properties#104

Merged
dmccoystephenson merged 2 commits into
mainfrom
feature/home-ownership
Jul 11, 2026
Merged

Add Home Ownership (trade-up) and Investment Properties#104
dmccoystephenson merged 2 commits into
mainfrom
feature/home-ownership

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

  • Home (src/housing): a trade-up model. From the Home menu's "Manage Home", moving to a different tier (Driftwood Shack → Cozy Cottage → Sturdy Cabin → Waterfront Manor) sells your current home and buys the target one — net cost is the price difference, so upgrading costs money and downgrading pays cash back. The only benefit is personal comfort (higher energy cap on sleep); there's no rental income, since you live there.
  • Investment Properties (new src/investments module): rental units the player owns but doesn't live in — any number of units, of any mix of 3 types (Dockside Cottage, Fisherman's Rowhouse, Harborview Flat), bought/sold individually. Each owned unit pays its own daily rental income automatically during TimeService.increaseDay(), mirroring how the fishing business' hired crew works (src/business) — you don't personally staff it, but it earns. Managed from the Bank ("Manage Investment Properties"), fitting Margaret the Teller's role.
  • Two new milestone pairs: Homeowner/Waterfront Manor (home tier progress) and Landlord/Property Mogul (lifetime properties bought).
  • player.homeTier, player.rentalProperties, and the new Stats fields follow the codebase's existing .get(key, default) backwards-compatible save/load pattern; schemas/player.json and schemas/stats.json are updated to match.

This replaces an earlier version of this PR where upgrading your own home paid you rental income (didn't make sense — you live there) and tiers only stacked with no way to sell back. Splitting "the place you live" (comfort, trade-up economics) from "assets you rent out" (pure income, buy/sell) fixes both.

Test plan

  • python3 -m pytest --verbose -vv --cov=src --cov-report=term-missing — 293 passed (up from 254 on main), including tests/housing/test_housing.py (rewritten for the trade-up model), new tests/investments/test_investments.py, and updates to tests/location/test_home.py, tests/location/test_bank.py, tests/player/*, tests/stats/*, tests/world/test_timeService.py.
  • Manual smoke test via direct API calls: moved a home from tier 1 → 2 → 1 (confirmed $750 net cost up, $525 cash back down, highestHomeTier doesn't regress); bought 2 Dockside Cottages + 1 Fisherman's Rowhouse, confirmed daily income ($75/day) accrues on increaseDay(), then sold one Cottage for its resale value.
  • Verified tier-1 home defaults (free, 100 energy) match pre-existing behavior exactly, so existing saves are unaffected until a player moves.

🤖 Generated with Claude Code

dmccoystephenson and others added 2 commits July 11, 2026 20:24
…usiness

The player's home has always been a static free location. This adds tiered
home ownership (Driftwood Shack -> Cozy Cottage -> Sturdy Cabin -> Waterfront
Manor), managed from a new "Manage Home" option in the Home menu. Each
upgrade costs money and raises the energy cap restored on sleep, plus adds a
small passive daily rental income paid out alongside bank interest and crew
wages during TimeService.increaseDay().

Mirrors the existing src/business tier pattern (data-driven tier list,
currentTier/tierInfo resolvers) as its own module, src/housing, rather than
folding into business - the two ownership tracks are independent. Tier 1 is
free with today's numbers (100 energy, no income), so existing saves and
default-player behavior are unchanged until a player upgrades.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Address feedback that the original design didn't make sense: paying rental
income on the house you live in, and stacking home tiers with no way to sell
back.

- src/housing (the player's own home) is now a trade-up model: moving to a
  different tier sells the current home and buys the target one, so the net
  cost is the price difference (upgrading costs money, downgrading pays cash
  back). No more rental income - the only benefit is personal comfort (a
  higher energy cap on sleep), since you live there.
- New src/investments module covers rental units the player owns but doesn't
  live in - any number of properties, of any mix of three types, bought and
  sold individually, each paying its own daily rental income automatically
  (the same "you don't personally staff it, but it earns" idea already used
  for the fishing business' hired crew). Managed from the Bank ("Manage
  Investment Properties"), fitting Margaret the Teller's role.
- stats.totalRentalIncome now tracks investment income specifically;
  stats.totalPropertiesBought backs two new milestones (Landlord, Property
  Mogul), alongside the existing home-tier milestones (Homeowner, Waterfront
  Manor).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dmccoystephenson dmccoystephenson changed the title Add Home Ownership as a second property track Add Home Ownership (trade-up) and Investment Properties Jul 11, 2026
@dmccoystephenson dmccoystephenson marked this pull request as ready for review July 11, 2026 22:41
@dmccoystephenson dmccoystephenson merged commit 9eb0fa3 into main Jul 11, 2026
1 check passed
@dmccoystephenson dmccoystephenson deleted the feature/home-ownership branch July 11, 2026 22:41
dmccoystephenson added a commit that referenced this pull request Jul 11, 2026
…g stack

Multi-angle review of the home-ownership/investments/homeless-and-renting
stack (PRs #104, #105) turned up several real issues, all fixed here:

Correctness:
- Player.__init__ hardcoded energy=100, but a fresh player's homeTier=0
  (Homeless) caps at 60 - new players started 40 energy over their own cap.
  Now derived from housing.HOUSING_TIERS[0]["maxEnergy"].
- moveHome() changed player.homeTier but never reclamped player.energy to
  the new (possibly lower) cap, so downgrading or being evicted left the
  player over-cap until their next sleep. Eviction is now routed through
  moveHome() (instead of duplicating the tier assignment inline), so it
  gets this reconciliation automatically.
- investments.typeInfo()/housing.tierInfo() silently wrapped around via
  Python's negative-index behavior on an out-of-range id instead of failing
  loudly; both now raise ValueError, and schemas/player.json gained the
  matching maximum bounds on homeTier/rentalProperties.

UX (eviction was completely silent - the player could lose their room and
never be told):
- TimeService.increaseDay()/increaseTime() now return {"evicted": bool}.
  Every place a day can roll over (Home.sleep, Tavern.getDrunk, a multi-hour
  Docks.fish trip, and the main game loop as a catch-all for everything
  else) surfaces housing.EVICTION_MESSAGE when it happens.
- "Move to Rented Room (free)" was misleading - it now discloses the
  recurring $10/day rent up front, before the player commits.
  "+$X" cash-back downgrade label is now "get $X back".
- Energy is now shown as "current/cap" (not just current) in all three
  front-ends (console, pygame, web), so the housing energy-cap benefit is
  visible without opening Manage Home.

Cleanup:
- netCostToMove() simplified (dict.get defaults already encode "only owned
  tiers have a cost/resaleValue", the status-gated ternaries were redundant).
- home.py's repeated adjacent-tier-move computation (built separately in
  _housingStatus() and manageHome()) is now computed once via
  _availableMoves().
- manageInvestments() combined its two PROPERTY_TYPES loops (buy options,
  then sell options) into one pass, backed by a new investments.ownedCounts()
  that computes the whole portfolio's counts in one pass instead of an
  O(types) list.count() scan per type per render.

Deliberately not changed (flagged in review, judged not worth the churn):
menu-loop duplication across manageHome/manageInvestments/manageBusiness,
investments' 1-based vs housing's 0-based tier indexing, and
displayStats()'s hand-rolled optional-block style vs achievements.py's
data-driven MILESTONES pattern.

Stacked on feature/homeless-and-renting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dmccoystephenson added a commit that referenced this pull request Jul 11, 2026
…g stack (#106)

Multi-angle review of the home-ownership/investments/homeless-and-renting
stack (PRs #104, #105) turned up several real issues, all fixed here:

Correctness:
- Player.__init__ hardcoded energy=100, but a fresh player's homeTier=0
  (Homeless) caps at 60 - new players started 40 energy over their own cap.
  Now derived from housing.HOUSING_TIERS[0]["maxEnergy"].
- moveHome() changed player.homeTier but never reclamped player.energy to
  the new (possibly lower) cap, so downgrading or being evicted left the
  player over-cap until their next sleep. Eviction is now routed through
  moveHome() (instead of duplicating the tier assignment inline), so it
  gets this reconciliation automatically.
- investments.typeInfo()/housing.tierInfo() silently wrapped around via
  Python's negative-index behavior on an out-of-range id instead of failing
  loudly; both now raise ValueError, and schemas/player.json gained the
  matching maximum bounds on homeTier/rentalProperties.

UX (eviction was completely silent - the player could lose their room and
never be told):
- TimeService.increaseDay()/increaseTime() now return {"evicted": bool}.
  Every place a day can roll over (Home.sleep, Tavern.getDrunk, a multi-hour
  Docks.fish trip, and the main game loop as a catch-all for everything
  else) surfaces housing.EVICTION_MESSAGE when it happens.
- "Move to Rented Room (free)" was misleading - it now discloses the
  recurring $10/day rent up front, before the player commits.
  "+$X" cash-back downgrade label is now "get $X back".
- Energy is now shown as "current/cap" (not just current) in all three
  front-ends (console, pygame, web), so the housing energy-cap benefit is
  visible without opening Manage Home.

Cleanup:
- netCostToMove() simplified (dict.get defaults already encode "only owned
  tiers have a cost/resaleValue", the status-gated ternaries were redundant).
- home.py's repeated adjacent-tier-move computation (built separately in
  _housingStatus() and manageHome()) is now computed once via
  _availableMoves().
- manageInvestments() combined its two PROPERTY_TYPES loops (buy options,
  then sell options) into one pass, backed by a new investments.ownedCounts()
  that computes the whole portfolio's counts in one pass instead of an
  O(types) list.count() scan per type per render.

Deliberately not changed (flagged in review, judged not worth the churn):
menu-loop duplication across manageHome/manageInvestments/manageBusiness,
investments' 1-based vs housing's 0-based tier indexing, and
displayStats()'s hand-rolled optional-block style vs achievements.py's
data-driven MILESTONES pattern.

Stacked on feature/homeless-and-renting.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant