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:
44
config.yaml
Normal file
44
config.yaml
Normal 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
75
config/config.go
Normal 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
85
config/config_test.go
Normal 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
1
go.mod
@@ -41,4 +41,5 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
1
go.sum
1
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/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 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
Reference in New Issue
Block a user