From 0f524779c087e0d6f5de3a949460f190b0b3cded Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 12:59:44 +0900 Subject: [PATCH] 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) --- config.yaml | 44 ++++++++++++++++++++++ config/config.go | 75 ++++++++++++++++++++++++++++++++++++++ config/config_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 1 + 5 files changed, 206 insertions(+) create mode 100644 config.yaml create mode 100644 config/config.go create mode 100644 config/config_test.go diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..e03c8e2 --- /dev/null +++ b/config.yaml @@ -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" diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..d9b3e54 --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..4468167 --- /dev/null +++ b/config/config_test.go @@ -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) + } +} diff --git a/go.mod b/go.mod index 8a21088..4c8d1d7 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 5d09ced..6bbf631 100644 --- a/go.sum +++ b/go.sum @@ -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=