Skip to content

Hire villagers as crew, unlocking new dialogue - #136

Merged
dmccoystephenson merged 2 commits into
mainfrom
feature/hire-villagers-as-crew
Aug 1, 2026
Merged

Hire villagers as crew, unlocking new dialogue#136
dmccoystephenson merged 2 commits into
mainfrom
feature/hire-villagers-as-crew

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

Crew slots used to be an anonymous headcount — "Hire a Worker" just incremented player.workers. Hiring is now a choice of which villager joins the outfit, and every hire unlocks conversations the player couldn't have before.

  • A named workforce. src/npc/villagers.py adds a 12-villager roster, each with a backstory, a specialty, and a one-line blurb shown next to the wage in the hire menu. The roster is deliberately at least as long as the largest boat's crew capacity, so every berth on a Fishing Fleet can be filled by name.
  • Conditional dialogue. NPC dialogue options now accept an optional "condition" — a zero-arg callable — and stay hidden while it returns False. get_dialogue_response indexes the available options, i.e. the same list the front-ends number their menus from, so a hidden option can never shift a response onto the wrong question.
  • Talk to Your Crew. A new docks option appears as soon as anyone is aboard. Each crew member answers about their work, the boat, and whether wages are turning up on time, with three further questions that unlock on a full crew, a named business, and a Fishing Fleet respectively.
  • The villagers you already know react. Sam sizes up who you hired (naming them and their specialty), Gilbert notices them spending wages across his counter, and Old Tom relays what they say at the bar — including a warning when payroll is short. All three are gated behind having hired anyone.
  • Persistence. player.hiredWorkers round-trips through PlayerJsonReaderWriter and schemas/player.json. workers stays the authoritative headcount the production maths runs on, so a save written before crews had names loads with its crew intact — those hands are just unnamed, and are dismissed (or quit over unpaid wages) before any named villager is.
  • business.hireWorker / dismissWorker centralise the roster/headcount invariant; sellBoat and the unpaid-wages quit path keep the two in sync, and runDailyProduction's summary now reports quitNames.

Test plan

  • python3 -m compileall -q src tests
  • SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python3 -m pytest --verbose -vv --cov=src --cov-report=term-missing --cov-report=xml:cov.xml — 463 passed. New/changed lines are fully covered (src/npc/villagers.py 100%, src/npc/npc.py 100%, src/player/* 100%, src/location/docks.py 98% with only pre-existing fish() branches uncovered).
  • black + autoflake run over the changed files only (unrelated files the formatter also touches were left alone to keep the diff readable).
  • Front-end parity. Everything new goes through the shared primitives — showOptions, showDialogue, showInteractiveDialogue — so all three front-ends are covered: pygame and web inherit BaseUserInterface.showInteractiveDialogue, and the console overrides it. Both paths re-read get_dialogue_options() each turn, and each has its own test that a conditional option is hidden until unlocked (tests/ui/test_baseUserInterface.py, tests/ui/test_userInterface.py).
  • Played the hire → talk → dismiss flow manually through the real console front-end.

Docs

README.md's Fishing Business section was rewritten to describe hiring by name and the dialogue it unlocks. PLANNING.md and the other schemas were checked and needed no change.

dmccoystephenson and others added 2 commits August 1, 2026 06:43
Crew slots used to be an anonymous headcount. Hiring is now a choice of
which villager joins the outfit, and each hire opens conversations the
player couldn't have before.

- Add a 12-villager roster (src/npc/villagers.py), each with a backstory,
  a specialty, and a hire-menu blurb; the roster is at least as long as
  the largest boat's crew so every berth can be filled by name
- Give NPC dialogue options an optional "condition" callable so questions
  can unlock with game state; get_dialogue_response now indexes the same
  filtered list the front-ends number their menus from
- Add "Talk to Your Crew" at the docks, with per-crew-member dialogue that
  reflects the boat, the wages, and further questions that unlock on a
  full crew / named business / Fishing Fleet
- Unlock a crew question on Sam, Gilbert, and Old Tom once anyone is hired
- Persist the crew by name (player.hiredWorkers) through
  PlayerJsonReaderWriter and schemas/player.json; saves without the field
  keep their headcount as unnamed hands

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Reword the Talk to Your Crew prompt so it fits one line (the formatter
  had split it into adjacent string literals)
- Put the off-roster hand's backstory in first person, matching every
  roster entry - it's read back as "<name>: <backstory>"
- Drop an unused local in Gilbert's crew-customer dialogue

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@dmccoystephenson dmccoystephenson left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review (no configured reviewer on this repo). Read the full diff back; the suite is green at 463 tests with the new modules fully covered. Three notes below on deliberate choices a reader might otherwise flag, plus one behaviour worth knowing about. Nothing here blocks merge.

Comment thread src/npc/npc.py
being fixed at NPC-construction time."""
if 0 <= option_index < len(self.dialogue_options):
response = self.dialogue_options[option_index].get("response", "")
options = self.get_dialogue_options()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the one behavioural change to an existing contract: get_dialogue_response used to index self.dialogue_options directly and now indexes the filtered list. That's required for correctness — every front-end numbers its menu from get_dialogue_options(), so with a hidden option present the old indexing would return the wrong answer. For any NPC without conditions the two lists are identical, so existing callers are unaffected.

Comment thread src/location/docks.py
self.manageBusiness()
return LocationType.DOCKS

elif input == "8" and self.player.hiredWorkers:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The and self.player.hiredWorkers guard is redundant today — all three front-ends validate the choice against the option list they were handed (console loops on a mismatch, web rejects anything outside valid, pygame bounds-checks the number keys and wraps the arrow selection), so "8" can only come back when the option was actually offered. Kept anyway so the branch can't fire against an empty crew if a future front-end is looser about it.

Comment thread src/location/docks.py
# Appended (rather than slotted in next to the other "Talk to" option)
# so the numbering of everything above stays put whether or not the
# player has anyone to talk to.
if self.player.hiredWorkers:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth knowing: this gates on hiredWorkers, not workers. A save from before this change loads with a crew but no names, so those players see no "Talk to Your Crew" until they hire someone new — at which point the option appears and Sam's line mentions the unnamed hands alongside the named one. That's the intended read of an old save rather than an oversight; the alternative (backfilling names onto existing headcount) would invent history the save never had.

Comment thread src/business/business.py


def _trimCrewRoster(player):
"""Drop named crew until the roster fits the headcount, and return the

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_trimCrewRoster is the single place the roster/headcount invariant (len(hiredWorkers) <= workers) is enforced, called from dismissWorker and the unpaid-wages path in runDailyProduction. Popping from the tail gives the desired ordering for free: unnamed legacy hands are absorbed by the headcount drop before any named villager is asked to leave, and among named ones the most recent hire goes first.

@dmccoystephenson
dmccoystephenson merged commit fcd1311 into main Aug 1, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the feature/hire-villagers-as-crew branch August 1, 2026 07:02
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