From 2bec24859af8f03c80a0a58e3240519942450167 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 19 Jun 2026 18:33:43 -0400 Subject: readme --- skill_plans/hacking.md | 1391 ------------------------------------------------ 1 file changed, 1391 deletions(-) delete mode 100644 skill_plans/hacking.md (limited to 'skill_plans/hacking.md') diff --git a/skill_plans/hacking.md b/skill_plans/hacking.md deleted file mode 100644 index 4559209..0000000 --- a/skill_plans/hacking.md +++ /dev/null @@ -1,1391 +0,0 @@ -# Hacking Skill — Implementation Plan - -## 1. Overview - -Hacking is the Hunter analog for The House of Icarus. Instead of setting traps and catching creatures in the wilderness, players "jack in" to terminal objects scattered around the game world and play text-based minigames to train their Hacking skill. - -The core loop: -1. Player finds a terminal object in a room (e.g., `terminal_basic` in the Hacking Lab). -2. Player types `jack` or `jack ` to jack into it. -3. The session enters `StateHacking` — all further input is routed to the active minigame handler instead of normal command dispatch. -4. Player plays the minigame (Hunt the Wumpus, Mastermind, Liar's Dice, etc.). -5. On completion (win or lose), Hacking XP is awarded and the player returns to `StateGame`. -6. Player can type `jack out` or `quit` at any time to abort (small consolation XP for attempt if progress was made). - -Different terminals require different Hacking levels and offer different minigames. Higher-level terminals offer harder minigames with more XP. - -## 2. Architecture - -### Session State - -Add `StateHacking` to the `SessionState` enum in `internal/net/server.go`: - -```go -// In the const block, after StateColorChoice: -StateHacking -``` - -When `sess.State == net.StateHacking`, `HandleSession` in `internal/game/game.go` routes all input to `g.handleHackingInput(sess, input)`: - -```go -// In HandleSession switch: -case net.StateHacking: - g.handleHackingInput(sess, input) -``` - -### Hacking State Storage - -Add a `hackingStates` map to the `Game` struct: - -```go -type Game struct { - // ... existing fields ... - hackingStates map[string]*HackingSession // keyed by player name -} -``` - -Initialize it in `New()`: - -```go -hackingStates: make(map[string]*HackingSession), -``` - -The `HackingSession` struct: - -```go -type HackingSession struct { - Minigame HackingMinigame - TerminalID string // object def ID (e.g., "terminal_basic") - Level int // player's hacking level at start - ReqLevel int // terminal's required level - Started bool // true once first input received (for attempt XP) -} -``` - -### ActionState - -Add `ActionHacking` to `action_state.go`: - -```go -ActionHacking ActionType = "hacking" -``` - -Add a case in `Description()`: - -```go -case ActionHacking: - return "jacked into a " + a.TargetName -``` - -### Cleanup - -In `Game.SetHub`'s `OnRemove` callback (or wherever disconnect cleanup happens), delete the hacking state: - -```go -delete(g.hackingStates, p.Name) -``` - -Also in `CancelAction`, if the player is in StateHacking, clean up: - -```go -if _, ok := g.hackingStates[p.Name]; ok { - delete(g.hackingStates, p.Name) - sess.State = net.StateGame -} -``` - -## 3. Commands - -### `jack` / `jackin` (Active command) - -Add to `classifyCommand()` in `game.go`: - -```go -case "jack", "jackin": - return ClassActive -``` - -Add to `executeCommand()` in `game.go`: - -```go -case "jack", "jackin": - g.CancelAction(p) - g.doJack(sess, strings.Join(args, " ")) - return -``` - -### In-Minigame Input - -While in `StateHacking`, ALL input goes to `handleHackingInput()`. The minigame's `HandleInput()` method processes it. Special pre-check: - -- If input is `jack out`, `quit`, or `disconnect`: exit the minigame, award attempt XP if `Started == true`, restore `StateGame`, write prompt. -- Otherwise: pass to `Minigame.HandleInput(input)`. - -### No Queuing While Hacking - -While `sess.State == net.StateHacking`, the player cannot queue commands. Input does not go through `handleGameCommand` at all. This is the same pattern as `StateTalk`, `StateRecipeChoice`, etc. - -## 4. New Files to Create - -### Go Files (all in `internal/game/`) - -| File | Purpose | -|---|---| -| `cmd_jack.go` | `doJack()` command handler — find terminal, check level, start minigame | -| `hacking.go` | `HackingMinigame` interface, `HackingSession` struct, `handleHackingInput()`, `startHacking()`, `endHacking()`, XP calculation | -| `hacking_wumpus.go` | Hunt the Wumpus minigame implementation | -| `hacking_mastermind.go` | Mastermind / Code Breaker minigame implementation | -| `hacking_liars_dice.go` | Liar's Dice / Signal Bluff minigame implementation | - -### YAML Data Files - -| File | Purpose | -|---|---| -| `data/objects/terminal_basic.yaml` | Level 1 terminal (Wumpus) | -| `data/objects/terminal_advanced.yaml` | Level 20 terminal (Mastermind) | -| `data/objects/terminal_secure.yaml` | Level 40 terminal (Liar's Dice) | -| `data/behaviors/netrunner_talk.yaml` | Netrunner NPC talk behavior | -| `data/objects/netrunner.yaml` | Netrunner NPC object | -| `data/help/jack.yaml` | Help for the jack command | -| `data/help/hacking.yaml` | Help for the hacking skill | -| `data/rooms/8.yaml` | Updated Hacking Lab with terminal + NPC | - -## 5. Code Changes to Existing Files - -### `internal/net/server.go` - -Add `StateHacking` to the `SessionState` const block: - -```go -const ( - // ... existing states ... - StateColorChoice - StateHacking // <-- add after StateColorChoice -) -``` - -No changes to the `Session` struct needed — hacking state is stored on `Game.hackingStates`, not on the session. - -### `internal/game/game.go` - -**HandleSession** — add case: - -```go -case net.StateHacking: - g.handleHackingInput(sess, input) -``` - -This goes after the `StateColorChoice` case, before the closing `}`. - -**classifyCommand** — add: - -```go -case "jack", "jackin": - return ClassActive -``` - -Add `"jack", "jackin"` to the active commands list in the switch (the line with `"get", "take", "grab", "pick", "drop", ...`). - -**executeCommand** — add case: - -```go -case "jack", "jackin": - g.CancelAction(p) - g.doJack(sess, strings.Join(args, " ")) - return -``` - -**New()** — add initialization: - -```go -hackingStates: make(map[string]*HackingSession), -``` - -**Game struct** — add field: - -```go -hackingStates map[string]*HackingSession -``` - -### `internal/game/action_state.go` - -Add constant: - -```go -ActionHacking ActionType = "hacking" -``` - -Add Description case: - -```go -case ActionHacking: - return "jacked into a " + a.TargetName -``` - -## 6. Terminal Objects - -### `data/objects/terminal_basic.yaml` - -```yaml -id: terminal_basic -name: basic terminal -color: "40" -description: "A battered terminal with a cracked screen. Faded text scrolls across the display — it looks like it's running some kind of legacy intrusion detection system. {40}[Level 1 Hacking]{/}" -inroom_description: "A {40}basic terminal{/} hums quietly against the wall." -``` - -No `behavior` field — the `jack` command handles terminal interaction directly by checking object ID prefix `terminal_`. - -### `data/objects/terminal_advanced.yaml` - -```yaml -id: terminal_advanced -name: advanced terminal -color: "33" -description: "A sleek terminal with a holographic display. Encrypted data streams flow across the screen in complex patterns. {33}[Level 20 Hacking]{/}" -inroom_description: "An {33}advanced terminal{/} projects a holographic interface." -``` - -### `data/objects/terminal_secure.yaml` - -```yaml -id: terminal_secure -name: secure terminal -color: "196" -description: "A heavily reinforced terminal embedded in the wall. Warning glyphs pulse across its surface. A bluffing protocol runs on loop — it's probing you as much as you're probing it. {196}[Level 40 Hacking]{/}" -inroom_description: "A {196}secure terminal{/} glows with warning indicators." -``` - -### Terminal Registry (in `hacking.go`) - -Rather than using YAML behaviors, terminals are registered in a Go map. This keeps minigame logic in Go (minigames are complex stateful Go code, not YAML-driven): - -```go -type TerminalDef struct { - ObjectID string - Level int - Minigame func() HackingMinigame - BaseXPWin int - BaseXPLose int - Name string -} - -var terminalDefs = map[string]TerminalDef{ - "terminal_basic": { - ObjectID: "terminal_basic", - Level: 1, - Minigame: func() HackingMinigame { return NewWumpusGame() }, - BaseXPWin: 50, - BaseXPLose: 10, - Name: "Hunt the Wumpus", - }, - "terminal_advanced": { - ObjectID: "terminal_advanced", - Level: 20, - Minigame: func() HackingMinigame { return NewMastermindGame() }, - BaseXPWin: 150, - BaseXPLose: 30, - Name: "Code Breaker", - }, - "terminal_secure": { - ObjectID: "terminal_secure", - Level: 40, - Minigame: func() HackingMinigame { return NewLiarsDiceGame() }, - BaseXPWin: 300, - BaseXPLose: 50, - Name: "Signal Bluff", - }, -} -``` - -## 7. Minigame Interface - -Defined in `internal/game/hacking.go`: - -```go -type HackingMinigame interface { - Init(level int) string - HandleInput(input string) (output string, done bool, won bool) - Name() string -} -``` - -### `Init(level int) string` - -Called once when the minigame starts. `level` is the player's current Hacking level (may influence difficulty scaling in future). Returns the initial display string — the welcome message, instructions, and initial game state. - -### `HandleInput(input string) (output string, done bool, won bool)` - -Called for every line of player input while in the minigame. - -- `output`: Text to display to the player (game response, updated state, etc.) -- `done`: If `true`, the minigame is over. `won` indicates win/lose. -- `won`: Only meaningful when `done == true`. `true` = player completed the objective. - -### `Name() string` - -Returns the display name of the minigame (e.g., "Hunt the Wumpus"). - -## 8. Hunt the Wumpus (Level 1) - -File: `internal/game/hacking_wumpus.go` - -### Theme Reskin - -| Classic | Sci-Fi | -|---|---| -| Cave | Network node | -| Wumpus | Rogue AI | -| Pit | Data trap (firewall) | -| Super bats | ICE (Intrusion Countermeasures Electronics) | -| Arrows | Probes | -| Shoot | Launch probe | - -### Struct - -```go -type WumpusGame struct { - rooms [20][3]int // adjacency list (dodecahedron) - player int // current room (0-19) - wumpus int // wumpus room - pits [2]int // pit rooms - ice [2]int // ICE rooms - probes int // remaining probes (starts at 5) - gameOver bool - won bool -} -``` - -### Dodecahedron Topology - -The classic 20-room dodecahedron. Each room connects to exactly 3 others. Hard-coded adjacency table (same as the original 1973 Hunt the Wumpus): - -```go -var dodecahedron = [20][3]int{ - {1, 4, 7}, // room 0 - {0, 2, 9}, // room 1 - {1, 3, 11}, // room 2 - {2, 4, 13}, // room 3 - {0, 3, 5}, // room 4 - {4, 6, 14}, // room 5 - {5, 7, 16}, // room 6 - {0, 6, 8}, // room 7 - {7, 9, 17}, // room 8 - {1, 8, 10}, // room 9 - {9, 11, 18}, // room 10 - {2, 10, 12}, // room 11 - {11, 13, 19}, // room 12 - {3, 12, 14}, // room 13 - {5, 13, 15}, // room 14 - {14, 16, 19}, // room 15 - {6, 15, 17}, // room 16 - {8, 16, 18}, // room 17 - {10, 17, 19}, // room 18 - {12, 15, 18}, // room 19 -} -``` - -### Initialization (`Init`) - -1. Randomly place: player, wumpus, 2 pits, 2 ICE — all in distinct rooms. -2. Set `probes = 5`. -3. Return welcome text + initial room description. - -Welcome text: -``` -=== HUNT THE ROGUE AI === - -You've jacked into an abandoned network. Somewhere in this maze of -20 nodes, a rogue AI lurks. You have 5 probes to find and neutralize it. - -Hazards: - - Rogue AI: Moves to an adjacent node if you enter its node. Kills you. - - Data Traps: Fall in and you're fried. 2 in the network. - - ICE: Grabs you and dumps you in a random node. 2 in the network. - -Commands: - move - Move to an adjacent node (1-20) - shoot - Launch a probe into an adjacent node (1-20) - status - Show your current status - map - Show network map - jack out - Disconnect (forfeit) -``` - -### Room Description - -On entering a room (or at start), display: - -``` ---- Node 7 --- -Tunnels lead to: 1, 7, 9 -``` - -Then hazard warnings for adjacent rooms: -- Wumpus adjacent: `"You detect corrupted data nearby..."` -- Pit adjacent: `"You sense a void in the network..."` -- ICE adjacent: `"You hear static crackling..."` - -### Commands - -**`move `** (or just `` as shorthand for move): - -1. Validate N is 1-20 and adjacent to current room (display uses 1-indexed, internal is 0-indexed). -2. Move player to room N-1. -3. Check hazards in new room: - - **Wumpus room**: Wumpus wakes up and moves to a random adjacent room (75% chance) or stays and kills player (25% chance). If it moves, player survives and sees "The rogue AI stirs and relocates..." + new warnings. - - **Pit room**: Player dies. "You've fallen into a data trap! Your connection is severed." `done=true, won=false`. - - **ICE room**: Player teleported to a random room. "ICE detected! You're relocated to a random node..." Then check the new room for hazards recursively (can chain into pit or wumpus). -4. Show new room description. - -**`shoot `** (or `probe `, `launch `): - -1. Validate N is 1-20 and adjacent to current room. -2. Decrement probes. -3. If wumpus is in room N-1: "Your probe hits the rogue AI! It destabilizes and crashes." `done=true, won=true`. -4. If miss: "Your probe finds nothing in node N." Wumpus is startled and moves to a random adjacent room (75% chance). "You hear something shift in the network..." -5. If probes == 0: "You're out of probes. The rogue AI detects your presence and terminates your connection." `done=true, won=false`. - -**`status`**: -``` -Node: 7 | Probes: 4/5 -``` - -**`map`**: - -Display the full 20-node dodecahedron as a text map. Mark the player's current position with `[*]` and visited nodes with `[.]`. Unknown nodes shown as `[ ]`. - -Simple list format (since drawing a dodecahedron in ASCII is complex): -``` -=== Network Map === - [*] Node 7 -> 1, 7, 9 - [ ] Node 1 -> 1, 5, 8 - ... -``` - -Only show nodes the player has visited plus their current adjacencies. Unvisited non-adjacent nodes shown as `[ ] Node N -> ?, ?, ?`. - -### Win/Lose - -- **Win** (shot the wumpus): Award `BaseXPWin` (50 XP). -- **Lose** (fell in pit, eaten by wumpus, out of probes): Award `BaseXPLose` (10 XP). -- **Quit** (jack out before completion): Award `BaseXPLose` only if `Started == true` (player made at least one move/shot). - -### Constructor - -```go -func NewWumpusGame() *WumpusGame { - return &WumpusGame{} -} -``` - -The `Init` method does all setup using `math/rand`. - -## 9. Mastermind / Code Breaker (Level 20) - -File: `internal/game/hacking_mastermind.go` - -### Theme Reskin - -| Classic | Sci-Fi | -|---|---| -| Colored pegs | Cipher digits (1-6) | -| Code maker | Encryption module | -| Black pegs (exact) | Exact matches (locked) | -| White pegs (color match) | Partial matches (found) | - -### Struct - -```go -type MastermindGame struct { - code [4]int // secret code, digits 1-6, no repeats - guesses []MastermindGuess - maxGuess int // 10 - gameOver bool - won bool -} - -type MastermindGuess struct { - Digits [4]int - Exact int // digits in correct position - Partial int // correct digit, wrong position -} -``` - -### Initialization (`Init`) - -1. Generate a random 4-digit code using digits 1-6, no repeats. -2. Set `maxGuess = 10`. -3. Return welcome text. - -Welcome text: -``` -=== CODE BREAKER === - -The encryption module is running a 4-digit cipher using symbols 1-6. -No symbol repeats. You have 10 attempts to crack the code. - -After each guess, you'll see: - [X] = correct symbol in correct position - [O] = correct symbol in wrong position - [ ] = symbol not in code - -Commands: - <4 digits> - Guess the code (e.g., 1234) - status - Show previous guesses - jack out - Disconnect (forfeit) -``` - -### Commands - -**`<4 digits>`** (e.g., `1234`, `5621`): - -1. Validate: exactly 4 characters, each 1-6, no repeats. If invalid, show error and don't consume a guess. -2. Calculate exact matches and partial matches: - - Exact: digit is correct and in correct position. - - Partial: digit is in the code but wrong position. -3. Display the guess with feedback: - ``` - Attempt 3/10: 1 2 3 4 -> [X][ ][O][ ] (1 locked, 1 found) - ``` -4. If exact == 4: "Code cracked! The encryption module yields." `done=true, won=true`. -5. If guesses exhausted: "Out of attempts. The code was: 5 2 6 1. Connection terminated." `done=true, won=false`. - -**`status`**: - -Display all previous guesses with their feedback: -``` -=== Attempts === - 1: 1 2 3 4 -> [X][ ][O][ ] (1 locked, 1 found) - 2: 1 5 6 3 -> [X][O][ ][ ] (1 locked, 1 found) -Remaining: 8/10 -``` - -### Win/Lose + Bonus - -- **Win**: `BaseXPWin` (150 XP) + bonus for fewer guesses: `bonusXP = (maxGuess - guessesUsed) * 15`. Solving in 1 guess = 150 + 135 = 285 XP. Solving in 10 = 150 + 0 = 150 XP. -- **Lose**: `BaseXPLose` (30 XP). -- **Quit**: `BaseXPLose` if at least 1 guess was made. - -### Constructor - -```go -func NewMastermindGame() *MastermindGame { - return &MastermindGame{} -} -``` - -## 10. Liar's Dice / Signal Bluff (Level 40) - -File: `internal/game/hacking_liars_dice.go` - -### Theme Reskin - -| Classic | Sci-Fi | -|---|---| -| Dice | Signal fragments | -| Bid | Broadcast | -| Call (liar) | Intercept | -| Spot on | Exact match | -| Round | Cycle | - -### Concept - -Liar's Dice is a bluffing/deduction game. You and the AI each roll hidden dice. Players take turns making increasingly bold claims about the total dice showing a certain face across ALL dice (yours and the AI's combined). You can raise the bid or call the opponent a liar. If you call correctly, they lose a die. If you're wrong, you lose a die. Last player with dice wins. - -This works perfectly over telnet — just a few lines of text per turn. - -### Struct - -```go -type LiarsDiceGame struct { - playerDice []int // player's current dice (face values 1-6) - aiDice []int // AI's current dice (face values 1-6) - currentBid LiarsBid - playerTurn bool - round int - gameOver bool - won bool - lastAction string // for display context -} - -type LiarsBid struct { - Quantity int // "three" - Face int // "fours" (i.e., 3 dice showing 4) -} -``` - -### Rules - -- Both players start with 5 dice each (10 total). -- At the start of each round, all dice are re-rolled. You see only your own. -- The starting player makes a bid: "I claim there are at least N dice showing face F across all dice." -- The next player must either: - - **Raise**: Increase the quantity, OR keep the same quantity but increase the face value, OR increase both. - - **Call ("intercept")**: Challenge the bid. All dice are revealed. If the actual count >= the bid, the caller loses a die. If the actual count < the bid, the bidder loses a die. - - **Exact ("exact match")**: Claim the bid is EXACTLY right. If correct, the caller GAINS a die (up to max 5). If wrong, they lose a die. High risk, high reward. -- **Ones (1s) are wild** — they count as ANY face when tallying. This is the standard Liar's Dice rule and adds strategic depth. -- When a player loses all dice, they lose the game. -- Player who starts each round alternates. - -### Initialization (`Init`) - -1. Set `playerDice = make([]int, 5)`, `aiDice = make([]int, 5)`. -2. Roll all dice. -3. Player goes first in round 1. -4. Return welcome text + first round display. - -Welcome text: -``` -=== SIGNAL BLUFF === - -You've connected to a secure channel running a bluffing protocol. -You and the system each have 5 signal fragments (dice). Each round, -fragments are scrambled. You see only yours. Take turns making claims -about the total count of a specific signal across ALL fragments. - -1s are wild — they count as any signal. - -Commands: - bid - Claim at least N fragments show face F - (e.g., 'bid 3 4' = "at least three 4s") - call - Challenge the last bid (reveal all) - exact - Claim the bid is exactly right - status - Show your fragments and current bid - jack out - Disconnect (forfeit) -``` - -### Round Start Display - -``` ---- Cycle 1 --- -Your fragments: [3] [1] [5] [3] [6] (You: 5, System: 5) -You go first. Make a bid. -``` - -### Commands - -**`bid `** (or `b `, or just ` `): - -1. Validate: qty >= 1, face 1-6. -2. Validate the bid is higher than the current bid: - - Higher quantity with any face, OR - - Same quantity with higher face. - - Exception: bids on 1s follow special rules (since 1s are wild, bidding on 1s requires half the quantity, rounded up — standard Liar's Dice convention). **Simplification: skip the special 1s bidding rule. 1s are wild for counting but bidding on 1s works like any other face.** This keeps it simpler. -3. Set `currentBid = {qty, face}`. -4. AI takes its turn (see AI section). - -**`call`** (or `c`, `intercept`, `liar`): - -1. Can only be used when there's an active bid from the AI. -2. Reveal all dice. -3. Count total dice showing `currentBid.Face` across both players' dice, plus all 1s (wild). -4. Display: - ``` - You intercept! - Your fragments: [3] [1] [5] [3] [6] - System fragments: [2] [4] [4] [1] [4] - - Bid was: 4 fours - Actual count: 4 (three 4s + one wild 1 from system) - - The bid holds! You lose a fragment. - ``` - Or: - ``` - Actual count: 2 (one 4 + one wild 1) - - Bluff called! System loses a fragment. - ``` -5. Loser removes one die. Check for game over. -6. Start new round. - -**`exact`** (or `e`, `exact match`): - -1. Can only be used when there's an active bid from the AI. -2. Reveal all dice. Count as above. -3. If count == bid quantity exactly: Caller gains one die (up to 5). "Exact match! You recover a fragment." -4. If count != bid quantity: Caller loses one die. "Not an exact match. You lose a fragment." -5. Start new round. - -**`status`**: -``` ---- Cycle 3 --- -Your fragments: [3] [1] [5] (You: 3, System: 4) -Current bid: 3 fives (by System) -Your turn: bid higher, call, or exact. -``` - -### AI Strategy - -The AI should be competent but beatable. Implement a simple probabilistic strategy: - -```go -func (g *LiarsDiceGame) aiTurn() string { - totalDice := len(g.playerDice) + len(g.aiDice) - - // Count how many of the current bid face the AI has (including wilds) - aiCount := countFace(g.aiDice, g.currentBid.Face) - - // Estimate total: AI's known count + expected from unknown dice - // Expected per unknown die: 1/3 chance (1/6 for the face + 1/6 for wild 1) - unknownDice := len(g.playerDice) - estimated := float64(aiCount) + float64(unknownDice) / 3.0 - - // If current bid seems unreasonable, call - if float64(g.currentBid.Quantity) > estimated * 1.5 { - // Call - return g.resolveCall(false) // false = AI is calling - } - - // Otherwise, raise the bid - // Try to bid on a face the AI has many of - bestFace, bestCount := g.aiBestFace() - newQty := g.currentBid.Quantity - newFace := g.currentBid.Face - - if bestFace > newFace { - newFace = bestFace - } else { - newQty++ - newFace = bestFace - } - - // Sanity check: don't bid impossibly high - if newQty > totalDice { - return g.resolveCall(false) // forced to call - } - - g.currentBid = LiarsBid{newQty, newFace} - g.playerTurn = true - return fmt.Sprintf("System broadcasts: %d %ss.\nYour turn: bid higher, call, or exact.", - newQty, faceName(newFace)) -} -``` - -Add some randomness to the AI's bluffing — occasionally it bluffs high, occasionally it calls when it shouldn't. Scale the AI's skill with the player's hacking level for replayability. - -```go -func (g *LiarsDiceGame) aiBestFace() (face int, count int) { - counts := make(map[int]int) - for _, d := range g.aiDice { - if d == 1 { // wilds count for everything, skip - continue - } - counts[d]++ - } - wilds := 0 - for _, d := range g.aiDice { - if d == 1 { - wilds++ - } - } - best := 2 // never bid on 1s voluntarily - bestN := counts[2] + wilds - for f := 3; f <= 6; f++ { - n := counts[f] + wilds - if n > bestN || (n == bestN && f > best) { - best = f - bestN = n - } - } - return best, bestN -} -``` - -### Face Names Helper - -```go -func faceName(f int) string { - names := []string{"", "one", "two", "three", "four", "five", "six"} - return names[f] -} - -func faceNamePlural(f int) string { - names := []string{"", "ones", "twos", "threes", "fours", "fives", "sixes"} - return names[f] -} -``` - -### Win/Lose - -- **Win** (AI runs out of dice): `BaseXPWin` (300 XP). -- **Lose** (player runs out of dice): `BaseXPLose` (50 XP). -- **Quit**: `BaseXPLose` if at least one round was completed. -- **Bonus**: If the player wins without ever losing a die (perfect game), award 1.5x XP. Implement via `XPBonuser` interface: - -```go -func (g *LiarsDiceGame) BonusXP() int { - if len(g.playerDice) == 5 { - return 150 // 50% bonus for perfect game - } - return 0 -} -``` - -### Constructor - -```go -func NewLiarsDiceGame() *LiarsDiceGame { - return &LiarsDiceGame{} -} -``` - -## 11. XP Scaling - -XP is calculated in `endHacking()` in `hacking.go`: - -```go -func hackingXP(baseXP int, playerLevel int, reqLevel int) int { - multiplier := 1.0 + 0.01*float64(playerLevel-reqLevel) - if multiplier < 1.0 { - multiplier = 1.0 - } - return int(float64(baseXP) * multiplier) -} -``` - -- A level 1 player on the basic terminal (req 1): `50 * 1.0 = 50 XP` -- A level 30 player on the basic terminal (req 1): `50 * 1.29 = 64 XP` -- A level 20 player on the advanced terminal (req 20): `150 * 1.0 = 150 XP` -- A level 50 player on the advanced terminal (req 20): `150 * 1.30 = 195 XP` - -The scaling is mild — it mainly incentivizes unlocking harder terminals rather than grinding low-level ones forever. - -XP is awarded using the same pattern as other skills: - -```go -p.GainXP(player.Hacking, xp) -``` - -And level-up messages use the same `xpGain` pattern: - -```go -g.awardXP(sess, []xpGain{{Skill: string(player.Hacking), XP: xp}}) -``` - -Find the existing `awardXP` helper (used by gathering, combat, production) and call it the same way. - -## 12. Hacking Rumors NPC — "Netrunner" - -### `data/objects/netrunner.yaml` - -```yaml -id: netrunner -name: Netrunner -color: "39" -behavior: netrunner_talk -description: "A wiry figure hunched over a custom rig, fingers dancing across holographic keys. Cables snake from the back of their neck into the terminal. They glance up at you with augmented eyes." -inroom_description: "A {39}Netrunner{/} sits cross-legged on the floor, wired into the network." -``` - -### `data/behaviors/netrunner_talk.yaml` - -```yaml -id: netrunner_talk -type: talk -nodes: - start: - message: "The Netrunner looks up. \"You want in on the network? I can give you some pointers.\"" - options: - - text: "\"What is hacking?\"" - goto: what_is - - text: "\"Where can I find terminals?\"" - goto: terminals - - text: "\"Any tips for the basic terminal?\"" - goto: tips_basic - - text: "\"What about the advanced terminal?\"" - goto: tips_advanced - condition: - player_flag: hacking_asked_basic - value: true - - text: "\"Tell me about the secure terminal.\"" - goto: tips_secure - condition: - player_flag: hacking_asked_advanced - value: true - - text: "\"Goodbye.\"" - end: true - what_is: - message: "\"Hacking's about jacking into terminals and running their gauntlets. Each terminal runs a different challenge — puzzles, hunts, pattern games. Crack 'em and you get better at it. Simple as that.\"" - options: - - text: "\"Where can I find terminals?\"" - goto: terminals - - text: "\"Goodbye.\"" - end: true - terminals: - message: "\"There's a basic terminal right here in this lab — good for beginners. The station down east has an advanced one if you've got the chops. And if you're really good, there's a secure terminal deep in the compound. You'll need the skills to match though.\"" - options: - - text: "\"Any tips?\"" - goto: tips_basic - - text: "\"Thanks.\"" - end: true - tips_basic: - message: "\"The basic terminal runs an old rogue AI hunt. Twenty nodes in a network. You've got probes to flush it out. Listen for the warnings — corrupted data means it's close, voids mean a trap, static means ICE. Shoot into adjacent nodes, don't walk into danger.\"" - action: - set_player_flags: - hacking_asked_basic: true - options: - - text: "\"What about the advanced terminal?\"" - goto: tips_advanced - - text: "\"Thanks.\"" - end: true - tips_advanced: - message: "\"Advanced terminal runs a cipher crack. Four symbols, no repeats. You get feedback after each guess — locked means right spot, found means right symbol wrong spot. Process of elimination. Ten tries.\"" - action: - set_player_flags: - hacking_asked_advanced: true - options: - - text: "\"Tell me about the secure terminal.\"" - goto: tips_secure - - text: "\"Thanks.\"" - end: true - tips_secure: - message: "\"Secure terminal? Signal Bluff. It's a bluffing game — you and the system each roll hidden dice and take turns making claims about the total. Ones are wild. Call the bluff or raise the stakes. Read the probabilities, play the odds, and don't let it psych you out.\"" - options: - - text: "\"Thanks.\"" - end: true -``` - -## 13. Rooms - -### Updated `data/rooms/8.yaml` - -```yaml -id: 8 -name: "Hacking Lab" -description: "A dimly lit room filled with old terminals and flickering screens. Cables dangle from the ceiling, some still sparking. The air smells of ozone and burnt circuitry. A {40}basic terminal{/} against the far wall still has power." -exits: - east: 9 - west: 6 -objects: - - id: terminal_basic - - id: netrunner -``` - -### Terminal Placements in Other Rooms - -Place the advanced and secure terminals in existing rooms. Exact room IDs depend on the world layout, but suggested placements: - -- `terminal_advanced` — place in a room that feels like a more secure/advanced area. Add to a room's `objects:` list. Example: a room in the production district or deeper in the compound. -- `terminal_secure` — place in a high-level area or behind a condition-gated exit. - -For now, if no appropriate rooms exist yet, create placeholder rooms or add them to existing higher-level areas. Minimum viable: all three terminals can be in room 8 for testing, then moved later: - -```yaml -# For testing, room 8 can have all three: -objects: - - id: terminal_basic - - id: terminal_advanced - - id: terminal_secure - - id: netrunner -``` - -## 14. Display - -All output uses `sess.WriteLine()` for lines and `sess.Write()` for prompts without newlines. - -### Hacking Prompt - -While in StateHacking, after each minigame output, display a custom prompt: - -```go -sess.Write("\nhack> ") -``` - -This replaces the normal game prompt while jacked in. - -### Colorization - -Use the existing `colorize()` helper with color targets. Suggested color usage: - -- Terminal name: use the object's color field -- Minigame title/headers: `{39}` (cyan) or `{40}` (green) -- Warnings in Wumpus: `{196}` (red) for wumpus, `{208}` (orange) for pits, `{226}` (yellow) for ICE -- Mastermind exact match `[X]`: `{40}` (green) -- Mastermind partial match `[O]`: `{226}` (yellow) -- Mastermind miss `[ ]`: default -- Liar's Dice bid announcement: `{33}` (cyan) -- Liar's Dice call result (win): `{40}` (green) -- Liar's Dice call result (lose): `{196}` (red) -- Liar's Dice dice display `[N]`: `{230}` (yellow-white) - -Use `g.colorize(sess, ...)` or raw inline color tags in output strings. Since minigames return plain strings, either: -1. Return strings with inline `{N}text{/}` tags and have `handleHackingInput` pass them through the color system before writing, OR -2. Minigames return raw strings and the handler colorizes key parts. - -Option 1 is simpler. The `sess.WriteLine()` path should already support inline tags if the existing `colorize`/`colorTag` system is used. Check how room descriptions handle inline tags — the same mechanism applies. - -### Wumpus Map Display - -For the `map` command in Wumpus, display a simple node list: - -``` -=== Network Map === - Node 1: -> 2, 5, 8 - Node 2: -> 1, 3, 10 - ... - Node 20: -> 13, 16, 19 - -You are at Node 7. -``` - -Mark the player's node with `[*]`. Optionally track visited nodes and mark them differently, but this is not required for v1. - -### Mastermind Guess Display - -``` -=== Code Breaker === -Attempt 1/10: 1 2 3 4 -> {40}[X]{/} [ ] {226}[O]{/} [ ] (1 locked, 1 found) -Attempt 2/10: 5 6 1 2 -> [ ] {226}[O]{/} [ ] {226}[O]{/} (0 locked, 2 found) -``` - -### Liar's Dice Display - -All output is simple text lines — no grid needed: - -``` ---- Cycle 2 --- -Your fragments: [3] [1] [5] [3] (You: 4, System: 5) -System broadcasts: 3 fours. -Your turn: bid higher, call, or exact. -``` - -On a call: -``` -You intercept! -Your fragments: [3] [1] [5] [3] -System fragments: [2] [4] [4] [1] [4] - -Bid was: 3 fours -Actual count: 4 (three 4s + one wild 1) - -The bid holds! You lose a fragment. -``` - -## 15. State Management - -### Full Flow - -``` -1. Player types: jack terminal -2. handleGameCommand -> classifyCommand("jack") -> ClassActive -> queued -3. ProcessQueuedCommands -> executeCommand -> doJack() -4. doJack(): - a. Find terminal object in room (use World.FindObjInstances or check objects list) - b. Look up TerminalDef by object ID - c. Check hacking level >= required level - d. Check player not in combat - e. Cancel any active action - f. Create HackingSession, store in hackingStates[p.Name] - g. Set p.ActionState = &ActionState{Type: ActionHacking, TargetName: terminalDef.Name} - h. Set sess.State = net.StateHacking - i. Call minigame.Init(level), write output to session - j. Write "hack> " prompt -5. Player input now routes to handleHackingInput() -6. handleHackingInput(): - a. Check for "jack out" / "quit" -> endHacking(sess, false, false) - b. Otherwise: call minigame.HandleInput(input) - c. Write output - d. If done: endHacking(sess, true, won) - e. Else: write "hack> " prompt -7. endHacking(): - a. Calculate XP (win or lose) - b. Award XP via awardXP() - c. Clear hackingStates[p.Name] - d. Clear p.ActionState - e. Set sess.State = net.StateGame - f. Write summary message - g. Write normal game prompt -``` - -### `cmd_jack.go` — Full Implementation Spec - -```go -func (g *Game) doJack(sess *net.Session, target string) { - p := sess.Player.(*player.Player) - - if combat.GetCombat(p.Name) != nil { - sess.WriteLine("You can't do that during combat!") - return - } - - // If target is empty, auto-detect: find any terminal_* object in the room - if target == "" { - target = g.findTerminalInRoom(p.RoomID) - if target == "" { - sess.WriteLine("There's no terminal here to jack into.") - return - } - } - - // Find matching object in room - instances := g.World.FindObjInstances(p.RoomID, strings.ToLower(target)) - if len(instances) == 0 { - sess.WriteLine("You don't see that here.") - return - } - - // Check if it's a terminal - defID := instances[0].DefID - tDef, ok := terminalDefs[defID] - if !ok { - sess.WriteLine("You can't jack into that.") - return - } - - // Check hacking level - hackLevel := p.SkillLevel(player.Hacking) - if hackLevel < tDef.Level { - sess.WriteLine(fmt.Sprintf("You need level %d Hacking to use this terminal.", tDef.Level)) - return - } - - // Start minigame - minigame := tDef.Minigame() - display := minigame.Init(hackLevel) - - g.hackingStates[p.Name] = &HackingSession{ - Minigame: minigame, - TerminalID: defID, - Level: hackLevel, - ReqLevel: tDef.Level, - Started: false, - } - - p.ActionState = &ActionState{Type: ActionHacking, TargetName: tDef.Name} - sess.State = net.StateHacking - - // Broadcast to room - g.broadcast(sess, p.RoomID, fmt.Sprintf("%s jacks into a %s.", p.Name, tDef.Name)) - - sess.WriteLine(display) - sess.Write("\nhack> ") -} -``` - -### `hacking.go` — `handleHackingInput` Implementation Spec - -```go -func (g *Game) handleHackingInput(sess *net.Session, input string) { - p, ok := sess.Player.(*player.Player) - if !ok { - sess.State = net.StateGame - return - } - - hs, ok := g.hackingStates[p.Name] - if !ok { - sess.State = net.StateGame - g.writePrompt(sess) - return - } - - input = strings.TrimSpace(input) - lower := strings.ToLower(input) - - if lower == "jack out" || lower == "quit" || lower == "disconnect" { - g.endHacking(sess, p, false, false) - return - } - - hs.Started = true - output, done, won := hs.Minigame.HandleInput(input) - - sess.WriteLine(output) - - if done { - g.endHacking(sess, p, true, won) - return - } - - sess.Write("\nhack> ") -} - -func (g *Game) endHacking(sess *net.Session, p *player.Player, completed bool, won bool) { - hs, ok := g.hackingStates[p.Name] - if !ok { - sess.State = net.StateGame - g.writePrompt(sess) - return - } - - tDef := terminalDefs[hs.TerminalID] - var xp int - - if completed && won { - xp = hackingXP(tDef.BaseXPWin, hs.Level, hs.ReqLevel) - sess.WriteLine(fmt.Sprintf("\nConnection terminated. Contract complete.")) - } else if completed { - xp = hackingXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel) - sess.WriteLine(fmt.Sprintf("\nConnection lost.")) - } else if hs.Started { - xp = hackingXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel) - sess.WriteLine("\nYou jack out of the terminal.") - } else { - sess.WriteLine("\nYou jack out of the terminal.") - } - - // Handle bonus XP for Mastermind (fewer guesses) - // The minigame can embed bonus info — but simpler to just use baseXP. - // For Mastermind, override: the HandleInput returns the bonus data via naming convention - // or we add a BonusXP() method to the interface. See Alternative below. - - if xp > 0 { - g.awardXP(sess, []xpGain{{Skill: string(player.Hacking), XP: xp}}) - } - - // Broadcast to room - g.broadcast(sess, p.RoomID, fmt.Sprintf("%s disconnects from the terminal.", p.Name)) - - delete(g.hackingStates, p.Name) - p.ActionState = nil - sess.State = net.StateGame - g.writePrompt(sess) -} -``` - -### Bonus XP for Mastermind - -Add an optional method to the interface (or use a concrete type assertion): - -```go -type XPBonuser interface { - BonusXP() int -} -``` - -In `endHacking`, after calculating base XP: - -```go -if won { - if b, ok := hs.Minigame.(XPBonuser); ok { - xp += hackingXP(b.BonusXP(), hs.Level, hs.ReqLevel) - } -} -``` - -Mastermind implements `BonusXP()`: - -```go -func (m *MastermindGame) BonusXP() int { - return (m.maxGuess - len(m.guesses)) * 15 -} -``` - -### Disconnect Cleanup - -In `Game.SetHub`'s `OnRemove` callback, after the existing player cleanup: - -```go -if p, ok := sess.Player.(*player.Player); ok { - delete(g.hackingStates, p.Name) -} -``` - -### Combat Interruption - -If a mob wanders into the room and attacks the player while jacked in, the hacking session should be forcibly ended. In the mob attack initiation code, check if the player is in `StateHacking` and call `endHacking` first. Alternatively, since mobs only attack via combat initiation and the player isn't in `StateGame`, mobs should not be able to initiate combat with a jacked-in player. This is the simpler approach — **mobs do not attack jacked-in players** (the terminal provides safety, lore-wise the player's physical body is in a protected alcove). - -## 16. Help Files - -### `data/help/jack.yaml` - -```yaml -name: "jack" -category: "Commands" -description: | - Jack into a terminal to start a hacking minigame. - - Usage: jack [terminal] - - Connects to a hacking terminal in the room and starts its - minigame. Each terminal type offers a different challenge - and requires a minimum Hacking level. - - If there's only one terminal in the room, you can just type 'jack'. - - While jacked in, all input goes to the minigame. Type 'jack out' - or 'quit' to disconnect early. - - Terminal types: - Basic terminal - Level 1 - Hunt the Rogue AI - Advanced terminal - Level 20 - Code Breaker - Secure terminal - Level 40 - Signal Bluff - - See also: help hacking -``` - -### `data/help/hacking.yaml` - -```yaml -name: "hacking" -category: "Skills" -description: | - Hacking is trained by jacking into terminals and completing - their minigames. Different terminals offer different challenges - at different difficulty levels. - - Find terminals scattered around the world. Use the 'jack' command - to connect. Each terminal runs a unique puzzle — solve it for XP. - Winning gives full XP; losing or quitting early gives a small - consolation amount. - - Talk to the Netrunner in the Hacking Lab for tips on each - terminal's challenge. - - XP scales slightly with your Hacking level above the terminal's - requirement, but the main progression is unlocking harder terminals - with bigger rewards. - - See also: help jack -``` - -## 17. Implementation Order - -Recommended order for an implementing agent: - -1. **`internal/net/server.go`** — Add `StateHacking` constant. -2. **`internal/game/action_state.go`** — Add `ActionHacking` constant and `Description()` case. -3. **`internal/game/hacking.go`** — Create file with `HackingMinigame` interface, `HackingSession` struct, `terminalDefs` map, `handleHackingInput()`, `endHacking()`, `hackingXP()`, `XPBonuser` interface. -4. **`internal/game/hacking_wumpus.go`** — Implement `WumpusGame`. -5. **`internal/game/hacking_mastermind.go`** — Implement `MastermindGame`. -6. **`internal/game/hacking_liars_dice.go`** — Implement `LiarsDiceGame`. -7. **`internal/game/cmd_jack.go`** — Implement `doJack()` and `findTerminalInRoom()`. -8. **`internal/game/game.go`** — Add `hackingStates` field, init in `New()`, add `StateHacking` case in `HandleSession`, add `jack`/`jackin` to `classifyCommand` and `executeCommand`, add disconnect cleanup in `SetHub`. -9. **Data files** — Create all YAML files (objects, behaviors, help, update room 8). -10. **Test** — `make build && make vet` to verify compilation. Manual testing via telnet. - -## 18. Testing Checklist - -- [ ] `make build` compiles without errors -- [ ] `make vet` passes -- [ ] `make test` passes (no existing tests break) -- [ ] Player can `jack` into basic terminal in room 8 -- [ ] Wumpus game: move, shoot, status, map commands work -- [ ] Wumpus game: winning awards full XP -- [ ] Wumpus game: losing awards consolation XP -- [ ] `jack out` exits minigame cleanly -- [ ] Player returns to normal `StateGame` after minigame -- [ ] Level check prevents low-level players from using advanced terminals -- [ ] Player's ActionState shows "jacked into a Hunt the Wumpus" in `look` -- [ ] Disconnect while jacked in cleans up state -- [ ] Mastermind: guessing, feedback, win/lose, bonus XP -- [ ] Liar's Dice: bidding, calling, exact, AI turns, win/lose -- [ ] Netrunner NPC responds to `talk netrunner` -- [ ] `help jack` and `help hacking` display correctly -- [ ] XP scaling formula applies correctly - -## 19. Future Expansion - -Additional minigames can be added by: -1. Creating a new `hacking_.go` file implementing `HackingMinigame` -2. Creating a new `terminal_.yaml` object -3. Adding an entry to `terminalDefs` - -Potential future minigames: -- **Blackjack / Data Assembly** (Level 60): Assemble data packets to exactly 21 without busting -- **Logic Gates** (Level 70): Solve a boolean circuit puzzle -- **Stock Trading / Market Exploit** (Level 50): Buy/sell commodities over N rounds, maximize profit -- **Number Maze** (Level 80): Navigate a grid where each cell tells you how far you can jump - -The interface is simple enough that adding new minigames is self-contained — no changes to the core hacking infrastructure needed. -- cgit v1.2.3