aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hacking
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/hacking')
-rw-r--r--internal/game/hacking/hacking.go63
-rw-r--r--internal/game/hacking/hacking_liars_dice.go395
-rw-r--r--internal/game/hacking/hacking_mastermind.go177
-rw-r--r--internal/game/hacking/hacking_wumpus.go286
4 files changed, 921 insertions, 0 deletions
diff --git a/internal/game/hacking/hacking.go b/internal/game/hacking/hacking.go
new file mode 100644
index 0000000..f022ee1
--- /dev/null
+++ b/internal/game/hacking/hacking.go
@@ -0,0 +1,63 @@
+package hacking
+
+type Minigame interface {
+ Init(level int) string
+ HandleInput(input string) (output string, done bool, won bool)
+ Name() string
+}
+
+type XPBonuser interface {
+ BonusXP() int
+}
+
+type Session struct {
+ Minigame Minigame
+ TerminalID string
+ Level int
+ ReqLevel int
+ Started bool
+}
+
+type TerminalDef struct {
+ ObjectID string
+ Level int
+ Minigame func() Minigame
+ BaseXPWin int
+ BaseXPLose int
+ Name string
+}
+
+var Terminals = map[string]TerminalDef{
+ "terminal_basic": {
+ ObjectID: "terminal_basic",
+ Level: 1,
+ Minigame: func() Minigame { return NewWumpusGame() },
+ BaseXPWin: 50,
+ BaseXPLose: 10,
+ Name: "Hunt the Wumpus",
+ },
+ "terminal_advanced": {
+ ObjectID: "terminal_advanced",
+ Level: 20,
+ Minigame: func() Minigame { return NewMastermindGame() },
+ BaseXPWin: 150,
+ BaseXPLose: 30,
+ Name: "Code Breaker",
+ },
+ "terminal_secure": {
+ ObjectID: "terminal_secure",
+ Level: 40,
+ Minigame: func() Minigame { return NewLiarsDiceGame() },
+ BaseXPWin: 300,
+ BaseXPLose: 50,
+ Name: "Signal Bluff",
+ },
+}
+
+func CalcXP(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)
+}
diff --git a/internal/game/hacking/hacking_liars_dice.go b/internal/game/hacking/hacking_liars_dice.go
new file mode 100644
index 0000000..bcfbd40
--- /dev/null
+++ b/internal/game/hacking/hacking_liars_dice.go
@@ -0,0 +1,395 @@
+package hacking
+
+import (
+ "fmt"
+ "math/rand"
+ "strconv"
+ "strings"
+)
+
+type LiarsDiceGame struct {
+ playerDice []int
+ aiDice []int
+ currentBid LiarsBid
+ hasBid bool
+ playerTurn bool
+ round int
+ gameOver bool
+ won bool
+ playerLost int
+ completed bool
+}
+
+type LiarsBid struct {
+ Quantity int
+ Face int
+}
+
+func NewLiarsDiceGame() *LiarsDiceGame {
+ return &LiarsDiceGame{
+ playerDice: make([]int, 5),
+ aiDice: make([]int, 5),
+ }
+}
+
+func (l *LiarsDiceGame) Name() string {
+ return "Signal Bluff"
+}
+
+func (l *LiarsDiceGame) Init(level int) string {
+ l.playerDice = make([]int, 5)
+ l.aiDice = make([]int, 5)
+ l.round = 0
+ l.gameOver = false
+ l.playerTurn = true
+ l.hasBid = false
+ l.playerLost = 0
+ l.completed = false
+
+ var sb strings.Builder
+ sb.WriteString("=== SIGNAL BLUFF ===\n")
+ sb.WriteString("\n")
+ sb.WriteString("You've connected to a secure channel running a bluffing protocol.\n")
+ sb.WriteString("You and the system each have 5 signal fragments (dice). Each round,\n")
+ sb.WriteString("fragments are scrambled. You see only yours. Take turns making claims\n")
+ sb.WriteString("about the total count of a specific signal across ALL fragments.\n")
+ sb.WriteString("\n")
+ sb.WriteString("1s are wild -- they count as any signal.\n")
+ sb.WriteString("\n")
+ sb.WriteString("Commands:\n")
+ sb.WriteString(" bid <qty> <face> - Claim at least N fragments show face F\n")
+ sb.WriteString(" (e.g., 'bid 3 4' = \"at least three 4s\")\n")
+ sb.WriteString(" call - Challenge the last bid (reveal all)\n")
+ sb.WriteString(" exact - Claim the bid is exactly right\n")
+ sb.WriteString(" status - Show your fragments and current bid\n")
+ sb.WriteString(" jack out - Disconnect (forfeit)\n")
+ sb.WriteString("\n")
+ sb.WriteString(l.startRound())
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) HandleInput(input string) (string, bool, bool) {
+ input = strings.TrimSpace(strings.ToLower(input))
+ parts := strings.Fields(input)
+
+ if len(parts) == 0 {
+ return "Commands: bid <qty> <face>, call, exact, status, jack out", false, false
+ }
+
+ cmd := parts[0]
+
+ switch cmd {
+ case "bid", "b":
+ if len(parts) < 3 {
+ return "Usage: bid <quantity> <face> (e.g., bid 3 4)", false, false
+ }
+ qty, err1 := strconv.Atoi(parts[1])
+ face, err2 := strconv.Atoi(parts[2])
+ if err1 != nil || err2 != nil {
+ return "Usage: bid <quantity> <face> (e.g., bid 3 4)", false, false
+ }
+ return l.doBid(qty, face)
+ case "call", "c", "intercept", "liar":
+ return l.doCall(false)
+ case "exact", "e":
+ return l.doExact()
+ case "status":
+ return l.doStatus(), false, false
+ default:
+ if len(parts) == 2 {
+ qty, err1 := strconv.Atoi(parts[0])
+ face, err2 := strconv.Atoi(parts[1])
+ if err1 == nil && err2 == nil {
+ return l.doBid(qty, face)
+ }
+ }
+ return "Commands: bid <qty> <face>, call, exact, status, jack out", false, false
+ }
+}
+
+func (l *LiarsDiceGame) doBid(qty, face int) (string, bool, bool) {
+ if face < 1 || face > 6 {
+ return "Face must be 1-6.", false, false
+ }
+ if qty < 1 {
+ return "Quantity must be at least 1.", false, false
+ }
+ if !l.playerTurn {
+ return "It's not your turn to bid. Use 'call' or 'exact'.", false, false
+ }
+
+ if l.hasBid {
+ if qty < l.currentBid.Quantity || (qty == l.currentBid.Quantity && face <= l.currentBid.Face) {
+ return "You must bid higher (higher quantity or same quantity with higher face).", false, false
+ }
+ }
+
+ l.currentBid = LiarsBid{Quantity: qty, Face: face}
+ l.hasBid = true
+ l.playerTurn = false
+
+ return l.aiTurn()
+}
+
+func (l *LiarsDiceGame) doCall(player bool) (string, bool, bool) {
+ if !l.hasBid {
+ return "No bid to call yet. Make a bid first.", false, false
+ }
+
+ var sb strings.Builder
+ if player {
+ sb.WriteString("System intercepts!\n")
+ } else {
+ sb.WriteString("You intercept!\n")
+ }
+
+ sb.WriteString(l.showAllDice())
+ sb.WriteString(fmt.Sprintf("\nBid was: %d %s\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face)))
+
+ total := l.countFace(l.currentBid.Face)
+ sb.WriteString(fmt.Sprintf("Actual count: %d (%d %ss + %d wild 1s)\n\n",
+ total,
+ l.countFaceNonWild(l.currentBid.Face),
+ faceName(l.currentBid.Face),
+ l.countFace(1)-l.countFaceNonWild(1)))
+
+ if total >= l.currentBid.Quantity {
+ sb.WriteString("The bid holds! ")
+ if player {
+ sb.WriteString("System loses a fragment.")
+ l.aiDice = l.aiDice[:len(l.aiDice)-1]
+ } else {
+ sb.WriteString("You lose a fragment.")
+ l.playerDice = l.playerDice[:len(l.playerDice)-1]
+ l.playerLost++
+ }
+ } else {
+ sb.WriteString("Bluff called! ")
+ if player {
+ sb.WriteString("You lose a fragment.")
+ l.playerDice = l.playerDice[:len(l.playerDice)-1]
+ l.playerLost++
+ } else {
+ sb.WriteString("System loses a fragment.")
+ l.aiDice = l.aiDice[:len(l.aiDice)-1]
+ }
+ }
+
+ return l.checkGameOver(sb)
+}
+
+func (l *LiarsDiceGame) doExact() (string, bool, bool) {
+ if !l.hasBid {
+ return "No bid to claim exact on. Make a bid first.", false, false
+ }
+
+ var sb strings.Builder
+ sb.WriteString("You claim exact match!\n")
+
+ sb.WriteString(l.showAllDice())
+ total := l.countFace(l.currentBid.Face)
+ sb.WriteString(fmt.Sprintf("\nBid was: %d %s\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face)))
+ sb.WriteString(fmt.Sprintf("Actual count: %d\n\n", total))
+
+ if total == l.currentBid.Quantity {
+ sb.WriteString("Exact match! You recover a fragment.\n")
+ if len(l.playerDice) < 5 {
+ l.playerDice = append(l.playerDice, rand.Intn(6)+1)
+ }
+ } else {
+ sb.WriteString("Not an exact match. You lose a fragment.\n")
+ l.playerDice = l.playerDice[:len(l.playerDice)-1]
+ l.playerLost++
+ }
+
+ return l.checkGameOver(sb)
+}
+
+func (l *LiarsDiceGame) checkGameOver(sb strings.Builder) (string, bool, bool) {
+ if len(l.playerDice) == 0 {
+ sb.WriteString("\nYou've lost all your fragments. Connection terminated.")
+ l.gameOver = true
+ l.won = false
+ l.completed = true
+ return sb.String(), true, false
+ }
+ if len(l.aiDice) == 0 {
+ sb.WriteString("\nSystem has no fragments left. You win!")
+ l.gameOver = true
+ l.won = true
+ l.completed = true
+ return sb.String(), true, true
+ }
+
+ sb.WriteString(l.startRound())
+ return sb.String(), false, false
+}
+
+func (l *LiarsDiceGame) showAllDice() string {
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("Your fragments: %s\n", l.diceStr(l.playerDice)))
+ sb.WriteString(fmt.Sprintf("System fragments: %s\n", l.diceStr(l.aiDice)))
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) diceStr(dice []int) string {
+ var parts []string
+ for _, d := range dice {
+ parts = append(parts, fmt.Sprintf("[%d]", d))
+ }
+ return strings.Join(parts, " ")
+}
+
+func (l *LiarsDiceGame) doStatus() string {
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("--- Cycle %d ---\n", l.round))
+ sb.WriteString(fmt.Sprintf("Your fragments: %s (You: %d, System: %d)\n",
+ l.diceStr(l.playerDice), len(l.playerDice), len(l.aiDice)))
+ if l.hasBid {
+ who := "System"
+ if !l.playerTurn {
+ who = "System"
+ } else {
+ who = "you"
+ }
+ sb.WriteString(fmt.Sprintf("Current bid: %d %s (by %s)\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face), who))
+ }
+ if l.playerTurn {
+ sb.WriteString("Your turn: make a bid.")
+ } else {
+ sb.WriteString("Your turn: bid higher, call, or exact.")
+ }
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) startRound() string {
+ l.round++
+ for i := range l.playerDice {
+ l.playerDice[i] = rand.Intn(6) + 1
+ }
+ for i := range l.aiDice {
+ l.aiDice[i] = rand.Intn(6) + 1
+ }
+ l.hasBid = false
+ l.playerTurn = true
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("--- Cycle %d ---\n", l.round))
+ sb.WriteString(fmt.Sprintf("Your fragments: %s (You: %d, System: %d)\n",
+ l.diceStr(l.playerDice), len(l.playerDice), len(l.aiDice)))
+ sb.WriteString("You go first. Make a bid.\n")
+ return sb.String()
+}
+
+func (l *LiarsDiceGame) aiTurn() (string, bool, bool) {
+ totalDice := len(l.playerDice) + len(l.aiDice)
+
+ aiCount := l.countFace(l.currentBid.Face)
+ unknownDice := len(l.playerDice)
+ estimated := float64(aiCount) + float64(unknownDice)/3.0
+
+ if float64(l.currentBid.Quantity) > estimated*1.5 {
+ return l.doCall(true)
+ }
+
+ bestFace, bestCount := l.aiBestFace()
+ newQty := l.currentBid.Quantity
+ newFace := l.currentBid.Face
+
+ if bestFace > newFace && bestCount >= newQty {
+ newFace = bestFace
+ } else {
+ newQty++
+ }
+
+ if newQty > totalDice {
+ return l.doCall(true)
+ }
+
+ l.currentBid = LiarsBid{Quantity: newQty, Face: newFace}
+ l.playerTurn = true
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("System broadcasts: %d %s.\n", newQty, faceNamePlural(newFace)))
+ sb.WriteString("Your turn: bid higher, call, or exact.\n")
+ return sb.String(), false, false
+}
+
+func (l *LiarsDiceGame) aiBestFace() (face int, count int) {
+ counts := make(map[int]int)
+ for _, d := range l.aiDice {
+ if d == 1 {
+ continue
+ }
+ counts[d]++
+ }
+ wilds := 0
+ for _, d := range l.aiDice {
+ if d == 1 {
+ wilds++
+ }
+ }
+ best := 2
+ 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
+}
+
+func (l *LiarsDiceGame) countFace(face int) int {
+ count := 0
+ for _, d := range l.playerDice {
+ if d == face || d == 1 {
+ count++
+ }
+ }
+ for _, d := range l.aiDice {
+ if d == face || d == 1 {
+ count++
+ }
+ }
+ return count
+}
+
+func (l *LiarsDiceGame) countFaceNonWild(face int) int {
+ count := 0
+ for _, d := range l.playerDice {
+ if d == face {
+ count++
+ }
+ }
+ for _, d := range l.aiDice {
+ if d == face {
+ count++
+ }
+ }
+ return count
+}
+
+func faceName(f int) string {
+ names := []string{"", "one", "two", "three", "four", "five", "six"}
+ if f >= 1 && f <= 6 {
+ return names[f]
+ }
+ return fmt.Sprint(f)
+}
+
+func faceNamePlural(f int) string {
+ names := []string{"", "ones", "twos", "threes", "fours", "fives", "sixes"}
+ if f >= 1 && f <= 6 {
+ return names[f]
+ }
+ return fmt.Sprint(f)
+}
+
+func (l *LiarsDiceGame) BonusXP() int {
+ if l.completed && l.won && l.playerLost == 0 {
+ return 150
+ }
+ return 0
+}
diff --git a/internal/game/hacking/hacking_mastermind.go b/internal/game/hacking/hacking_mastermind.go
new file mode 100644
index 0000000..8d4eb1c
--- /dev/null
+++ b/internal/game/hacking/hacking_mastermind.go
@@ -0,0 +1,177 @@
+package hacking
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+)
+
+type MastermindGame struct {
+ code [4]int
+ guesses []MastermindGuess
+ maxGuess int
+ gameOver bool
+ won bool
+}
+
+type MastermindGuess struct {
+ Digits [4]int
+ Exact int
+ Partial int
+}
+
+func NewMastermindGame() *MastermindGame {
+ return &MastermindGame{
+ maxGuess: 10,
+ }
+}
+
+func (m *MastermindGame) Name() string {
+ return "Code Breaker"
+}
+
+func (m *MastermindGame) Init(level int) string {
+ pool := []int{1, 2, 3, 4, 5, 6}
+ rand.Shuffle(len(pool), func(i, j int) { pool[i], pool[j] = pool[j], pool[i] })
+ copy(m.code[:], pool[:4])
+ m.guesses = nil
+ m.gameOver = false
+ m.won = false
+
+ var sb strings.Builder
+ sb.WriteString("=== CODE BREAKER ===\n")
+ sb.WriteString("\n")
+ sb.WriteString("The encryption module is running a 4-digit cipher using symbols 1-6.\n")
+ sb.WriteString("No symbol repeats. You have 10 attempts to crack the code.\n")
+ sb.WriteString("\n")
+ sb.WriteString("After each guess, you'll see:\n")
+ sb.WriteString(" {40}[X]{/} = correct symbol in correct position\n")
+ sb.WriteString(" {226}[O]{/} = correct symbol in wrong position\n")
+ sb.WriteString(" [ ] = symbol not in code\n")
+ sb.WriteString("\n")
+ sb.WriteString("Commands:\n")
+ sb.WriteString(" <4 digits> - Guess the code (e.g., 1234)\n")
+ sb.WriteString(" status - Show previous guesses\n")
+ sb.WriteString(" jack out - Disconnect (forfeit)\n")
+ return sb.String()
+}
+
+func (m *MastermindGame) HandleInput(input string) (string, bool, bool) {
+ input = strings.TrimSpace(input)
+ lower := strings.ToLower(strings.TrimSpace(input))
+
+ if lower == "status" {
+ return m.doStatus(), false, false
+ }
+
+ if len(input) != 4 {
+ return "Enter exactly 4 digits (1-6, no repeats).", false, false
+ }
+
+ var guess [4]int
+ seen := make(map[int]bool)
+ for i, ch := range input {
+ if ch < '1' || ch > '6' {
+ return "Each digit must be 1-6.", false, false
+ }
+ d := int(ch - '0')
+ if seen[d] {
+ return "No repeating digits allowed.", false, false
+ }
+ seen[d] = true
+ guess[i] = d
+ }
+
+ exact := 0
+ partial := 0
+ for i := 0; i < 4; i++ {
+ if guess[i] == m.code[i] {
+ exact++
+ } else {
+ for j := 0; j < 4; j++ {
+ if guess[i] == m.code[j] {
+ partial++
+ break
+ }
+ }
+ }
+ }
+
+ m.guesses = append(m.guesses, MastermindGuess{
+ Digits: guess,
+ Exact: exact,
+ Partial: partial,
+ })
+
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("Attempt %d/%d: ", len(m.guesses), m.maxGuess))
+ for i, d := range guess {
+ match := "[ ]"
+ if guess[i] == m.code[i] {
+ match = "{40}[X]{/}"
+ } else {
+ for j := 0; j < 4; j++ {
+ if guess[i] == m.code[j] {
+ match = "{226}[O]{/}"
+ break
+ }
+ }
+ }
+ sb.WriteString(fmt.Sprintf("%d%s ", d, match))
+ }
+ sb.WriteString(fmt.Sprintf(" (%d locked, %d found)", exact, partial))
+
+ if exact == 4 {
+ m.gameOver = true
+ m.won = true
+ sb.WriteString("\n\nCode cracked! The encryption module yields.\nConnection terminated.")
+ return sb.String(), true, true
+ }
+
+ if len(m.guesses) >= m.maxGuess {
+ m.gameOver = true
+ m.won = false
+ sb.WriteString(fmt.Sprintf("\n\nOut of attempts. The code was: %d %d %d %d.\nConnection terminated.",
+ m.code[0], m.code[1], m.code[2], m.code[3]))
+ return sb.String(), true, false
+ }
+
+ return sb.String(), false, false
+}
+
+func (m *MastermindGame) doStatus() string {
+ if len(m.guesses) == 0 {
+ return fmt.Sprintf("No guesses yet. Remaining: %d/%d", m.maxGuess, m.maxGuess)
+ }
+ var sb strings.Builder
+ sb.WriteString("=== Attempts ===\n")
+ for i, g := range m.guesses {
+ sb.WriteString(fmt.Sprintf(" %2d: %d %d %d %d -> ", i+1, g.Digits[0], g.Digits[1], g.Digits[2], g.Digits[3]))
+ for j, d := range g.Digits {
+ match := "[ ]"
+ if g.Digits[j] == m.code[j] {
+ match = "{40}[X]{/}"
+ } else {
+ found := false
+ for k := 0; k < 4; k++ {
+ if g.Digits[j] == m.code[k] {
+ found = true
+ break
+ }
+ }
+ if found {
+ match = "{226}[O]{/}"
+ }
+ }
+ _ = d
+ sb.WriteString(match)
+ }
+ sb.WriteString(fmt.Sprintf(" (%d locked, %d found)\n", g.Exact, g.Partial))
+ }
+ sb.WriteString(fmt.Sprintf("Remaining: %d/%d\n", m.maxGuess-len(m.guesses), m.maxGuess))
+ return sb.String()
+}
+
+func (m *MastermindGame) BonusXP() int {
+ return (m.maxGuess - len(m.guesses)) * 15
+}
diff --git a/internal/game/hacking/hacking_wumpus.go b/internal/game/hacking/hacking_wumpus.go
new file mode 100644
index 0000000..91b9243
--- /dev/null
+++ b/internal/game/hacking/hacking_wumpus.go
@@ -0,0 +1,286 @@
+package hacking
+
+import (
+ "fmt"
+ "math/rand"
+ "strconv"
+ "strings"
+)
+
+var dodecahedron = [20][3]int{
+ {1, 4, 7},
+ {0, 2, 9},
+ {1, 3, 11},
+ {2, 4, 13},
+ {0, 3, 5},
+ {4, 6, 14},
+ {5, 7, 16},
+ {0, 6, 8},
+ {7, 9, 17},
+ {1, 8, 10},
+ {9, 11, 18},
+ {2, 10, 12},
+ {11, 13, 19},
+ {3, 12, 14},
+ {5, 13, 15},
+ {14, 16, 19},
+ {6, 15, 17},
+ {8, 16, 18},
+ {10, 17, 19},
+ {12, 15, 18},
+}
+
+type WumpusGame struct {
+ player int
+ wumpus int
+ pits [2]int
+ ice [2]int
+ probes int
+ gameOver bool
+ won bool
+}
+
+func NewWumpusGame() *WumpusGame {
+ return &WumpusGame{}
+}
+
+func (w *WumpusGame) Name() string {
+ return "Hunt the Wumpus"
+}
+
+func (w *WumpusGame) Init(level int) string {
+ taken := make(map[int]bool)
+ w.player = rand.Intn(20)
+ taken[w.player] = true
+
+ w.wumpus = randFree(taken)
+ taken[w.wumpus] = true
+
+ w.pits[0] = randFree(taken)
+ taken[w.pits[0]] = true
+ w.pits[1] = randFree(taken)
+ taken[w.pits[1]] = true
+
+ w.ice[0] = randFree(taken)
+ taken[w.ice[0]] = true
+ w.ice[1] = randFree(taken)
+
+ w.probes = 5
+ w.gameOver = false
+ w.won = false
+
+ var sb strings.Builder
+ sb.WriteString("=== HUNT THE ROGUE AI ===\n")
+ sb.WriteString("\n")
+ sb.WriteString("You've jacked into an abandoned network. Somewhere in this maze of\n")
+ sb.WriteString("20 nodes, a rogue AI lurks. You have 5 probes to find and neutralize it.\n")
+ sb.WriteString("\n")
+ sb.WriteString("Hazards:\n")
+ sb.WriteString(" - Rogue AI: Moves to an adjacent node if you enter its node. Kills you.\n")
+ sb.WriteString(" - Data Traps: Fall in and you're fried. 2 in the network.\n")
+ sb.WriteString(" - ICE: Grabs you and dumps you in a random node. 2 in the network.\n")
+ sb.WriteString("\n")
+ sb.WriteString("Commands:\n")
+ sb.WriteString(" move <node> - Move to an adjacent node (1-20)\n")
+ sb.WriteString(" shoot <node> - Launch a probe into an adjacent node (1-20)\n")
+ sb.WriteString(" status - Show your current status\n")
+ sb.WriteString(" map - Show network map\n")
+ sb.WriteString(" jack out - Disconnect (forfeit)\n")
+ sb.WriteString("\n")
+ sb.WriteString(w.roomDesc())
+ return sb.String()
+}
+
+func (w *WumpusGame) HandleInput(input string) (string, bool, bool) {
+ input = strings.TrimSpace(strings.ToLower(input))
+ parts := strings.Fields(input)
+
+ if len(parts) == 0 {
+ return "Unknown command.", false, false
+ }
+
+ cmd := parts[0]
+
+ switch cmd {
+ case "move", "m":
+ if len(parts) < 2 {
+ return "Move where? (e.g., move 3)", false, false
+ }
+ n, err := strconv.Atoi(parts[1])
+ if err != nil || n < 1 || n > 20 {
+ return "Invalid node. Use 1-20.", false, false
+ }
+ return w.doMove(n - 1)
+ case "shoot", "probe", "launch", "s":
+ if len(parts) < 2 {
+ return "Shoot where? (e.g., shoot 3)", false, false
+ }
+ n, err := strconv.Atoi(parts[1])
+ if err != nil || n < 1 || n > 20 {
+ return "Invalid node. Use 1-20.", false, false
+ }
+ return w.doShoot(n - 1)
+ case "status":
+ return w.doStatus(), false, false
+ case "map":
+ return w.doMap(), false, false
+ default:
+ n, err := strconv.Atoi(cmd)
+ if err == nil && n >= 1 && n <= 20 {
+ return w.doMove(n - 1)
+ }
+ return "Commands: move <N>, shoot <N>, status, map, jack out", false, false
+ }
+}
+
+func (w *WumpusGame) doMove(target int) (string, bool, bool) {
+ if !w.hasAdjacent(w.player, target) {
+ adj := w.adjStrings(w.player)
+ return fmt.Sprintf("Can't move there. Adjacent nodes: %s", strings.Join(adj, ", ")), false, false
+ }
+ w.player = target
+ return w.checkRoom()
+}
+
+func (w *WumpusGame) doShoot(target int) (string, bool, bool) {
+ if !w.hasAdjacent(w.player, target) {
+ adj := w.adjStrings(w.player)
+ return fmt.Sprintf("Can't shoot there. Adjacent nodes: %s", strings.Join(adj, ", ")), false, false
+ }
+ w.probes--
+ if target == w.wumpus {
+ w.gameOver = true
+ w.won = true
+ return "Your probe hits the rogue AI! It destabilizes and crashes.\n\nConnection terminated.", true, true
+ }
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("Your probe finds nothing in node %d.", target+1))
+ if w.probes == 0 {
+ sb.WriteString("\nYou're out of probes. The rogue AI detects your presence and terminates your connection.")
+ w.gameOver = true
+ w.won = false
+ return sb.String(), true, false
+ }
+ if rand.Float64() < 0.75 {
+ w.wumpus = w.randomAdjacent(w.wumpus)
+ sb.WriteString("\nYou hear something shift in the network...")
+ }
+ sb.WriteString("\n")
+ sb.WriteString(w.roomDesc())
+ return sb.String(), false, false
+}
+
+func (w *WumpusGame) checkRoom() (string, bool, bool) {
+ var sb strings.Builder
+ cur := w.player
+ if cur == w.wumpus {
+ if rand.Float64() < 0.75 {
+ newRoom := w.randomAdjacent(w.wumpus)
+ w.wumpus = newRoom
+ sb.WriteString("--- Node " + fmt.Sprint(cur+1) + " ---\n")
+ sb.WriteString("The rogue AI stirs and relocates to a nearby node...\n")
+ sb.WriteString("\n")
+ sb.WriteString(w.roomDesc())
+ return sb.String(), false, false
+ }
+ sb.WriteString("The rogue AI devours your connection!\n\nConnection terminated.")
+ w.gameOver = true
+ w.won = false
+ return sb.String(), true, false
+ }
+ for _, p := range w.pits {
+ if cur == p {
+ sb.WriteString("You've fallen into a data trap! Your connection is severed.")
+ w.gameOver = true
+ w.won = false
+ return sb.String(), true, false
+ }
+ }
+ for _, i := range w.ice {
+ if cur == i {
+ sb.WriteString("ICE detected! You're relocated to a random node...\n")
+ w.player = rand.Intn(20)
+ chained, _, _ := w.checkRoom()
+ sb.WriteString(chained)
+ return sb.String(), false, false
+ }
+ }
+ sb.WriteString(w.roomDesc())
+ return sb.String(), false, false
+}
+
+func (w *WumpusGame) roomDesc() string {
+ cur := w.player
+ var sb strings.Builder
+ sb.WriteString(fmt.Sprintf("--- Node %d ---\n", cur+1))
+ adj := w.adjStrings(cur)
+ sb.WriteString(fmt.Sprintf("Tunnels lead to: %s\n", strings.Join(adj, ", ")))
+
+ for _, a := range dodecahedron[cur] {
+ if a == w.wumpus {
+ sb.WriteString("{196}You detect corrupted data nearby...{/}\n")
+ }
+ for _, p := range w.pits {
+ if a == p {
+ sb.WriteString("{208}You sense a void in the network...{/}\n")
+ }
+ }
+ for _, i := range w.ice {
+ if a == i {
+ sb.WriteString("{226}You hear static crackling...{/}\n")
+ }
+ }
+ }
+ return sb.String()
+}
+
+func (w *WumpusGame) doStatus() string {
+ return fmt.Sprintf("Node: %d | Probes: %d/5", w.player+1, w.probes)
+}
+
+func (w *WumpusGame) doMap() string {
+ var sb strings.Builder
+ sb.WriteString("=== Network Map ===\n")
+ for i := 0; i < 20; i++ {
+ marker := " "
+ if i == w.player {
+ marker = "*"
+ }
+ adj := dodecahedron[i]
+ sb.WriteString(fmt.Sprintf(" [%s] Node %2d -> %2d, %2d, %2d\n",
+ marker, i+1, adj[0]+1, adj[1]+1, adj[2]+1))
+ }
+ sb.WriteString(fmt.Sprintf("\nYou are at Node %d.\n", w.player+1))
+ return sb.String()
+}
+
+func (w *WumpusGame) hasAdjacent(from, to int) bool {
+ for _, a := range dodecahedron[from] {
+ if a == to {
+ return true
+ }
+ }
+ return false
+}
+
+func (w *WumpusGame) randomAdjacent(r int) int {
+ adj := dodecahedron[r]
+ return adj[rand.Intn(len(adj))]
+}
+
+func (w *WumpusGame) adjStrings(r int) []string {
+ var out []string
+ for _, a := range dodecahedron[r] {
+ out = append(out, fmt.Sprint(a+1))
+ }
+ return out
+}
+
+func randFree(taken map[int]bool) int {
+ for {
+ r := rand.Intn(20)
+ if !taken[r] {
+ return r
+ }
+ }
+}