feat: add configuration package with YAML loading and defaults

Add config package that loads game settings from YAML files with
sensible defaults for server, game, combat, dungeon, and backup
settings. Includes config.yaml with all defaults documented.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 12:59:44 +09:00
parent 089d5c76ed
commit 0f524779c0
5 changed files with 206 additions and 0 deletions

44
config.yaml Normal file
View File

@@ -0,0 +1,44 @@
# Catacombs Configuration
# All values shown are defaults. Uncomment and modify as needed.
server:
# SSH port for game client connections
ssh_port: 2222
# HTTP port for web interface
http_port: 8080
game:
# Seconds allowed per player turn
turn_timeout_sec: 5
# Maximum players per game session
max_players: 4
# Maximum dungeon floors
max_floors: 20
# Cooperative play bonus multiplier
coop_bonus: 0.10
# Maximum items a player can carry
inventory_limit: 10
# Number of skill uses per floor
skill_uses: 3
combat:
# Probability of successfully fleeing combat
flee_chance: 0.50
# Monster stat scaling per floor
monster_scaling: 1.15
# HP reduction when playing solo
solo_hp_reduction: 0.50
dungeon:
# Map dimensions in tiles
map_width: 60
map_height: 20
# Room count range per floor
min_rooms: 5
max_rooms: 8
backup:
# Minutes between automatic backups
interval_min: 60
# Directory for backup files
dir: "./data/backup"

75
config/config.go Normal file
View File

@@ -0,0 +1,75 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Game GameConfig `yaml:"game"`
Combat CombatConfig `yaml:"combat"`
Dungeon DungeonConfig `yaml:"dungeon"`
Backup BackupConfig `yaml:"backup"`
}
type ServerConfig struct {
SSHPort int `yaml:"ssh_port"`
HTTPPort int `yaml:"http_port"`
}
type GameConfig struct {
TurnTimeoutSec int `yaml:"turn_timeout_sec"`
MaxPlayers int `yaml:"max_players"`
MaxFloors int `yaml:"max_floors"`
CoopBonus float64 `yaml:"coop_bonus"`
InventoryLimit int `yaml:"inventory_limit"`
SkillUses int `yaml:"skill_uses"`
}
type CombatConfig struct {
FleeChance float64 `yaml:"flee_chance"`
MonsterScaling float64 `yaml:"monster_scaling"`
SoloHPReduction float64 `yaml:"solo_hp_reduction"`
}
type DungeonConfig struct {
MapWidth int `yaml:"map_width"`
MapHeight int `yaml:"map_height"`
MinRooms int `yaml:"min_rooms"`
MaxRooms int `yaml:"max_rooms"`
}
type BackupConfig struct {
IntervalMin int `yaml:"interval_min"`
Dir string `yaml:"dir"`
}
func defaults() Config {
return Config{
Server: ServerConfig{SSHPort: 2222, HTTPPort: 8080},
Game: GameConfig{
TurnTimeoutSec: 5, MaxPlayers: 4, MaxFloors: 20,
CoopBonus: 0.10, InventoryLimit: 10, SkillUses: 3,
},
Combat: CombatConfig{FleeChance: 0.50, MonsterScaling: 1.15, SoloHPReduction: 0.50},
Dungeon: DungeonConfig{MapWidth: 60, MapHeight: 20, MinRooms: 5, MaxRooms: 8},
Backup: BackupConfig{IntervalMin: 60, Dir: "./data/backup"},
}
}
func Load(path string) (*Config, error) {
cfg := defaults()
if path == "" {
return &cfg, nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}

85
config/config_test.go Normal file
View File

@@ -0,0 +1,85 @@
package config
import (
"os"
"testing"
)
func TestLoadDefaults(t *testing.T) {
cfg, err := Load("")
if err != nil {
t.Fatal(err)
}
if cfg.Server.SSHPort != 2222 {
t.Errorf("expected SSH port 2222, got %d", cfg.Server.SSHPort)
}
if cfg.Server.HTTPPort != 8080 {
t.Errorf("expected HTTP port 8080, got %d", cfg.Server.HTTPPort)
}
if cfg.Game.TurnTimeoutSec != 5 {
t.Errorf("expected turn timeout 5, got %d", cfg.Game.TurnTimeoutSec)
}
if cfg.Game.MaxPlayers != 4 {
t.Errorf("expected max players 4, got %d", cfg.Game.MaxPlayers)
}
if cfg.Game.MaxFloors != 20 {
t.Errorf("expected max floors 20, got %d", cfg.Game.MaxFloors)
}
if cfg.Game.CoopBonus != 0.10 {
t.Errorf("expected coop bonus 0.10, got %f", cfg.Game.CoopBonus)
}
if cfg.Game.InventoryLimit != 10 {
t.Errorf("expected inventory limit 10, got %d", cfg.Game.InventoryLimit)
}
if cfg.Combat.FleeChance != 0.50 {
t.Errorf("expected flee chance 0.50, got %f", cfg.Combat.FleeChance)
}
if cfg.Combat.MonsterScaling != 1.15 {
t.Errorf("expected monster scaling 1.15, got %f", cfg.Combat.MonsterScaling)
}
}
func TestLoadFromFile(t *testing.T) {
content := []byte(`
server:
ssh_port: 3333
http_port: 9090
game:
turn_timeout_sec: 10
max_players: 2
`)
f, err := os.CreateTemp("", "config-*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
f.Write(content)
f.Close()
cfg, err := Load(f.Name())
if err != nil {
t.Fatal(err)
}
if cfg.Server.SSHPort != 3333 {
t.Errorf("expected SSH port 3333, got %d", cfg.Server.SSHPort)
}
if cfg.Server.HTTPPort != 9090 {
t.Errorf("expected HTTP port 9090, got %d", cfg.Server.HTTPPort)
}
if cfg.Game.TurnTimeoutSec != 10 {
t.Errorf("expected turn timeout 10, got %d", cfg.Game.TurnTimeoutSec)
}
if cfg.Game.MaxPlayers != 2 {
t.Errorf("expected max players 2, got %d", cfg.Game.MaxPlayers)
}
// Unset fields should have defaults
if cfg.Game.MaxFloors != 20 {
t.Errorf("expected default max floors 20, got %d", cfg.Game.MaxFloors)
}
if cfg.Combat.FleeChance != 0.50 {
t.Errorf("expected default flee chance 0.50, got %f", cfg.Combat.FleeChance)
}
if cfg.Dungeon.MapWidth != 60 {
t.Errorf("expected default map width 60, got %d", cfg.Dungeon.MapWidth)
}
}

1
go.mod
View File

@@ -41,4 +41,5 @@ require (
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

1
go.sum
View File

@@ -79,5 +79,6 @@ golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=