feat: replace all hardcoded constants with config values

Replace hardcoded game constants with values from the config system:
- GameSession now receives *config.Config from Lobby
- TurnTimeout, MaxFloors, SkillUses, InventoryLimit use config values
- combat.AttemptFlee accepts fleeChance param
- combat.ResolveAttacks accepts coopBonus param
- entity.NewMonster accepts scaling param
- Solo HP/DEF reduction uses config SoloHPReduction
- Lobby JoinRoom uses config MaxPlayers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 13:08:52 +09:00
parent ad1482ae03
commit f85775dd3e
10 changed files with 56 additions and 43 deletions

View File

@@ -81,21 +81,21 @@ func (s *GameSession) spawnMonsters() {
for i := 0; i < count; i++ {
mt := valid[rand.Intn(len(valid))]
m := entity.NewMonster(mt, floor)
m := entity.NewMonster(mt, floor, s.cfg.Combat.MonsterScaling)
if s.state.SoloMode {
m.HP = m.HP / 2
m.HP = int(float64(m.HP) * s.cfg.Combat.SoloHPReduction)
if m.HP < 1 {
m.HP = 1
}
m.MaxHP = m.HP
m.DEF = m.DEF / 2
m.DEF = int(float64(m.DEF) * s.cfg.Combat.SoloHPReduction)
}
s.state.Monsters[i] = m
}
// Reset skill uses for all players at combat start
for _, p := range s.state.Players {
p.SkillUses = 3
p.SkillUses = s.cfg.Game.SkillUses
}
}
@@ -113,7 +113,7 @@ func (s *GameSession) spawnBoss() {
default:
mt = entity.MonsterBoss5
}
boss := entity.NewMonster(mt, s.state.FloorNum)
boss := entity.NewMonster(mt, s.state.FloorNum, s.cfg.Combat.MonsterScaling)
switch mt {
case entity.MonsterBoss5:
boss.Pattern = entity.PatternAoE
@@ -125,22 +125,22 @@ func (s *GameSession) spawnBoss() {
boss.Pattern = entity.PatternHeal
}
if s.state.SoloMode {
boss.HP = boss.HP / 2
boss.HP = int(float64(boss.HP) * s.cfg.Combat.SoloHPReduction)
boss.MaxHP = boss.HP
boss.DEF = boss.DEF / 2
boss.DEF = int(float64(boss.DEF) * s.cfg.Combat.SoloHPReduction)
}
s.state.Monsters = []*entity.Monster{boss}
// Reset skill uses for all players at combat start
for _, p := range s.state.Players {
p.SkillUses = 3
p.SkillUses = s.cfg.Game.SkillUses
}
}
func (s *GameSession) grantTreasure() {
floor := s.state.FloorNum
for _, p := range s.state.Players {
if len(p.Inventory) >= 10 {
if len(p.Inventory) >= s.cfg.Game.InventoryLimit {
s.addLog(fmt.Sprintf("%s's inventory is full!", p.Name))
continue
}

View File

@@ -53,6 +53,10 @@ func NewLobby(cfg *config.Config) *Lobby {
}
}
func (l *Lobby) Cfg() *config.Config {
return l.cfg
}
func (l *Lobby) RegisterSession(fingerprint, roomCode string) {
l.mu.Lock()
defer l.mu.Unlock()
@@ -144,7 +148,7 @@ func (l *Lobby) JoinRoom(code, playerName, fingerprint string) error {
if !ok {
return fmt.Errorf("room %s not found", code)
}
if len(room.Players) >= 4 {
if len(room.Players) >= l.cfg.Game.MaxPlayers {
return fmt.Errorf("room %s is full", code)
}
if room.Status != RoomWaiting {

View File

@@ -5,6 +5,7 @@ import (
"sync"
"time"
"github.com/tolelom/catacombs/config"
"github.com/tolelom/catacombs/dungeon"
"github.com/tolelom/catacombs/entity"
)
@@ -71,6 +72,7 @@ func (s *GameSession) clearLog() {
type GameSession struct {
mu sync.Mutex
cfg *config.Config
state GameState
started bool
actions map[string]PlayerAction // playerName -> action
@@ -85,8 +87,9 @@ type playerActionMsg struct {
Action PlayerAction
}
func NewGameSession() *GameSession {
func NewGameSession(cfg *config.Config) *GameSession {
return &GameSession{
cfg: cfg,
state: GameState{
FloorNum: 1,
},
@@ -340,7 +343,7 @@ func (s *GameSession) BuyItem(playerID string, itemIdx int) bool {
item := s.state.ShopItems[itemIdx]
for _, p := range s.state.Players {
if p.Fingerprint == playerID && p.Gold >= item.Price {
if len(p.Inventory) >= 10 {
if len(p.Inventory) >= s.cfg.Game.InventoryLimit {
return false
}
p.Gold -= item.Price

View File

@@ -4,11 +4,18 @@ import (
"testing"
"time"
"github.com/tolelom/catacombs/config"
"github.com/tolelom/catacombs/entity"
)
func testCfg(t *testing.T) *config.Config {
t.Helper()
cfg, _ := config.Load("")
return cfg
}
func TestGetStateNoRace(t *testing.T) {
s := NewGameSession()
s := NewGameSession(testCfg(t))
p := entity.NewPlayer("Racer", entity.ClassWarrior)
p.Fingerprint = "test-fp"
s.AddPlayer(p)
@@ -40,7 +47,7 @@ func TestGetStateNoRace(t *testing.T) {
}
func TestSessionTurnTimeout(t *testing.T) {
s := NewGameSession()
s := NewGameSession(testCfg(t))
p := entity.NewPlayer("test", entity.ClassWarrior)
p.Fingerprint = "test-fp"
s.AddPlayer(p)
@@ -62,7 +69,7 @@ func TestSessionTurnTimeout(t *testing.T) {
}
func TestRevealNextLog(t *testing.T) {
s := NewGameSession()
s := NewGameSession(testCfg(t))
// No logs to reveal
if s.RevealNextLog() {
@@ -95,7 +102,7 @@ func TestRevealNextLog(t *testing.T) {
}
func TestDeepCopyIndependence(t *testing.T) {
s := NewGameSession()
s := NewGameSession(testCfg(t))
p := entity.NewPlayer("Test", entity.ClassWarrior)
p.Fingerprint = "fp-test"
p.Inventory = append(p.Inventory, entity.Item{Name: "Sword", Type: entity.ItemWeapon, Bonus: 5})
@@ -118,7 +125,7 @@ func TestDeepCopyIndependence(t *testing.T) {
}
func TestBuyItemInventoryFull(t *testing.T) {
s := NewGameSession()
s := NewGameSession(testCfg(t))
p := entity.NewPlayer("Buyer", entity.ClassWarrior)
p.Fingerprint = "fp-buyer"
p.Gold = 1000
@@ -141,7 +148,7 @@ func TestBuyItemInventoryFull(t *testing.T) {
}
func TestSendChat(t *testing.T) {
s := NewGameSession()
s := NewGameSession(testCfg(t))
s.SendChat("Alice", "hello")
st := s.GetState()
if len(st.CombatLog) != 1 || st.CombatLog[0] != "[Alice] hello" {

View File

@@ -10,8 +10,6 @@ import (
"github.com/tolelom/catacombs/entity"
)
const TurnTimeout = 5 * time.Second
func (s *GameSession) RunTurn() {
s.mu.Lock()
s.state.TurnNum++
@@ -28,9 +26,10 @@ func (s *GameSession) RunTurn() {
s.mu.Unlock()
// Collect actions with timeout
timer := time.NewTimer(TurnTimeout)
turnTimeout := time.Duration(s.cfg.Game.TurnTimeoutSec) * time.Second
timer := time.NewTimer(turnTimeout)
s.mu.Lock()
s.state.TurnDeadline = time.Now().Add(TurnTimeout)
s.state.TurnDeadline = time.Now().Add(turnTimeout)
s.mu.Unlock()
collected := 0
@@ -180,7 +179,7 @@ func (s *GameSession) resolvePlayerActions() {
s.addLog(fmt.Sprintf("%s has no items to use!", p.Name))
}
case ActionFlee:
if combat.AttemptFlee() {
if combat.AttemptFlee(s.cfg.Combat.FleeChance) {
s.addLog(fmt.Sprintf("%s fled from battle!", p.Name))
s.state.FleeSucceeded = true
if s.state.SoloMode {
@@ -215,7 +214,7 @@ func (s *GameSession) resolvePlayerActions() {
}
if len(intents) > 0 && len(s.state.Monsters) > 0 {
results := combat.ResolveAttacks(intents, s.state.Monsters)
results := combat.ResolveAttacks(intents, s.state.Monsters, s.cfg.Game.CoopBonus)
for i, r := range results {
owner := intentOwners[i]
if r.IsAoE {
@@ -287,7 +286,7 @@ func (s *GameSession) resolvePlayerActions() {
}
func (s *GameSession) advanceFloor() {
if s.state.FloorNum >= 20 {
if s.state.FloorNum >= s.cfg.Game.MaxFloors {
s.state.Phase = PhaseResult
s.state.Victory = true
s.state.GameOver = true