create control package
This commit is contained in:
parent
906042a67e
commit
42e090b05f
203
internal/control/control.go
Normal file
203
internal/control/control.go
Normal file
@ -0,0 +1,203 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dk/internal/database"
|
||||
|
||||
"zombiezen.com/go/sqlite"
|
||||
)
|
||||
|
||||
// Control represents the game control settings in the database
|
||||
// There is only ever one control record with ID 1
|
||||
type Control struct {
|
||||
ID int `json:"id"`
|
||||
WorldSize int `json:"world_size"`
|
||||
Open int `json:"open"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
Class1Name string `json:"class_1_name"`
|
||||
Class2Name string `json:"class_2_name"`
|
||||
Class3Name string `json:"class_3_name"`
|
||||
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// Find retrieves the control record by ID (typically only ID 1 exists)
|
||||
func Find(db *database.DB, id int) (*Control, error) {
|
||||
control := &Control{db: db}
|
||||
|
||||
query := "SELECT id, world_size, open, admin_email, class_1_name, class_2_name, class_3_name FROM control WHERE id = ?"
|
||||
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||
control.ID = stmt.ColumnInt(0)
|
||||
control.WorldSize = stmt.ColumnInt(1)
|
||||
control.Open = stmt.ColumnInt(2)
|
||||
control.AdminEmail = stmt.ColumnText(3)
|
||||
control.Class1Name = stmt.ColumnText(4)
|
||||
control.Class2Name = stmt.ColumnText(5)
|
||||
control.Class3Name = stmt.ColumnText(6)
|
||||
return nil
|
||||
}, id)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find control: %w", err)
|
||||
}
|
||||
|
||||
if control.ID == 0 {
|
||||
return nil, fmt.Errorf("control with ID %d not found", id)
|
||||
}
|
||||
|
||||
return control, nil
|
||||
}
|
||||
|
||||
// Get retrieves the main control record (ID 1)
|
||||
func Get(db *database.DB) (*Control, error) {
|
||||
return Find(db, 1)
|
||||
}
|
||||
|
||||
// Save updates the control record in the database
|
||||
func (c *Control) Save() error {
|
||||
if c.ID == 0 {
|
||||
return fmt.Errorf("cannot save control without ID")
|
||||
}
|
||||
|
||||
query := `UPDATE control SET world_size = ?, open = ?, admin_email = ?, class_1_name = ?, class_2_name = ?, class_3_name = ? WHERE id = ?`
|
||||
return c.db.Exec(query, c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name, c.ID)
|
||||
}
|
||||
|
||||
// IsOpen returns true if the game world is open for new players
|
||||
func (c *Control) IsOpen() bool {
|
||||
return c.Open == 1
|
||||
}
|
||||
|
||||
// SetOpen sets whether the game world is open for new players
|
||||
func (c *Control) SetOpen(open bool) {
|
||||
if open {
|
||||
c.Open = 1
|
||||
} else {
|
||||
c.Open = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the game world to new players
|
||||
func (c *Control) Close() {
|
||||
c.Open = 0
|
||||
}
|
||||
|
||||
// OpenWorld opens the game world to new players
|
||||
func (c *Control) OpenWorld() {
|
||||
c.Open = 1
|
||||
}
|
||||
|
||||
// GetClassNames returns all class names as a slice
|
||||
func (c *Control) GetClassNames() []string {
|
||||
classes := make([]string, 0, 3)
|
||||
if c.Class1Name != "" {
|
||||
classes = append(classes, c.Class1Name)
|
||||
}
|
||||
if c.Class2Name != "" {
|
||||
classes = append(classes, c.Class2Name)
|
||||
}
|
||||
if c.Class3Name != "" {
|
||||
classes = append(classes, c.Class3Name)
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
// SetClassNames sets all class names from a slice
|
||||
func (c *Control) SetClassNames(classes []string) {
|
||||
// Reset all class names
|
||||
c.Class1Name = ""
|
||||
c.Class2Name = ""
|
||||
c.Class3Name = ""
|
||||
|
||||
// Set provided class names
|
||||
if len(classes) > 0 {
|
||||
c.Class1Name = classes[0]
|
||||
}
|
||||
if len(classes) > 1 {
|
||||
c.Class2Name = classes[1]
|
||||
}
|
||||
if len(classes) > 2 {
|
||||
c.Class3Name = classes[2]
|
||||
}
|
||||
}
|
||||
|
||||
// GetClassName returns the name of a specific class (1-3)
|
||||
func (c *Control) GetClassName(classNum int) string {
|
||||
switch classNum {
|
||||
case 1:
|
||||
return c.Class1Name
|
||||
case 2:
|
||||
return c.Class2Name
|
||||
case 3:
|
||||
return c.Class3Name
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// SetClassName sets the name of a specific class (1-3)
|
||||
func (c *Control) SetClassName(classNum int, name string) bool {
|
||||
switch classNum {
|
||||
case 1:
|
||||
c.Class1Name = name
|
||||
return true
|
||||
case 2:
|
||||
c.Class2Name = name
|
||||
return true
|
||||
case 3:
|
||||
c.Class3Name = name
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidClassName returns true if the given name matches one of the configured classes
|
||||
func (c *Control) IsValidClassName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name
|
||||
}
|
||||
|
||||
// GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found
|
||||
func (c *Control) GetClassNumber(name string) int {
|
||||
if name == c.Class1Name && name != "" {
|
||||
return 1
|
||||
}
|
||||
if name == c.Class2Name && name != "" {
|
||||
return 2
|
||||
}
|
||||
if name == c.Class3Name && name != "" {
|
||||
return 3
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// HasAdminEmail returns true if an admin email is configured
|
||||
func (c *Control) HasAdminEmail() bool {
|
||||
return c.AdminEmail != ""
|
||||
}
|
||||
|
||||
// IsWorldSizeValid returns true if the world size is within reasonable bounds
|
||||
func (c *Control) IsWorldSizeValid() bool {
|
||||
return c.WorldSize > 0 && c.WorldSize <= 10000
|
||||
}
|
||||
|
||||
// GetWorldRadius returns the world radius (half the world size)
|
||||
func (c *Control) GetWorldRadius() int {
|
||||
return c.WorldSize / 2
|
||||
}
|
||||
|
||||
// IsWithinWorldBounds returns true if the given coordinates are within world bounds
|
||||
func (c *Control) IsWithinWorldBounds(x, y int) bool {
|
||||
radius := c.GetWorldRadius()
|
||||
return x >= -radius && x <= radius && y >= -radius && y <= radius
|
||||
}
|
||||
|
||||
// GetWorldBounds returns the minimum and maximum coordinates for the world
|
||||
func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) {
|
||||
radius := c.GetWorldRadius()
|
||||
return -radius, -radius, radius, radius
|
||||
}
|
418
internal/control/control_test.go
Normal file
418
internal/control/control_test.go
Normal file
@ -0,0 +1,418 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"dk/internal/database"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *database.DB {
|
||||
testDB := "test_control.db"
|
||||
t.Cleanup(func() {
|
||||
os.Remove(testDB)
|
||||
})
|
||||
|
||||
db, err := database.Open(testDB)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test database: %v", err)
|
||||
}
|
||||
|
||||
// Create control table
|
||||
createTable := `CREATE TABLE control (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
world_size INTEGER NOT NULL DEFAULT 250,
|
||||
open INTEGER NOT NULL DEFAULT 1,
|
||||
admin_email TEXT NOT NULL DEFAULT '',
|
||||
class_1_name TEXT NOT NULL DEFAULT '',
|
||||
class_2_name TEXT NOT NULL DEFAULT '',
|
||||
class_3_name TEXT NOT NULL DEFAULT ''
|
||||
)`
|
||||
|
||||
if err := db.Exec(createTable); err != nil {
|
||||
t.Fatalf("Failed to create control table: %v", err)
|
||||
}
|
||||
|
||||
// Insert default control record
|
||||
insertControl := `INSERT INTO control VALUES (1, 250, 1, 'admin@example.com', 'Mage', 'Warrior', 'Paladin')`
|
||||
|
||||
if err := db.Exec(insertControl); err != nil {
|
||||
t.Fatalf("Failed to insert test control: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// Test finding existing control record
|
||||
control, err := Find(db, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to find control: %v", err)
|
||||
}
|
||||
|
||||
if control.ID != 1 {
|
||||
t.Errorf("Expected ID 1, got %d", control.ID)
|
||||
}
|
||||
if control.WorldSize != 250 {
|
||||
t.Errorf("Expected world size 250, got %d", control.WorldSize)
|
||||
}
|
||||
if control.Open != 1 {
|
||||
t.Errorf("Expected open 1, got %d", control.Open)
|
||||
}
|
||||
if control.AdminEmail != "admin@example.com" {
|
||||
t.Errorf("Expected admin email 'admin@example.com', got '%s'", control.AdminEmail)
|
||||
}
|
||||
if control.Class1Name != "Mage" {
|
||||
t.Errorf("Expected class 1 name 'Mage', got '%s'", control.Class1Name)
|
||||
}
|
||||
if control.Class2Name != "Warrior" {
|
||||
t.Errorf("Expected class 2 name 'Warrior', got '%s'", control.Class2Name)
|
||||
}
|
||||
if control.Class3Name != "Paladin" {
|
||||
t.Errorf("Expected class 3 name 'Paladin', got '%s'", control.Class3Name)
|
||||
}
|
||||
|
||||
// Test finding non-existent control record
|
||||
_, err = Find(db, 999)
|
||||
if err == nil {
|
||||
t.Error("Expected error when finding non-existent control")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// Test getting main control record
|
||||
control, err := Get(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get control: %v", err)
|
||||
}
|
||||
|
||||
if control.ID != 1 {
|
||||
t.Errorf("Expected ID 1, got %d", control.ID)
|
||||
}
|
||||
if control.WorldSize != 250 {
|
||||
t.Errorf("Expected world size 250, got %d", control.WorldSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
control, err := Get(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get control: %v", err)
|
||||
}
|
||||
|
||||
// Modify control settings
|
||||
control.WorldSize = 500
|
||||
control.Open = 0
|
||||
control.AdminEmail = "newadmin@example.com"
|
||||
control.Class1Name = "Wizard"
|
||||
control.Class2Name = "Knight"
|
||||
control.Class3Name = "Cleric"
|
||||
|
||||
// Save changes
|
||||
err = control.Save()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save control: %v", err)
|
||||
}
|
||||
|
||||
// Verify changes were saved
|
||||
updatedControl, err := Get(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get updated control: %v", err)
|
||||
}
|
||||
|
||||
if updatedControl.WorldSize != 500 {
|
||||
t.Errorf("Expected updated world size 500, got %d", updatedControl.WorldSize)
|
||||
}
|
||||
if updatedControl.Open != 0 {
|
||||
t.Errorf("Expected updated open 0, got %d", updatedControl.Open)
|
||||
}
|
||||
if updatedControl.AdminEmail != "newadmin@example.com" {
|
||||
t.Errorf("Expected updated admin email 'newadmin@example.com', got '%s'", updatedControl.AdminEmail)
|
||||
}
|
||||
if updatedControl.Class1Name != "Wizard" {
|
||||
t.Errorf("Expected updated class 1 name 'Wizard', got '%s'", updatedControl.Class1Name)
|
||||
}
|
||||
if updatedControl.Class2Name != "Knight" {
|
||||
t.Errorf("Expected updated class 2 name 'Knight', got '%s'", updatedControl.Class2Name)
|
||||
}
|
||||
if updatedControl.Class3Name != "Cleric" {
|
||||
t.Errorf("Expected updated class 3 name 'Cleric', got '%s'", updatedControl.Class3Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenMethods(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
control, _ := Get(db)
|
||||
|
||||
// Test IsOpen
|
||||
if !control.IsOpen() {
|
||||
t.Error("Expected control to be open initially")
|
||||
}
|
||||
|
||||
// Test SetOpen
|
||||
control.SetOpen(false)
|
||||
if control.IsOpen() {
|
||||
t.Error("Expected control to be closed after SetOpen(false)")
|
||||
}
|
||||
|
||||
control.SetOpen(true)
|
||||
if !control.IsOpen() {
|
||||
t.Error("Expected control to be open after SetOpen(true)")
|
||||
}
|
||||
|
||||
// Test Close
|
||||
control.Close()
|
||||
if control.IsOpen() {
|
||||
t.Error("Expected control to be closed after Close()")
|
||||
}
|
||||
|
||||
// Test OpenWorld
|
||||
control.OpenWorld()
|
||||
if !control.IsOpen() {
|
||||
t.Error("Expected control to be open after OpenWorld()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassMethods(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
control, _ := Get(db)
|
||||
|
||||
// Test GetClassNames
|
||||
classNames := control.GetClassNames()
|
||||
expectedNames := []string{"Mage", "Warrior", "Paladin"}
|
||||
|
||||
if len(classNames) != len(expectedNames) {
|
||||
t.Errorf("Expected %d class names, got %d", len(expectedNames), len(classNames))
|
||||
}
|
||||
|
||||
for i, expected := range expectedNames {
|
||||
if i < len(classNames) && classNames[i] != expected {
|
||||
t.Errorf("Expected class name '%s' at position %d, got '%s'", expected, i, classNames[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Test SetClassNames
|
||||
newClasses := []string{"Sorcerer", "Barbarian", "Monk"}
|
||||
control.SetClassNames(newClasses)
|
||||
|
||||
if control.Class1Name != "Sorcerer" {
|
||||
t.Errorf("Expected class 1 name 'Sorcerer', got '%s'", control.Class1Name)
|
||||
}
|
||||
if control.Class2Name != "Barbarian" {
|
||||
t.Errorf("Expected class 2 name 'Barbarian', got '%s'", control.Class2Name)
|
||||
}
|
||||
if control.Class3Name != "Monk" {
|
||||
t.Errorf("Expected class 3 name 'Monk', got '%s'", control.Class3Name)
|
||||
}
|
||||
|
||||
// Test SetClassNames with fewer than 3 classes
|
||||
twoClasses := []string{"Ranger", "Druid"}
|
||||
control.SetClassNames(twoClasses)
|
||||
|
||||
if control.Class1Name != "Ranger" {
|
||||
t.Errorf("Expected class 1 name 'Ranger', got '%s'", control.Class1Name)
|
||||
}
|
||||
if control.Class2Name != "Druid" {
|
||||
t.Errorf("Expected class 2 name 'Druid', got '%s'", control.Class2Name)
|
||||
}
|
||||
if control.Class3Name != "" {
|
||||
t.Errorf("Expected class 3 name to be empty, got '%s'", control.Class3Name)
|
||||
}
|
||||
|
||||
// Test GetClassName
|
||||
if control.GetClassName(1) != "Ranger" {
|
||||
t.Errorf("Expected class 1 name 'Ranger', got '%s'", control.GetClassName(1))
|
||||
}
|
||||
if control.GetClassName(2) != "Druid" {
|
||||
t.Errorf("Expected class 2 name 'Druid', got '%s'", control.GetClassName(2))
|
||||
}
|
||||
if control.GetClassName(3) != "" {
|
||||
t.Errorf("Expected class 3 name to be empty, got '%s'", control.GetClassName(3))
|
||||
}
|
||||
if control.GetClassName(4) != "" {
|
||||
t.Errorf("Expected invalid class number to return empty string, got '%s'", control.GetClassName(4))
|
||||
}
|
||||
|
||||
// Test SetClassName
|
||||
if !control.SetClassName(3, "Rogue") {
|
||||
t.Error("Expected SetClassName(3, 'Rogue') to return true")
|
||||
}
|
||||
if control.Class3Name != "Rogue" {
|
||||
t.Errorf("Expected class 3 name 'Rogue', got '%s'", control.Class3Name)
|
||||
}
|
||||
|
||||
if control.SetClassName(4, "Invalid") {
|
||||
t.Error("Expected SetClassName(4, 'Invalid') to return false")
|
||||
}
|
||||
|
||||
// Test IsValidClassName
|
||||
if !control.IsValidClassName("Ranger") {
|
||||
t.Error("Expected 'Ranger' to be a valid class name")
|
||||
}
|
||||
if !control.IsValidClassName("Druid") {
|
||||
t.Error("Expected 'Druid' to be a valid class name")
|
||||
}
|
||||
if !control.IsValidClassName("Rogue") {
|
||||
t.Error("Expected 'Rogue' to be a valid class name")
|
||||
}
|
||||
if control.IsValidClassName("Bard") {
|
||||
t.Error("Expected 'Bard' not to be a valid class name")
|
||||
}
|
||||
if control.IsValidClassName("") {
|
||||
t.Error("Expected empty string not to be a valid class name")
|
||||
}
|
||||
|
||||
// Test GetClassNumber
|
||||
if control.GetClassNumber("Ranger") != 1 {
|
||||
t.Errorf("Expected 'Ranger' to be class number 1, got %d", control.GetClassNumber("Ranger"))
|
||||
}
|
||||
if control.GetClassNumber("Druid") != 2 {
|
||||
t.Errorf("Expected 'Druid' to be class number 2, got %d", control.GetClassNumber("Druid"))
|
||||
}
|
||||
if control.GetClassNumber("Rogue") != 3 {
|
||||
t.Errorf("Expected 'Rogue' to be class number 3, got %d", control.GetClassNumber("Rogue"))
|
||||
}
|
||||
if control.GetClassNumber("Bard") != 0 {
|
||||
t.Errorf("Expected 'Bard' to return class number 0, got %d", control.GetClassNumber("Bard"))
|
||||
}
|
||||
if control.GetClassNumber("") != 0 {
|
||||
t.Errorf("Expected empty string to return class number 0, got %d", control.GetClassNumber(""))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailMethods(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
control, _ := Get(db)
|
||||
|
||||
// Test HasAdminEmail (should be true initially)
|
||||
if !control.HasAdminEmail() {
|
||||
t.Error("Expected control to have admin email initially")
|
||||
}
|
||||
|
||||
// Test with empty email
|
||||
control.AdminEmail = ""
|
||||
if control.HasAdminEmail() {
|
||||
t.Error("Expected control not to have admin email when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorldSizeMethods(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
control, _ := Get(db)
|
||||
|
||||
// Test IsWorldSizeValid
|
||||
if !control.IsWorldSizeValid() {
|
||||
t.Error("Expected world size 250 to be valid")
|
||||
}
|
||||
|
||||
control.WorldSize = 0
|
||||
if control.IsWorldSizeValid() {
|
||||
t.Error("Expected world size 0 to be invalid")
|
||||
}
|
||||
|
||||
control.WorldSize = 10001
|
||||
if control.IsWorldSizeValid() {
|
||||
t.Error("Expected world size 10001 to be invalid")
|
||||
}
|
||||
|
||||
control.WorldSize = 1000
|
||||
if !control.IsWorldSizeValid() {
|
||||
t.Error("Expected world size 1000 to be valid")
|
||||
}
|
||||
|
||||
// Test GetWorldRadius
|
||||
expectedRadius := 500
|
||||
if control.GetWorldRadius() != expectedRadius {
|
||||
t.Errorf("Expected world radius %d, got %d", expectedRadius, control.GetWorldRadius())
|
||||
}
|
||||
|
||||
// Test IsWithinWorldBounds
|
||||
if !control.IsWithinWorldBounds(0, 0) {
|
||||
t.Error("Expected (0,0) to be within world bounds")
|
||||
}
|
||||
if !control.IsWithinWorldBounds(500, 500) {
|
||||
t.Error("Expected (500,500) to be within world bounds")
|
||||
}
|
||||
if !control.IsWithinWorldBounds(-500, -500) {
|
||||
t.Error("Expected (-500,-500) to be within world bounds")
|
||||
}
|
||||
if control.IsWithinWorldBounds(501, 0) {
|
||||
t.Error("Expected (501,0) to be outside world bounds")
|
||||
}
|
||||
if control.IsWithinWorldBounds(0, 501) {
|
||||
t.Error("Expected (0,501) to be outside world bounds")
|
||||
}
|
||||
if control.IsWithinWorldBounds(-501, 0) {
|
||||
t.Error("Expected (-501,0) to be outside world bounds")
|
||||
}
|
||||
if control.IsWithinWorldBounds(0, -501) {
|
||||
t.Error("Expected (0,-501) to be outside world bounds")
|
||||
}
|
||||
|
||||
// Test GetWorldBounds
|
||||
minX, minY, maxX, maxY := control.GetWorldBounds()
|
||||
expectedMin, expectedMax := -500, 500
|
||||
|
||||
if minX != expectedMin {
|
||||
t.Errorf("Expected minX %d, got %d", expectedMin, minX)
|
||||
}
|
||||
if minY != expectedMin {
|
||||
t.Errorf("Expected minY %d, got %d", expectedMin, minY)
|
||||
}
|
||||
if maxX != expectedMax {
|
||||
t.Errorf("Expected maxX %d, got %d", expectedMax, maxX)
|
||||
}
|
||||
if maxY != expectedMax {
|
||||
t.Errorf("Expected maxY %d, got %d", expectedMax, maxY)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyClassHandling(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
control, _ := Get(db)
|
||||
|
||||
// Set some classes to empty
|
||||
control.Class2Name = ""
|
||||
control.Class3Name = ""
|
||||
|
||||
// Test GetClassNames with empty classes
|
||||
classNames := control.GetClassNames()
|
||||
expectedCount := 1 // Only Class1Name should be included
|
||||
|
||||
if len(classNames) != expectedCount {
|
||||
t.Errorf("Expected %d non-empty class names, got %d", expectedCount, len(classNames))
|
||||
}
|
||||
|
||||
if len(classNames) > 0 && classNames[0] != "Mage" {
|
||||
t.Errorf("Expected first class name 'Mage', got '%s'", classNames[0])
|
||||
}
|
||||
|
||||
// Test IsValidClassName with empty string
|
||||
if control.IsValidClassName("") {
|
||||
t.Error("Expected empty string not to be valid class name")
|
||||
}
|
||||
|
||||
// Test GetClassNumber with empty class names
|
||||
if control.GetClassNumber("Warrior") != 0 {
|
||||
t.Error("Expected empty class 2 not to match 'Warrior'")
|
||||
}
|
||||
}
|
416
internal/control/doc.go
Normal file
416
internal/control/doc.go
Normal file
@ -0,0 +1,416 @@
|
||||
/*
|
||||
Package control is the active record implementation for game control settings.
|
||||
|
||||
The control package manages global game configuration settings stored in a single database record (ID 1). Unlike other packages, this one focuses on updating the existing control record rather than creating new ones, as there is only ever one set of game control settings.
|
||||
|
||||
# Basic Usage
|
||||
|
||||
To retrieve the main control settings:
|
||||
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("World size: %d, Game open: %v\n", settings.WorldSize, settings.IsOpen())
|
||||
|
||||
To find control settings by ID (typically always 1):
|
||||
|
||||
settings, err := control.Find(db, 1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
# Updating Settings
|
||||
|
||||
Control settings can be modified and saved back to the database:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
settings.WorldSize = 500
|
||||
settings.SetOpen(false)
|
||||
settings.AdminEmail = "newadmin@game.com"
|
||||
|
||||
err := settings.Save()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
# Database Schema
|
||||
|
||||
The control table has the following structure:
|
||||
|
||||
CREATE TABLE control (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
world_size INTEGER NOT NULL DEFAULT 250,
|
||||
open INTEGER NOT NULL DEFAULT 1,
|
||||
admin_email TEXT NOT NULL DEFAULT '',
|
||||
class_1_name TEXT NOT NULL DEFAULT '',
|
||||
class_2_name TEXT NOT NULL DEFAULT '',
|
||||
class_3_name TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
|
||||
Where:
|
||||
- id: Unique identifier (typically always 1)
|
||||
- world_size: The size of the game world (used for coordinate bounds)
|
||||
- open: Whether the game is open for new player registration (1=open, 0=closed)
|
||||
- admin_email: Administrator email address for notifications
|
||||
- class_1_name, class_2_name, class_3_name: Names of the three player classes
|
||||
|
||||
# Game World Management
|
||||
|
||||
## World Size and Boundaries
|
||||
|
||||
The world size determines the coordinate boundaries for the game:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
|
||||
// Get world size and radius
|
||||
fmt.Printf("World size: %d\n", settings.WorldSize)
|
||||
fmt.Printf("World radius: %d\n", settings.GetWorldRadius())
|
||||
|
||||
// Check coordinate boundaries
|
||||
minX, minY, maxX, maxY := settings.GetWorldBounds()
|
||||
fmt.Printf("World bounds: (%d,%d) to (%d,%d)\n", minX, minY, maxX, maxY)
|
||||
|
||||
// Validate coordinates
|
||||
if settings.IsWithinWorldBounds(playerX, playerY) {
|
||||
fmt.Println("Player is within world boundaries")
|
||||
}
|
||||
|
||||
The world is centered at (0,0) with coordinates ranging from -radius to +radius.
|
||||
|
||||
## World Size Validation
|
||||
|
||||
Ensure world size settings are reasonable:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
|
||||
if !settings.IsWorldSizeValid() {
|
||||
fmt.Println("Warning: World size is invalid (must be 1-10000)")
|
||||
settings.WorldSize = 250 // Reset to default
|
||||
settings.Save()
|
||||
}
|
||||
|
||||
# Player Registration Control
|
||||
|
||||
## Managing Game Access
|
||||
|
||||
Control whether new players can register:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
|
||||
// Check current status
|
||||
if settings.IsOpen() {
|
||||
fmt.Println("Game is open for new players")
|
||||
} else {
|
||||
fmt.Println("Game is closed to new players")
|
||||
}
|
||||
|
||||
// Change registration status
|
||||
settings.SetOpen(false) // Close registration
|
||||
settings.Close() // Alternative method
|
||||
settings.OpenWorld() // Reopen registration
|
||||
settings.Save()
|
||||
|
||||
## Maintenance Mode
|
||||
|
||||
Temporarily close the game for maintenance:
|
||||
|
||||
func enterMaintenanceMode(db *database.DB) error {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.Close()
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
func exitMaintenanceMode(db *database.DB) error {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.OpenWorld()
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
# Player Class Management
|
||||
|
||||
## Class Configuration
|
||||
|
||||
Manage the three player classes available in the game:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
|
||||
// Get all configured class names
|
||||
classes := settings.GetClassNames()
|
||||
fmt.Printf("Available classes: %v\n", classes)
|
||||
|
||||
// Get specific class name
|
||||
mageClass := settings.GetClassName(1)
|
||||
fmt.Printf("Class 1: %s\n", mageClass)
|
||||
|
||||
// Set all class names at once
|
||||
newClasses := []string{"Sorcerer", "Paladin", "Assassin"}
|
||||
settings.SetClassNames(newClasses)
|
||||
settings.Save()
|
||||
|
||||
## Individual Class Management
|
||||
|
||||
Manage classes individually:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
|
||||
// Set individual class names
|
||||
settings.SetClassName(1, "Wizard")
|
||||
settings.SetClassName(2, "Knight")
|
||||
settings.SetClassName(3, "Rogue")
|
||||
settings.Save()
|
||||
|
||||
// Validate class names
|
||||
if settings.IsValidClassName("Wizard") {
|
||||
fmt.Println("Wizard is a valid class")
|
||||
}
|
||||
|
||||
// Get class number by name
|
||||
classNum := settings.GetClassNumber("Knight")
|
||||
fmt.Printf("Knight is class number: %d\n", classNum)
|
||||
|
||||
## Class System Integration
|
||||
|
||||
Use class settings for player creation and validation:
|
||||
|
||||
func validatePlayerClass(db *database.DB, className string) bool {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return settings.IsValidClassName(className)
|
||||
}
|
||||
|
||||
func getAvailableClasses(db *database.DB) []string {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return settings.GetClassNames()
|
||||
}
|
||||
|
||||
func getClassID(db *database.DB, className string) int {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return settings.GetClassNumber(className)
|
||||
}
|
||||
|
||||
# Administrative Features
|
||||
|
||||
## Admin Contact Information
|
||||
|
||||
Manage administrator contact information:
|
||||
|
||||
settings, _ := control.Get(db)
|
||||
|
||||
// Check if admin email is configured
|
||||
if settings.HasAdminEmail() {
|
||||
fmt.Printf("Admin contact: %s\n", settings.AdminEmail)
|
||||
} else {
|
||||
fmt.Println("No admin email configured")
|
||||
settings.AdminEmail = "admin@mygame.com"
|
||||
settings.Save()
|
||||
}
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
Validate all control settings:
|
||||
|
||||
func validateControlSettings(db *database.DB) []string {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return []string{"Failed to load control settings"}
|
||||
}
|
||||
|
||||
var issues []string
|
||||
|
||||
// Check world size
|
||||
if !settings.IsWorldSizeValid() {
|
||||
issues = append(issues, "Invalid world size")
|
||||
}
|
||||
|
||||
// Check admin email
|
||||
if !settings.HasAdminEmail() {
|
||||
issues = append(issues, "No admin email configured")
|
||||
}
|
||||
|
||||
// Check class names
|
||||
classes := settings.GetClassNames()
|
||||
if len(classes) == 0 {
|
||||
issues = append(issues, "No player classes configured")
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
# Game Logic Integration
|
||||
|
||||
## Coordinate Validation
|
||||
|
||||
Use control settings for game coordinate validation:
|
||||
|
||||
func validatePlayerMovement(db *database.DB, newX, newY int) bool {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return settings.IsWithinWorldBounds(newX, newY)
|
||||
}
|
||||
|
||||
## Registration System
|
||||
|
||||
Integrate with player registration:
|
||||
|
||||
func canRegisterNewPlayer(db *database.DB) bool {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return settings.IsOpen()
|
||||
}
|
||||
|
||||
func getRegistrationMessage(db *database.DB) string {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return "Unable to check registration status"
|
||||
}
|
||||
|
||||
if settings.IsOpen() {
|
||||
classes := settings.GetClassNames()
|
||||
return fmt.Sprintf("Welcome! Choose from these classes: %v", classes)
|
||||
} else {
|
||||
return "Registration is currently closed"
|
||||
}
|
||||
}
|
||||
|
||||
# Configuration Management
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
Backup control settings:
|
||||
|
||||
func backupControlSettings(db *database.DB) (*control.Control, error) {
|
||||
return control.Get(db)
|
||||
}
|
||||
|
||||
func restoreControlSettings(db *database.DB, backup *control.Control) error {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.WorldSize = backup.WorldSize
|
||||
settings.Open = backup.Open
|
||||
settings.AdminEmail = backup.AdminEmail
|
||||
settings.Class1Name = backup.Class1Name
|
||||
settings.Class2Name = backup.Class2Name
|
||||
settings.Class3Name = backup.Class3Name
|
||||
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
## Default Settings
|
||||
|
||||
Reset to default configuration:
|
||||
|
||||
func resetToDefaults(db *database.DB) error {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.WorldSize = 250
|
||||
settings.SetOpen(true)
|
||||
settings.AdminEmail = ""
|
||||
settings.SetClassNames([]string{"Mage", "Warrior", "Paladin"})
|
||||
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
# Performance Considerations
|
||||
|
||||
## Caching Settings
|
||||
|
||||
Since control settings rarely change, consider caching:
|
||||
|
||||
var controlCache *control.Control
|
||||
var cacheTime time.Time
|
||||
|
||||
func getCachedControlSettings(db *database.DB) (*control.Control, error) {
|
||||
// Cache for 5 minutes
|
||||
if controlCache != nil && time.Since(cacheTime) < 5*time.Minute {
|
||||
return controlCache, nil
|
||||
}
|
||||
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
controlCache = settings
|
||||
cacheTime = time.Now()
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
## Batch Updates
|
||||
|
||||
Update multiple settings efficiently:
|
||||
|
||||
func updateGameConfiguration(db *database.DB, worldSize int, isOpen bool,
|
||||
adminEmail string, classes []string) error {
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update all settings
|
||||
settings.WorldSize = worldSize
|
||||
settings.SetOpen(isOpen)
|
||||
settings.AdminEmail = adminEmail
|
||||
settings.SetClassNames(classes)
|
||||
|
||||
// Single save operation
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
# Error Handling
|
||||
|
||||
Common error scenarios and handling:
|
||||
|
||||
settings, err := control.Get(db)
|
||||
if err != nil {
|
||||
// Handle database connection issues or missing control record
|
||||
log.Printf("Failed to load control settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate before using
|
||||
if !settings.IsWorldSizeValid() {
|
||||
log.Println("Warning: Invalid world size detected")
|
||||
// Could reset to default or reject changes
|
||||
}
|
||||
|
||||
// Save with error handling
|
||||
if err := settings.Save(); err != nil {
|
||||
log.Printf("Failed to save control settings: %v", err)
|
||||
// Could retry or alert administrator
|
||||
}
|
||||
|
||||
The control package provides a centralized way to manage all global game settings through a single, persistent record that can be easily modified and validated.
|
||||
*/
|
||||
package control
|
Loading…
x
Reference in New Issue
Block a user