create towns package
This commit is contained in:
parent
89af7644ba
commit
a6b34b7b87
115
internal/towns/builder.go
Normal file
115
internal/towns/builder.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package towns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"dk/internal/database"
|
||||||
|
|
||||||
|
"zombiezen.com/go/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder provides a fluent interface for creating towns
|
||||||
|
type Builder struct {
|
||||||
|
town *Town
|
||||||
|
db *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder creates a new town builder
|
||||||
|
func NewBuilder(db *database.DB) *Builder {
|
||||||
|
return &Builder{
|
||||||
|
town: &Town{
|
||||||
|
db: db,
|
||||||
|
},
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName sets the town name
|
||||||
|
func (b *Builder) WithName(name string) *Builder {
|
||||||
|
b.town.Name = name
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithX sets the X coordinate
|
||||||
|
func (b *Builder) WithX(x int) *Builder {
|
||||||
|
b.town.X = x
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithY sets the Y coordinate
|
||||||
|
func (b *Builder) WithY(y int) *Builder {
|
||||||
|
b.town.Y = y
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCoordinates sets both X and Y coordinates
|
||||||
|
func (b *Builder) WithCoordinates(x, y int) *Builder {
|
||||||
|
b.town.X = x
|
||||||
|
b.town.Y = y
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInnCost sets the inn cost
|
||||||
|
func (b *Builder) WithInnCost(cost int) *Builder {
|
||||||
|
b.town.InnCost = cost
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMapCost sets the map cost
|
||||||
|
func (b *Builder) WithMapCost(cost int) *Builder {
|
||||||
|
b.town.MapCost = cost
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTPCost sets the teleport cost
|
||||||
|
func (b *Builder) WithTPCost(cost int) *Builder {
|
||||||
|
b.town.TPCost = cost
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithShopList sets the shop list as a comma-separated string
|
||||||
|
func (b *Builder) WithShopList(shopList string) *Builder {
|
||||||
|
b.town.ShopList = shopList
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithShopItems sets the shop items from a slice of item IDs
|
||||||
|
func (b *Builder) WithShopItems(items []string) *Builder {
|
||||||
|
b.town.SetShopItems(items)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create saves the town to the database and returns the created town with ID
|
||||||
|
func (b *Builder) Create() (*Town, error) {
|
||||||
|
// Use a transaction to ensure we can get the ID
|
||||||
|
var town *Town
|
||||||
|
err := b.db.Transaction(func(tx *database.Tx) error {
|
||||||
|
query := `INSERT INTO towns (name, x, y, inn_cost, map_cost, tp_cost, shop_list)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
|
if err := tx.Exec(query, b.town.Name, b.town.X, b.town.Y,
|
||||||
|
b.town.InnCost, b.town.MapCost, b.town.TPCost, b.town.ShopList); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert town: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last insert ID
|
||||||
|
var id int
|
||||||
|
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
|
||||||
|
id = stmt.ColumnInt(0)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get insert ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.town.ID = id
|
||||||
|
town = b.town
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return town, nil
|
||||||
|
}
|
295
internal/towns/doc.go
Normal file
295
internal/towns/doc.go
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
Package towns is the active record implementation for towns in the game.
|
||||||
|
|
||||||
|
# Basic Usage
|
||||||
|
|
||||||
|
To retrieve a town by ID:
|
||||||
|
|
||||||
|
town, err := towns.Find(db, 1)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Found town: %s at (%d,%d)\n", town.Name, town.X, town.Y)
|
||||||
|
|
||||||
|
To get all towns:
|
||||||
|
|
||||||
|
allTowns, err := towns.All(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, town := range allTowns {
|
||||||
|
fmt.Printf("Town: %s\n", town.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
To find a town by name:
|
||||||
|
|
||||||
|
midworld, err := towns.ByName(db, "Midworld")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
To filter towns by affordability:
|
||||||
|
|
||||||
|
cheapInns, err := towns.ByMaxInnCost(db, 25)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
affordableTP, err := towns.ByMaxTPCost(db, 50)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
To find nearby towns:
|
||||||
|
|
||||||
|
nearbyTowns, err := towns.ByDistance(db, playerX, playerY, 100)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Creating Towns with Builder Pattern
|
||||||
|
|
||||||
|
The package provides a fluent builder interface for creating new towns:
|
||||||
|
|
||||||
|
town, err := towns.NewBuilder(db).
|
||||||
|
WithName("New Settlement").
|
||||||
|
WithCoordinates(150, -75).
|
||||||
|
WithInnCost(35).
|
||||||
|
WithMapCost(125).
|
||||||
|
WithTPCost(40).
|
||||||
|
WithShopItems([]string{"1", "5", "10", "20"}).
|
||||||
|
Create()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Created town with ID: %d\n", town.ID)
|
||||||
|
|
||||||
|
# Updating Towns
|
||||||
|
|
||||||
|
Towns can be modified and saved back to the database:
|
||||||
|
|
||||||
|
town, _ := towns.Find(db, 1)
|
||||||
|
town.Name = "Enhanced Midworld"
|
||||||
|
town.InnCost = 8
|
||||||
|
town.MapCost = 10
|
||||||
|
|
||||||
|
err := town.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deleting Towns
|
||||||
|
|
||||||
|
Towns can be removed from the database:
|
||||||
|
|
||||||
|
town, _ := towns.Find(db, 1)
|
||||||
|
err := town.Delete()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Database Schema
|
||||||
|
|
||||||
|
The towns table has the following structure:
|
||||||
|
|
||||||
|
CREATE TABLE towns (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
x INTEGER NOT NULL DEFAULT 0,
|
||||||
|
y INTEGER NOT NULL DEFAULT 0,
|
||||||
|
inn_cost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
map_cost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tp_cost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shop_list TEXT NOT NULL DEFAULT ''
|
||||||
|
)
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- id: Unique identifier
|
||||||
|
- name: Display name of the town
|
||||||
|
- x, y: World coordinates for the town location
|
||||||
|
- inn_cost: Cost in gold to rest at the inn
|
||||||
|
- map_cost: Cost in gold to buy the world map from this town
|
||||||
|
- tp_cost: Cost in gold to teleport to this town from elsewhere
|
||||||
|
- shop_list: Comma-separated list of item IDs sold in the town shop
|
||||||
|
|
||||||
|
# Town Economics
|
||||||
|
|
||||||
|
## Inn System
|
||||||
|
|
||||||
|
Towns provide rest services through inns:
|
||||||
|
|
||||||
|
if town.CanAffordInn(playerGold) {
|
||||||
|
fmt.Printf("You can rest at %s for %d gold\n", town.Name, town.InnCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
Inn costs typically increase with town size and distance from starting area.
|
||||||
|
|
||||||
|
## Map Purchase
|
||||||
|
|
||||||
|
Maps can be bought from certain towns:
|
||||||
|
|
||||||
|
if town.MapCost > 0 && town.CanAffordMap(playerGold) {
|
||||||
|
fmt.Printf("Map available for %d gold at %s\n", town.MapCost, town.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
Not all towns sell maps (cost = 0 means unavailable).
|
||||||
|
|
||||||
|
## Teleportation Network
|
||||||
|
|
||||||
|
Towns with teleport access allow fast travel:
|
||||||
|
|
||||||
|
if town.TPCost > 0 && town.CanAffordTeleport(playerGold) {
|
||||||
|
fmt.Printf("Can teleport to %s for %d gold\n", town.Name, town.TPCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
Starting town (Midworld) typically has no teleport cost.
|
||||||
|
|
||||||
|
# Shop System
|
||||||
|
|
||||||
|
## Item Management
|
||||||
|
|
||||||
|
Towns have shops that sell specific items:
|
||||||
|
|
||||||
|
shopItems := town.GetShopItems()
|
||||||
|
fmt.Printf("%s sells %d different items\n", town.Name, len(shopItems))
|
||||||
|
|
||||||
|
if town.HasShopItem("5") {
|
||||||
|
fmt.Println("This town sells item ID 5")
|
||||||
|
}
|
||||||
|
|
||||||
|
## Shop Inventory
|
||||||
|
|
||||||
|
Shop inventories are stored as comma-separated item ID lists:
|
||||||
|
|
||||||
|
// Current inventory
|
||||||
|
items := town.GetShopItems() // Returns []string{"1", "2", "3"}
|
||||||
|
|
||||||
|
// Update inventory
|
||||||
|
newItems := []string{"10", "15", "20", "25"}
|
||||||
|
town.SetShopItems(newItems)
|
||||||
|
town.Save() // Persist changes
|
||||||
|
|
||||||
|
# Geographic Queries
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
Towns exist on a 2D coordinate plane:
|
||||||
|
|
||||||
|
fmt.Printf("%s is located at (%d, %d)\n", town.Name, town.X, town.Y)
|
||||||
|
|
||||||
|
// Calculate distance between towns
|
||||||
|
distance := town1.DistanceFrom(town2.X, town2.Y)
|
||||||
|
|
||||||
|
## Proximity Search
|
||||||
|
|
||||||
|
Find towns within a certain distance:
|
||||||
|
|
||||||
|
// Get all towns within 50 units of player position
|
||||||
|
nearbyTowns, err := towns.ByDistance(db, playerX, playerY, 50)
|
||||||
|
|
||||||
|
// Results are ordered by distance (closest first)
|
||||||
|
if len(nearbyTowns) > 0 {
|
||||||
|
closest := nearbyTowns[0]
|
||||||
|
fmt.Printf("Closest town: %s\n", closest.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
## Starting Location
|
||||||
|
|
||||||
|
The starting town is identifiable:
|
||||||
|
|
||||||
|
if town.IsStartingTown() {
|
||||||
|
fmt.Println("This is where new players begin")
|
||||||
|
}
|
||||||
|
|
||||||
|
Starting towns are typically at coordinates (0, 0).
|
||||||
|
|
||||||
|
# Cost-Based Queries
|
||||||
|
|
||||||
|
## Budget-Friendly Services
|
||||||
|
|
||||||
|
Find towns within budget constraints:
|
||||||
|
|
||||||
|
// Towns with affordable inns
|
||||||
|
cheapInns, err := towns.ByMaxInnCost(db, playerGold)
|
||||||
|
|
||||||
|
// Towns with affordable teleportation
|
||||||
|
affordableTP, err := towns.ByMaxTPCost(db, playerGold)
|
||||||
|
|
||||||
|
Results are ordered by cost (cheapest first).
|
||||||
|
|
||||||
|
## Service Availability
|
||||||
|
|
||||||
|
Check what services a town offers:
|
||||||
|
|
||||||
|
fmt.Printf("Services at %s:\n", town.Name)
|
||||||
|
if town.InnCost > 0 {
|
||||||
|
fmt.Printf("- Inn: %d gold\n", town.InnCost)
|
||||||
|
}
|
||||||
|
if town.MapCost > 0 {
|
||||||
|
fmt.Printf("- Map: %d gold\n", town.MapCost)
|
||||||
|
}
|
||||||
|
if town.TPCost > 0 {
|
||||||
|
fmt.Printf("- Teleport destination: %d gold\n", town.TPCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Game Progression
|
||||||
|
|
||||||
|
## Town Hierarchy
|
||||||
|
|
||||||
|
Towns typically follow a progression pattern:
|
||||||
|
|
||||||
|
1. **Starting Towns** (Midworld): Free teleport, basic services, low costs
|
||||||
|
2. **Early Game Towns** (Roma, Bris): Moderate costs, expanded shops
|
||||||
|
3. **Mid Game Towns** (Kalle, Narcissa): Higher costs, specialized items
|
||||||
|
4. **Late Game Towns** (Hambry, Gilead, Endworld): Premium services, rare items
|
||||||
|
|
||||||
|
## Economic Scaling
|
||||||
|
|
||||||
|
Service costs often scale with game progression:
|
||||||
|
|
||||||
|
// Example progression analysis
|
||||||
|
towns, _ := towns.All(db)
|
||||||
|
for _, town := range towns {
|
||||||
|
if town.TPCost > 100 {
|
||||||
|
fmt.Printf("%s is a late-game destination\n", town.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# World Map Integration
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
Towns serve as waypoints for world navigation:
|
||||||
|
|
||||||
|
// Find path between towns
|
||||||
|
startTown, _ := towns.ByName(db, "Midworld")
|
||||||
|
endTown, _ := towns.ByName(db, "Endworld")
|
||||||
|
|
||||||
|
distance := startTown.DistanceFrom(endTown.X, endTown.Y)
|
||||||
|
fmt.Printf("Distance from %s to %s: %.0f units\n",
|
||||||
|
startTown.Name, endTown.Name, distance)
|
||||||
|
|
||||||
|
## Strategic Planning
|
||||||
|
|
||||||
|
Use town data for strategic decisions:
|
||||||
|
|
||||||
|
// Find cheapest inn route
|
||||||
|
route := []string{"Midworld", "Roma", "Bris"}
|
||||||
|
totalInnCost := 0
|
||||||
|
|
||||||
|
for _, townName := range route {
|
||||||
|
town, _ := towns.ByName(db, townName)
|
||||||
|
totalInnCost += town.InnCost
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Route inn cost: %d gold\n", totalInnCost)
|
||||||
|
|
||||||
|
# Error Handling
|
||||||
|
|
||||||
|
All functions return appropriate errors for common failure cases:
|
||||||
|
- Town not found (Find/ByName returns error for non-existent towns)
|
||||||
|
- Database connection issues
|
||||||
|
- Invalid operations (e.g., saving/deleting towns without IDs)
|
||||||
|
*/
|
||||||
|
package towns
|
268
internal/towns/towns.go
Normal file
268
internal/towns/towns.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
package towns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dk/internal/database"
|
||||||
|
|
||||||
|
"zombiezen.com/go/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Town represents a town in the database
|
||||||
|
type Town struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
InnCost int `json:"inn_cost"`
|
||||||
|
MapCost int `json:"map_cost"`
|
||||||
|
TPCost int `json:"tp_cost"`
|
||||||
|
ShopList string `json:"shop_list"`
|
||||||
|
|
||||||
|
db *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find retrieves a town by ID
|
||||||
|
func Find(db *database.DB, id int) (*Town, error) {
|
||||||
|
town := &Town{db: db}
|
||||||
|
|
||||||
|
query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE id = ?"
|
||||||
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
|
town.ID = stmt.ColumnInt(0)
|
||||||
|
town.Name = stmt.ColumnText(1)
|
||||||
|
town.X = stmt.ColumnInt(2)
|
||||||
|
town.Y = stmt.ColumnInt(3)
|
||||||
|
town.InnCost = stmt.ColumnInt(4)
|
||||||
|
town.MapCost = stmt.ColumnInt(5)
|
||||||
|
town.TPCost = stmt.ColumnInt(6)
|
||||||
|
town.ShopList = stmt.ColumnText(7)
|
||||||
|
return nil
|
||||||
|
}, id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find town: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if town.ID == 0 {
|
||||||
|
return nil, fmt.Errorf("town with ID %d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return town, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All retrieves all towns
|
||||||
|
func All(db *database.DB) ([]*Town, error) {
|
||||||
|
var towns []*Town
|
||||||
|
|
||||||
|
query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns ORDER BY id"
|
||||||
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
|
town := &Town{
|
||||||
|
ID: stmt.ColumnInt(0),
|
||||||
|
Name: stmt.ColumnText(1),
|
||||||
|
X: stmt.ColumnInt(2),
|
||||||
|
Y: stmt.ColumnInt(3),
|
||||||
|
InnCost: stmt.ColumnInt(4),
|
||||||
|
MapCost: stmt.ColumnInt(5),
|
||||||
|
TPCost: stmt.ColumnInt(6),
|
||||||
|
ShopList: stmt.ColumnText(7),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
towns = append(towns, town)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve all towns: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return towns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByName retrieves a town by name (case-insensitive)
|
||||||
|
func ByName(db *database.DB, name string) (*Town, error) {
|
||||||
|
town := &Town{db: db}
|
||||||
|
|
||||||
|
query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE LOWER(name) = LOWER(?) LIMIT 1"
|
||||||
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
|
town.ID = stmt.ColumnInt(0)
|
||||||
|
town.Name = stmt.ColumnText(1)
|
||||||
|
town.X = stmt.ColumnInt(2)
|
||||||
|
town.Y = stmt.ColumnInt(3)
|
||||||
|
town.InnCost = stmt.ColumnInt(4)
|
||||||
|
town.MapCost = stmt.ColumnInt(5)
|
||||||
|
town.TPCost = stmt.ColumnInt(6)
|
||||||
|
town.ShopList = stmt.ColumnText(7)
|
||||||
|
return nil
|
||||||
|
}, name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find town by name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if town.ID == 0 {
|
||||||
|
return nil, fmt.Errorf("town with name '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return town, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByMaxInnCost retrieves towns with inn cost at most the specified amount
|
||||||
|
func ByMaxInnCost(db *database.DB, maxCost int) ([]*Town, error) {
|
||||||
|
var towns []*Town
|
||||||
|
|
||||||
|
query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE inn_cost <= ? ORDER BY inn_cost, id"
|
||||||
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
|
town := &Town{
|
||||||
|
ID: stmt.ColumnInt(0),
|
||||||
|
Name: stmt.ColumnText(1),
|
||||||
|
X: stmt.ColumnInt(2),
|
||||||
|
Y: stmt.ColumnInt(3),
|
||||||
|
InnCost: stmt.ColumnInt(4),
|
||||||
|
MapCost: stmt.ColumnInt(5),
|
||||||
|
TPCost: stmt.ColumnInt(6),
|
||||||
|
ShopList: stmt.ColumnText(7),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
towns = append(towns, town)
|
||||||
|
return nil
|
||||||
|
}, maxCost)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve towns by max inn cost: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return towns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByMaxTPCost retrieves towns with teleport cost at most the specified amount
|
||||||
|
func ByMaxTPCost(db *database.DB, maxCost int) ([]*Town, error) {
|
||||||
|
var towns []*Town
|
||||||
|
|
||||||
|
query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE tp_cost <= ? ORDER BY tp_cost, id"
|
||||||
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
|
town := &Town{
|
||||||
|
ID: stmt.ColumnInt(0),
|
||||||
|
Name: stmt.ColumnText(1),
|
||||||
|
X: stmt.ColumnInt(2),
|
||||||
|
Y: stmt.ColumnInt(3),
|
||||||
|
InnCost: stmt.ColumnInt(4),
|
||||||
|
MapCost: stmt.ColumnInt(5),
|
||||||
|
TPCost: stmt.ColumnInt(6),
|
||||||
|
ShopList: stmt.ColumnText(7),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
towns = append(towns, town)
|
||||||
|
return nil
|
||||||
|
}, maxCost)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve towns by max TP cost: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return towns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByDistance retrieves towns within a certain distance from a point
|
||||||
|
func ByDistance(db *database.DB, fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||||
|
var towns []*Town
|
||||||
|
|
||||||
|
query := `SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list
|
||||||
|
FROM towns
|
||||||
|
WHERE ((x - ?) * (x - ?) + (y - ?) * (y - ?)) <= ?
|
||||||
|
ORDER BY ((x - ?) * (x - ?) + (y - ?) * (y - ?)), id`
|
||||||
|
|
||||||
|
maxDistance2 := maxDistance * maxDistance
|
||||||
|
err := db.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
|
town := &Town{
|
||||||
|
ID: stmt.ColumnInt(0),
|
||||||
|
Name: stmt.ColumnText(1),
|
||||||
|
X: stmt.ColumnInt(2),
|
||||||
|
Y: stmt.ColumnInt(3),
|
||||||
|
InnCost: stmt.ColumnInt(4),
|
||||||
|
MapCost: stmt.ColumnInt(5),
|
||||||
|
TPCost: stmt.ColumnInt(6),
|
||||||
|
ShopList: stmt.ColumnText(7),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
towns = append(towns, town)
|
||||||
|
return nil
|
||||||
|
}, fromX, fromX, fromY, fromY, maxDistance2, fromX, fromX, fromY, fromY)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve towns by distance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return towns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updates an existing town in the database
|
||||||
|
func (t *Town) Save() error {
|
||||||
|
if t.ID == 0 {
|
||||||
|
return fmt.Errorf("cannot save town without ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `UPDATE towns SET name = ?, x = ?, y = ?, inn_cost = ?, map_cost = ?, tp_cost = ?, shop_list = ? WHERE id = ?`
|
||||||
|
return t.db.Exec(query, t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList, t.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the town from the database
|
||||||
|
func (t *Town) Delete() error {
|
||||||
|
if t.ID == 0 {
|
||||||
|
return fmt.Errorf("cannot delete town without ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "DELETE FROM towns WHERE id = ?"
|
||||||
|
return t.db.Exec(query, t.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShopItems returns the shop items as a slice of item IDs
|
||||||
|
func (t *Town) GetShopItems() []string {
|
||||||
|
if t.ShopList == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return strings.Split(t.ShopList, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetShopItems sets the shop items from a slice of item IDs
|
||||||
|
func (t *Town) SetShopItems(items []string) {
|
||||||
|
t.ShopList = strings.Join(items, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasShopItem checks if the town's shop sells a specific item ID
|
||||||
|
func (t *Town) HasShopItem(itemID string) bool {
|
||||||
|
items := t.GetShopItems()
|
||||||
|
for _, item := range items {
|
||||||
|
if strings.TrimSpace(item) == itemID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceFrom calculates the distance from this town to given coordinates
|
||||||
|
func (t *Town) DistanceFrom(x, y int) float64 {
|
||||||
|
dx := float64(t.X - x)
|
||||||
|
dy := float64(t.Y - y)
|
||||||
|
return dx*dx + dy*dy // Return squared distance for performance
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStartingTown returns true if this is the starting town (Midworld)
|
||||||
|
func (t *Town) IsStartingTown() bool {
|
||||||
|
return t.X == 0 && t.Y == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAffordInn returns true if the player can afford the inn
|
||||||
|
func (t *Town) CanAffordInn(gold int) bool {
|
||||||
|
return gold >= t.InnCost
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAffordMap returns true if the player can afford to buy the map
|
||||||
|
func (t *Town) CanAffordMap(gold int) bool {
|
||||||
|
return gold >= t.MapCost
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAffordTeleport returns true if the player can afford to teleport here
|
||||||
|
func (t *Town) CanAffordTeleport(gold int) bool {
|
||||||
|
return gold >= t.TPCost
|
||||||
|
}
|
440
internal/towns/towns_test.go
Normal file
440
internal/towns/towns_test.go
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
package towns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dk/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestDB(t *testing.T) *database.DB {
|
||||||
|
testDB := "test_towns.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 towns table
|
||||||
|
createTable := `CREATE TABLE towns (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
x INTEGER NOT NULL DEFAULT 0,
|
||||||
|
y INTEGER NOT NULL DEFAULT 0,
|
||||||
|
inn_cost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
map_cost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tp_cost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shop_list TEXT NOT NULL DEFAULT ''
|
||||||
|
)`
|
||||||
|
|
||||||
|
if err := db.Exec(createTable); err != nil {
|
||||||
|
t.Fatalf("Failed to create towns table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert test data
|
||||||
|
testTowns := `INSERT INTO towns (name, x, y, inn_cost, map_cost, tp_cost, shop_list) VALUES
|
||||||
|
('Midworld', 0, 0, 5, 0, 0, '1,2,3,17,18,19'),
|
||||||
|
('Roma', 30, 30, 10, 25, 5, '2,3,4,18,19,29'),
|
||||||
|
('Bris', 70, -70, 25, 50, 15, '2,3,4,5,18,19,20'),
|
||||||
|
('Kalle', -100, 100, 40, 100, 30, '5,6,8,10,12,21,22,23'),
|
||||||
|
('Endworld', -250, -250, 125, 9000, 160, '16,27,33')`
|
||||||
|
|
||||||
|
if err := db.Exec(testTowns); err != nil {
|
||||||
|
t.Fatalf("Failed to insert test towns: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFind(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test finding existing town
|
||||||
|
town, err := Find(db, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if town.ID != 1 {
|
||||||
|
t.Errorf("Expected ID 1, got %d", town.ID)
|
||||||
|
}
|
||||||
|
if town.Name != "Midworld" {
|
||||||
|
t.Errorf("Expected name 'Midworld', got '%s'", town.Name)
|
||||||
|
}
|
||||||
|
if town.X != 0 {
|
||||||
|
t.Errorf("Expected X 0, got %d", town.X)
|
||||||
|
}
|
||||||
|
if town.Y != 0 {
|
||||||
|
t.Errorf("Expected Y 0, got %d", town.Y)
|
||||||
|
}
|
||||||
|
if town.InnCost != 5 {
|
||||||
|
t.Errorf("Expected inn_cost 5, got %d", town.InnCost)
|
||||||
|
}
|
||||||
|
if town.MapCost != 0 {
|
||||||
|
t.Errorf("Expected map_cost 0, got %d", town.MapCost)
|
||||||
|
}
|
||||||
|
if town.TPCost != 0 {
|
||||||
|
t.Errorf("Expected tp_cost 0, got %d", town.TPCost)
|
||||||
|
}
|
||||||
|
if town.ShopList != "1,2,3,17,18,19" {
|
||||||
|
t.Errorf("Expected shop_list '1,2,3,17,18,19', got '%s'", town.ShopList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test finding non-existent town
|
||||||
|
_, err = Find(db, 999)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when finding non-existent town")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAll(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
towns, err := All(db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get all towns: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(towns) != 5 {
|
||||||
|
t.Errorf("Expected 5 towns, got %d", len(towns))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ordering (by ID)
|
||||||
|
if towns[0].Name != "Midworld" {
|
||||||
|
t.Errorf("Expected first town to be 'Midworld', got '%s'", towns[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByName(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test finding existing town by name
|
||||||
|
town, err := ByName(db, "Midworld")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find town by name: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if town.Name != "Midworld" {
|
||||||
|
t.Errorf("Expected name 'Midworld', got '%s'", town.Name)
|
||||||
|
}
|
||||||
|
if town.X != 0 || town.Y != 0 {
|
||||||
|
t.Errorf("Expected coordinates (0,0), got (%d,%d)", town.X, town.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case insensitivity
|
||||||
|
townLower, err := ByName(db, "midworld")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find town by lowercase name: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if townLower.ID != town.ID {
|
||||||
|
t.Error("Case insensitive search should return same town")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-existent town
|
||||||
|
_, err = ByName(db, "Atlantis")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when finding non-existent town by name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByMaxInnCost(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test towns with inn cost <= 10
|
||||||
|
cheapInns, err := ByMaxInnCost(db, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get towns by max inn cost: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedCount := 2 // Midworld(5) and Roma(10)
|
||||||
|
if len(cheapInns) != expectedCount {
|
||||||
|
t.Errorf("Expected %d towns with inn cost <= 10, got %d", expectedCount, len(cheapInns))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all towns have inn cost <= 10
|
||||||
|
for _, town := range cheapInns {
|
||||||
|
if town.InnCost > 10 {
|
||||||
|
t.Errorf("Town %s has inn cost %d, expected <= 10", town.Name, town.InnCost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ordering (by inn cost, then ID)
|
||||||
|
if cheapInns[0].InnCost > cheapInns[1].InnCost {
|
||||||
|
t.Error("Expected towns to be ordered by inn cost")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByMaxTPCost(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test towns with TP cost <= 15
|
||||||
|
cheapTP, err := ByMaxTPCost(db, 15)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get towns by max TP cost: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedCount := 3 // Midworld(0), Roma(5), Bris(15)
|
||||||
|
if len(cheapTP) != expectedCount {
|
||||||
|
t.Errorf("Expected %d towns with TP cost <= 15, got %d", expectedCount, len(cheapTP))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all towns have TP cost <= 15
|
||||||
|
for _, town := range cheapTP {
|
||||||
|
if town.TPCost > 15 {
|
||||||
|
t.Errorf("Town %s has TP cost %d, expected <= 15", town.Name, town.TPCost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByDistance(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test towns within distance 50 from origin (0,0)
|
||||||
|
nearbyTowns, err := ByDistance(db, 0, 0, 50)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get towns by distance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Midworld (0,0) distance=0, Roma (30,30) distance=sqrt(1800)≈42.4
|
||||||
|
expectedCount := 2
|
||||||
|
if len(nearbyTowns) != expectedCount {
|
||||||
|
t.Errorf("Expected %d towns within distance 50, got %d", expectedCount, len(nearbyTowns))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify distances are within limit
|
||||||
|
for _, town := range nearbyTowns {
|
||||||
|
distance := town.DistanceFrom(0, 0)
|
||||||
|
if distance > 50*50 { // Using squared distance
|
||||||
|
t.Errorf("Town %s distance %.2f is beyond limit", town.Name, distance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ordering (by distance)
|
||||||
|
if len(nearbyTowns) >= 2 {
|
||||||
|
dist1 := nearbyTowns[0].DistanceFrom(0, 0)
|
||||||
|
dist2 := nearbyTowns[1].DistanceFrom(0, 0)
|
||||||
|
if dist1 > dist2 {
|
||||||
|
t.Error("Expected towns to be ordered by distance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilder(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create new town using builder
|
||||||
|
town, err := NewBuilder(db).
|
||||||
|
WithName("Test City").
|
||||||
|
WithCoordinates(100, -50).
|
||||||
|
WithInnCost(20).
|
||||||
|
WithMapCost(75).
|
||||||
|
WithTPCost(25).
|
||||||
|
WithShopItems([]string{"1", "2", "10", "15"}).
|
||||||
|
Create()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create town with builder: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if town.ID == 0 {
|
||||||
|
t.Error("Expected non-zero ID after creation")
|
||||||
|
}
|
||||||
|
if town.Name != "Test City" {
|
||||||
|
t.Errorf("Expected name 'Test City', got '%s'", town.Name)
|
||||||
|
}
|
||||||
|
if town.X != 100 {
|
||||||
|
t.Errorf("Expected X 100, got %d", town.X)
|
||||||
|
}
|
||||||
|
if town.Y != -50 {
|
||||||
|
t.Errorf("Expected Y -50, got %d", town.Y)
|
||||||
|
}
|
||||||
|
if town.InnCost != 20 {
|
||||||
|
t.Errorf("Expected inn cost 20, got %d", town.InnCost)
|
||||||
|
}
|
||||||
|
if town.MapCost != 75 {
|
||||||
|
t.Errorf("Expected map cost 75, got %d", town.MapCost)
|
||||||
|
}
|
||||||
|
if town.TPCost != 25 {
|
||||||
|
t.Errorf("Expected TP cost 25, got %d", town.TPCost)
|
||||||
|
}
|
||||||
|
if town.ShopList != "1,2,10,15" {
|
||||||
|
t.Errorf("Expected shop list '1,2,10,15', got '%s'", town.ShopList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it was saved to database
|
||||||
|
foundTown, err := Find(db, town.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find created town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundTown.Name != "Test City" {
|
||||||
|
t.Errorf("Created town not found in database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSave(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
town, err := Find(db, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify town
|
||||||
|
town.Name = "Updated Midworld"
|
||||||
|
town.X = 5
|
||||||
|
town.Y = -5
|
||||||
|
town.InnCost = 8
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
err = town.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify changes were saved
|
||||||
|
updatedTown, err := Find(db, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find updated town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedTown.Name != "Updated Midworld" {
|
||||||
|
t.Errorf("Expected updated name 'Updated Midworld', got '%s'", updatedTown.Name)
|
||||||
|
}
|
||||||
|
if updatedTown.X != 5 {
|
||||||
|
t.Errorf("Expected updated X 5, got %d", updatedTown.X)
|
||||||
|
}
|
||||||
|
if updatedTown.Y != -5 {
|
||||||
|
t.Errorf("Expected updated Y -5, got %d", updatedTown.Y)
|
||||||
|
}
|
||||||
|
if updatedTown.InnCost != 8 {
|
||||||
|
t.Errorf("Expected updated inn cost 8, got %d", updatedTown.InnCost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
town, err := Find(db, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete town
|
||||||
|
err = town.Delete()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete town: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify town was deleted
|
||||||
|
_, err = Find(db, 1)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when finding deleted town")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShopItemMethods(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
town, _ := Find(db, 1) // Midworld with shop_list "1,2,3,17,18,19"
|
||||||
|
|
||||||
|
// Test GetShopItems
|
||||||
|
items := town.GetShopItems()
|
||||||
|
expectedItems := []string{"1", "2", "3", "17", "18", "19"}
|
||||||
|
if len(items) != len(expectedItems) {
|
||||||
|
t.Errorf("Expected %d shop items, got %d", len(expectedItems), len(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, expected := range expectedItems {
|
||||||
|
if i < len(items) && items[i] != expected {
|
||||||
|
t.Errorf("Expected item %s at position %d, got %s", expected, i, items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test HasShopItem
|
||||||
|
if !town.HasShopItem("1") {
|
||||||
|
t.Error("Expected town to have shop item '1'")
|
||||||
|
}
|
||||||
|
if !town.HasShopItem("19") {
|
||||||
|
t.Error("Expected town to have shop item '19'")
|
||||||
|
}
|
||||||
|
if town.HasShopItem("99") {
|
||||||
|
t.Error("Expected town not to have shop item '99'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SetShopItems
|
||||||
|
newItems := []string{"5", "10", "15"}
|
||||||
|
town.SetShopItems(newItems)
|
||||||
|
if town.ShopList != "5,10,15" {
|
||||||
|
t.Errorf("Expected shop list '5,10,15', got '%s'", town.ShopList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with empty shop list
|
||||||
|
emptyTown, _ := Find(db, 5) // Create a town with empty shop list
|
||||||
|
emptyTown.ShopList = ""
|
||||||
|
emptyItems := emptyTown.GetShopItems()
|
||||||
|
if len(emptyItems) != 0 {
|
||||||
|
t.Errorf("Expected 0 items for empty shop list, got %d", len(emptyItems))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUtilityMethods(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
midworld, _ := Find(db, 1)
|
||||||
|
roma, _ := Find(db, 2)
|
||||||
|
|
||||||
|
// Test DistanceFrom
|
||||||
|
distance := roma.DistanceFrom(0, 0) // Roma is at (30,30)
|
||||||
|
expectedDistance := float64(30*30 + 30*30) // 1800
|
||||||
|
if distance != expectedDistance {
|
||||||
|
t.Errorf("Expected distance %.2f, got %.2f", expectedDistance, distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test IsStartingTown
|
||||||
|
if !midworld.IsStartingTown() {
|
||||||
|
t.Error("Expected Midworld to be starting town")
|
||||||
|
}
|
||||||
|
if roma.IsStartingTown() {
|
||||||
|
t.Error("Expected Roma not to be starting town")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CanAffordInn
|
||||||
|
if !midworld.CanAffordInn(10) {
|
||||||
|
t.Error("Expected to afford Midworld inn with 10 gold (cost 5)")
|
||||||
|
}
|
||||||
|
if midworld.CanAffordInn(3) {
|
||||||
|
t.Error("Expected not to afford Midworld inn with 3 gold (cost 5)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CanAffordMap
|
||||||
|
if !roma.CanAffordMap(30) {
|
||||||
|
t.Error("Expected to afford Roma map with 30 gold (cost 25)")
|
||||||
|
}
|
||||||
|
if roma.CanAffordMap(20) {
|
||||||
|
t.Error("Expected not to afford Roma map with 20 gold (cost 25)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CanAffordTeleport
|
||||||
|
if !roma.CanAffordTeleport(10) {
|
||||||
|
t.Error("Expected to afford Roma teleport with 10 gold (cost 5)")
|
||||||
|
}
|
||||||
|
if roma.CanAffordTeleport(3) {
|
||||||
|
t.Error("Expected not to afford Roma teleport with 3 gold (cost 5)")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user