From e4400c5f1e84c18d2126f2cb502ae10685977cbb Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 20 Jun 2026 03:20:01 -0400 Subject: refactor: combined all station crafting --- internal/game/hacking/hacking_liars_dice.go | 395 ---------------------------- internal/game/hacking/hacking_mastermind.go | 177 ------------- internal/game/hacking/hacking_wumpus.go | 286 -------------------- internal/game/hacking/liars_dice.go | 395 ++++++++++++++++++++++++++++ internal/game/hacking/mastermind.go | 177 +++++++++++++ internal/game/hacking/wumpus.go | 286 ++++++++++++++++++++ 6 files changed, 858 insertions(+), 858 deletions(-) delete mode 100644 internal/game/hacking/hacking_liars_dice.go delete mode 100644 internal/game/hacking/hacking_mastermind.go delete mode 100644 internal/game/hacking/hacking_wumpus.go create mode 100644 internal/game/hacking/liars_dice.go create mode 100644 internal/game/hacking/mastermind.go create mode 100644 internal/game/hacking/wumpus.go (limited to 'internal/game/hacking') diff --git a/internal/game/hacking/hacking_liars_dice.go b/internal/game/hacking/hacking_liars_dice.go deleted file mode 100644 index bcfbd40..0000000 --- a/internal/game/hacking/hacking_liars_dice.go +++ /dev/null @@ -1,395 +0,0 @@ -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 - 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 , call, exact, status, jack out", false, false - } - - cmd := parts[0] - - switch cmd { - case "bid", "b": - if len(parts) < 3 { - return "Usage: bid (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 (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 , 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 deleted file mode 100644 index 8d4eb1c..0000000 --- a/internal/game/hacking/hacking_mastermind.go +++ /dev/null @@ -1,177 +0,0 @@ -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 deleted file mode 100644 index 91b9243..0000000 --- a/internal/game/hacking/hacking_wumpus.go +++ /dev/null @@ -1,286 +0,0 @@ -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 - Move to an adjacent node (1-20)\n") - sb.WriteString(" shoot - 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 , shoot , 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 - } - } -} diff --git a/internal/game/hacking/liars_dice.go b/internal/game/hacking/liars_dice.go new file mode 100644 index 0000000..bcfbd40 --- /dev/null +++ b/internal/game/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 - 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 , call, exact, status, jack out", false, false + } + + cmd := parts[0] + + switch cmd { + case "bid", "b": + if len(parts) < 3 { + return "Usage: bid (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 (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 , 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/mastermind.go b/internal/game/hacking/mastermind.go new file mode 100644 index 0000000..8d4eb1c --- /dev/null +++ b/internal/game/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/wumpus.go b/internal/game/hacking/wumpus.go new file mode 100644 index 0000000..91b9243 --- /dev/null +++ b/internal/game/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 - Move to an adjacent node (1-20)\n") + sb.WriteString(" shoot - 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 , shoot , 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 + } + } +} -- cgit v1.2.3