feat: add 8 random event rooms with choice-based outcomes

Implement RandomEvent system with 8 events (altar, fountain, merchant,
trap_room, shrine, chest, ghost, mushroom), each with 2-3 choices that
resolve based on floor number. Update triggerEvent() to use the new
system, auto-resolving a random choice on a random alive player.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 15:27:07 +09:00
parent 1e155c62fb
commit e167165bbc
3 changed files with 401 additions and 12 deletions

View File

@@ -212,21 +212,63 @@ func armorName(floor int) string {
}
func (s *GameSession) triggerEvent() {
event := PickRandomEvent()
s.addLog(fmt.Sprintf("Event: %s — %s", event.Name, event.Description))
// Auto-resolve with a random choice
choice := event.Choices[rand.Intn(len(event.Choices))]
outcome := choice.Resolve(s.state.FloorNum)
s.addLog(fmt.Sprintf(" → %s: %s", choice.Label, outcome.Description))
// Pick a random alive player to apply the outcome
var alive []*entity.Player
for _, p := range s.state.Players {
if p.IsDead() {
continue
if !p.IsDead() {
alive = append(alive, p)
}
if rand.Float64() < 0.5 {
baseDmg := 10 + s.state.FloorNum
dmg := baseDmg + rand.Intn(baseDmg/2+1)
p.TakeDamage(dmg)
s.addLog(fmt.Sprintf("Trap! %s takes %d damage", p.Name, dmg))
}
if len(alive) == 0 {
return
}
target := alive[rand.Intn(len(alive))]
if outcome.HPChange > 0 {
before := target.HP
target.Heal(outcome.HPChange)
s.addLog(fmt.Sprintf(" %s heals %d HP", target.Name, target.HP-before))
} else if outcome.HPChange < 0 {
target.TakeDamage(-outcome.HPChange)
s.addLog(fmt.Sprintf(" %s takes %d damage", target.Name, -outcome.HPChange))
}
if outcome.GoldChange != 0 {
target.Gold += outcome.GoldChange
if target.Gold < 0 {
target.Gold = 0
}
if outcome.GoldChange > 0 {
s.addLog(fmt.Sprintf(" %s gains %d gold", target.Name, outcome.GoldChange))
} else {
baseHeal := 15 + s.state.FloorNum
heal := baseHeal + rand.Intn(baseHeal/2+1)
before := p.HP
p.Heal(heal)
s.addLog(fmt.Sprintf("Blessing! %s heals %d HP", p.Name, p.HP-before))
s.addLog(fmt.Sprintf(" %s loses %d gold", target.Name, -outcome.GoldChange))
}
}
if outcome.ItemDrop {
if len(target.Inventory) < s.cfg.Game.InventoryLimit {
floor := s.state.FloorNum
if rand.Float64() < 0.5 {
bonus := 3 + rand.Intn(6) + floor/3
item := entity.Item{Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus}
target.Inventory = append(target.Inventory, item)
s.addLog(fmt.Sprintf(" %s found %s (ATK+%d)", target.Name, item.Name, item.Bonus))
} else {
bonus := 2 + rand.Intn(4) + floor/4
item := entity.Item{Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus}
target.Inventory = append(target.Inventory, item)
s.addLog(fmt.Sprintf(" %s found %s (DEF+%d)", target.Name, item.Name, item.Bonus))
}
} else {
s.addLog(fmt.Sprintf(" %s's inventory is full!", target.Name))
}
}
}

247
game/random_event.go Normal file
View File

@@ -0,0 +1,247 @@
package game
import "math/rand"
// EventOutcome describes the result of choosing an event option.
type EventOutcome struct {
HPChange int
GoldChange int
ItemDrop bool
Description string
}
// EventChoice represents a single choice the player can make during an event.
type EventChoice struct {
Label string
Resolve func(floor int) EventOutcome
}
// RandomEvent represents a random event with multiple choices.
type RandomEvent struct {
Name string
Description string
Choices []EventChoice
}
// GetRandomEvents returns all 8 defined random events.
func GetRandomEvents() []RandomEvent {
return []RandomEvent{
{
Name: "altar",
Description: "You discover an ancient altar glowing with strange energy.",
Choices: []EventChoice{
{
Label: "Pray at the altar",
Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.6 {
heal := 15 + floor*2
return EventOutcome{HPChange: heal, Description: "The altar blesses you with healing light."}
}
dmg := 10 + floor
return EventOutcome{HPChange: -dmg, Description: "The altar's energy lashes out at you!"}
},
},
{
Label: "Offer gold",
Resolve: func(floor int) EventOutcome {
cost := 10 + floor
return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "You offer gold and receive a divine gift."}
},
},
{
Label: "Walk away",
Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "You leave the altar undisturbed."}
},
},
},
},
{
Name: "fountain",
Description: "A shimmering fountain bubbles in the center of the room.",
Choices: []EventChoice{
{
Label: "Drink from the fountain",
Resolve: func(floor int) EventOutcome {
heal := 20 + floor*2
return EventOutcome{HPChange: heal, Description: "The water rejuvenates you!"}
},
},
{
Label: "Toss a coin",
Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.5 {
gold := 15 + floor*3
return EventOutcome{GoldChange: gold, Description: "The fountain rewards your generosity!"}
}
return EventOutcome{GoldChange: -5, Description: "The coin sinks and nothing happens."}
},
},
},
},
{
Name: "merchant",
Description: "A hooded merchant appears from the shadows.",
Choices: []EventChoice{
{
Label: "Trade gold for healing",
Resolve: func(floor int) EventOutcome {
cost := 15 + floor
heal := 25 + floor*2
return EventOutcome{HPChange: heal, GoldChange: -cost, Description: "The merchant sells you a healing draught."}
},
},
{
Label: "Buy a mystery item",
Resolve: func(floor int) EventOutcome {
cost := 20 + floor*2
return EventOutcome{GoldChange: -cost, ItemDrop: true, Description: "The merchant hands you a wrapped package."}
},
},
{
Label: "Decline",
Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "The merchant vanishes into the shadows."}
},
},
},
},
{
Name: "trap_room",
Description: "The floor is covered with suspicious pressure plates.",
Choices: []EventChoice{
{
Label: "Carefully navigate",
Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.5 {
return EventOutcome{Description: "You skillfully avoid all the traps!"}
}
dmg := 8 + floor
return EventOutcome{HPChange: -dmg, Description: "You trigger a trap and take damage!"}
},
},
{
Label: "Rush through",
Resolve: func(floor int) EventOutcome {
dmg := 5 + floor/2
gold := 10 + floor*2
return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "You take minor damage but find hidden gold!"}
},
},
},
},
{
Name: "shrine",
Description: "A glowing shrine hums with divine power.",
Choices: []EventChoice{
{
Label: "Kneel and pray",
Resolve: func(floor int) EventOutcome {
heal := 30 + floor*2
return EventOutcome{HPChange: heal, Description: "The shrine fills you with renewed vigor!"}
},
},
{
Label: "Take the offering",
Resolve: func(floor int) EventOutcome {
gold := 20 + floor*3
dmg := 15 + floor
return EventOutcome{HPChange: -dmg, GoldChange: gold, Description: "You steal the offering but anger the spirits!"}
},
},
},
},
{
Name: "chest",
Description: "An ornate chest sits in the corner of the room.",
Choices: []EventChoice{
{
Label: "Open carefully",
Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.7 {
gold := 15 + floor*2
return EventOutcome{GoldChange: gold, Description: "The chest contains a pile of gold!"}
}
dmg := 12 + floor
return EventOutcome{HPChange: -dmg, Description: "The chest was a mimic! It bites you!"}
},
},
{
Label: "Smash it open",
Resolve: func(floor int) EventOutcome {
return EventOutcome{ItemDrop: true, Description: "You smash the chest and find equipment inside!"}
},
},
{
Label: "Leave it",
Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "Better safe than sorry."}
},
},
},
},
{
Name: "ghost",
Description: "A spectral figure materializes before you.",
Choices: []EventChoice{
{
Label: "Speak with the ghost",
Resolve: func(floor int) EventOutcome {
gold := 10 + floor*2
return EventOutcome{GoldChange: gold, Description: "The ghost thanks you for listening and rewards you."}
},
},
{
Label: "Attack the ghost",
Resolve: func(floor int) EventOutcome {
if rand.Float64() < 0.4 {
return EventOutcome{ItemDrop: true, Description: "The ghost drops a spectral weapon as it fades!"}
}
dmg := 15 + floor
return EventOutcome{HPChange: -dmg, Description: "The ghost retaliates with ghostly fury!"}
},
},
},
},
{
Name: "mushroom",
Description: "Strange glowing mushrooms grow in clusters here.",
Choices: []EventChoice{
{
Label: "Eat a mushroom",
Resolve: func(floor int) EventOutcome {
r := rand.Float64()
if r < 0.33 {
heal := 20 + floor*2
return EventOutcome{HPChange: heal, Description: "The mushroom tastes great and heals you!"}
} else if r < 0.66 {
dmg := 10 + floor
return EventOutcome{HPChange: -dmg, Description: "The mushroom was poisonous!"}
}
gold := 10 + floor
return EventOutcome{GoldChange: gold, Description: "The mushroom gives you strange visions... and gold falls from above!"}
},
},
{
Label: "Collect and sell",
Resolve: func(floor int) EventOutcome {
gold := 8 + floor
return EventOutcome{GoldChange: gold, Description: "You carefully harvest the mushrooms for sale."}
},
},
{
Label: "Ignore them",
Resolve: func(floor int) EventOutcome {
return EventOutcome{Description: "You wisely avoid the mysterious fungi."}
},
},
},
},
}
}
// PickRandomEvent returns a random event from the list.
func PickRandomEvent() RandomEvent {
events := GetRandomEvents()
return events[rand.Intn(len(events))]
}

100
game/random_event_test.go Normal file
View File

@@ -0,0 +1,100 @@
package game
import "testing"
func TestGetRandomEvents(t *testing.T) {
events := GetRandomEvents()
if len(events) < 8 {
t.Errorf("Expected at least 8 random events, got %d", len(events))
}
for _, e := range events {
if len(e.Choices) < 2 {
t.Errorf("Event %q should have at least 2 choices, got %d", e.Name, len(e.Choices))
}
if e.Name == "" {
t.Error("Event name should not be empty")
}
if e.Description == "" {
t.Errorf("Event %q description should not be empty", e.Name)
}
}
}
func TestResolveChoice(t *testing.T) {
events := GetRandomEvents()
// Test altar "Walk away" — always safe
var altar RandomEvent
for _, e := range events {
if e.Name == "altar" {
altar = e
break
}
}
if altar.Name == "" {
t.Fatal("altar event not found")
}
// The third choice (index 2) is "Walk away" — should have no HP/gold change
outcome := altar.Choices[2].Resolve(5)
if outcome.HPChange != 0 || outcome.GoldChange != 0 {
t.Errorf("Walk away should have no changes, got HP=%d Gold=%d", outcome.HPChange, outcome.GoldChange)
}
// Test fountain "Drink" — always heals
var fountain RandomEvent
for _, e := range events {
if e.Name == "fountain" {
fountain = e
break
}
}
if fountain.Name == "" {
t.Fatal("fountain event not found")
}
drinkOutcome := fountain.Choices[0].Resolve(10)
if drinkOutcome.HPChange <= 0 {
t.Errorf("Drinking from fountain should heal, got HP=%d", drinkOutcome.HPChange)
}
// Test shrine "Kneel and pray" — always heals
var shrine RandomEvent
for _, e := range events {
if e.Name == "shrine" {
shrine = e
break
}
}
if shrine.Name == "" {
t.Fatal("shrine event not found")
}
prayOutcome := shrine.Choices[0].Resolve(5)
if prayOutcome.HPChange <= 0 {
t.Errorf("Praying at shrine should heal, got HP=%d", prayOutcome.HPChange)
}
// Test chest "Smash it open" — always gives item
var chest RandomEvent
for _, e := range events {
if e.Name == "chest" {
chest = e
break
}
}
if chest.Name == "" {
t.Fatal("chest event not found")
}
smashOutcome := chest.Choices[1].Resolve(5)
if !smashOutcome.ItemDrop {
t.Error("Smashing chest should always give an item drop")
}
}
func TestPickRandomEvent(t *testing.T) {
event := PickRandomEvent()
if event.Name == "" {
t.Error("PickRandomEvent should return a valid event")
}
if len(event.Choices) < 2 {
t.Errorf("Picked event should have at least 2 choices, got %d", len(event.Choices))
}
}