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:
@@ -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
247
game/random_event.go
Normal 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
100
game/random_event_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user