new game, fs repo layer
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
module github.com/iliadenisov/galaxy
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/sys v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
+82
-1
@@ -1,6 +1,87 @@
|
||||
package game
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/iliadenisov/galaxy/pkg/generator"
|
||||
"github.com/iliadenisov/galaxy/pkg/model/game"
|
||||
)
|
||||
|
||||
type Repo interface {
|
||||
Persist(game.Game) error
|
||||
}
|
||||
|
||||
func NewGame(r Repo, races []string) (uuid.UUID, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("generate uuid: %s", err)
|
||||
}
|
||||
m, err := generator.Generate(func(ms *generator.MapSetting) {
|
||||
ms.Players = uint32(len(races))
|
||||
})
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("generate map: %s", err)
|
||||
}
|
||||
if len(races) != len(m.HomePlanets) {
|
||||
return uuid.Nil, fmt.Errorf("generate map: wrong number of home planets: %d, expected: %d ", len(m.HomePlanets), len(races))
|
||||
}
|
||||
g := &game.Game{
|
||||
ID: id,
|
||||
Race: make([]game.Race, len(races)),
|
||||
}
|
||||
|
||||
gameMap := &game.Map{
|
||||
Width: m.Width,
|
||||
Height: m.Height,
|
||||
Planet: make([]game.Planet, 0),
|
||||
}
|
||||
for hw := range races {
|
||||
g.Race[hw] = game.Race{
|
||||
Name: races[hw],
|
||||
Votes: 1, // TODO: check with rules
|
||||
VoteFor: races[hw],
|
||||
Drive: 1,
|
||||
Weapons: 1,
|
||||
Shields: 1,
|
||||
Cargo: 1,
|
||||
}
|
||||
gameMap.Planet = append(gameMap.Planet, game.Planet{
|
||||
Owner: races[hw],
|
||||
X: m.HomePlanets[hw].HW.Position.X,
|
||||
Y: m.HomePlanets[hw].HW.Position.Y,
|
||||
Size: m.HomePlanets[hw].HW.Size,
|
||||
Resources: m.HomePlanets[hw].HW.Resources,
|
||||
Production: game.ProductionCapital.AsType(""), // TODO: check default production
|
||||
})
|
||||
for dw := range m.HomePlanets[hw].DW {
|
||||
gameMap.Planet = append(gameMap.Planet, game.Planet{
|
||||
X: m.HomePlanets[hw].DW[dw].Position.X,
|
||||
Y: m.HomePlanets[hw].DW[dw].Position.Y,
|
||||
Size: m.HomePlanets[hw].DW[dw].Size,
|
||||
Resources: m.HomePlanets[hw].DW[dw].Resources,
|
||||
Production: game.ProductionNone.AsType(""),
|
||||
})
|
||||
}
|
||||
}
|
||||
for i := range m.FreePlanets {
|
||||
gameMap.Planet = append(gameMap.Planet, game.Planet{
|
||||
X: m.FreePlanets[i].Position.X,
|
||||
Y: m.FreePlanets[i].Position.Y,
|
||||
Size: m.FreePlanets[i].Size,
|
||||
Resources: m.FreePlanets[i].Resources,
|
||||
Production: game.ProductionNone.AsType(""),
|
||||
})
|
||||
}
|
||||
|
||||
g.Map = *gameMap
|
||||
|
||||
if err := r.Persist(*g); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("persist: %s", err)
|
||||
}
|
||||
return g.ID, nil
|
||||
}
|
||||
|
||||
func (r Race) FlightDistance() float64 {
|
||||
return r.Drive * 40
|
||||
|
||||
@@ -5,15 +5,24 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/iliadenisov/galaxy/pkg/generator"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerator(t *testing.T) {
|
||||
for players := 10; players <= 50; players++ {
|
||||
_, err := generator.Generate(func(ms *generator.MapSetting) { ms.Players = uint32(players) })
|
||||
var s generator.MapSetting
|
||||
m, err := generator.Generate(func(ms *generator.MapSetting) {
|
||||
ms.Players = uint32(players)
|
||||
s = *ms
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("generate: %s", err)
|
||||
break
|
||||
}
|
||||
assert.Equal(t, players, len(m.HomePlanets), "hw count")
|
||||
for i := range m.HomePlanets {
|
||||
assert.Equal(t, int(s.DWCount), len(m.HomePlanets[i].DW), "hw #%d: dw count", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package game
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type Game struct {
|
||||
ID uuid.UUID
|
||||
Map Map
|
||||
Race []Race
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package game
|
||||
|
||||
type Map struct {
|
||||
Width uint32
|
||||
Height uint32
|
||||
|
||||
Planet []Planet
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package game
|
||||
|
||||
import "math"
|
||||
|
||||
type Planet struct {
|
||||
X, Y float32
|
||||
Size float32
|
||||
|
||||
Name string
|
||||
Owner string
|
||||
|
||||
Production ProductionType
|
||||
Resources float32 // Сырьё
|
||||
Industry float32 // Промышленность
|
||||
Population float32 // Население
|
||||
|
||||
Capital float32 // CAP $ - Запасы промышленности
|
||||
Material float32 // MAT M - Запасы сырья
|
||||
Colonists float32 // COL C - Количество колонистов
|
||||
// Параметр "L" означает количество свободных производственных единиц.
|
||||
}
|
||||
|
||||
// Производственный потенциал (I)
|
||||
// промышленность * 0.75 + население * 0.25
|
||||
func (p Planet) ProductionCapacity() float32 {
|
||||
return p.Industry*0.75 + p.Population*0.25
|
||||
}
|
||||
|
||||
// Производство промышленности
|
||||
// TODO: test on real values
|
||||
func (p *Planet) IncreaseIndustry() {
|
||||
prod := p.ProductionCapacity() / 5
|
||||
industryIncrement := float32(math.Min(float64(prod), float64(p.Material)))
|
||||
p.Industry += industryIncrement
|
||||
if p.Industry > p.Population {
|
||||
p.Industry = p.Population
|
||||
p.Capital += p.Population - p.Industry
|
||||
}
|
||||
}
|
||||
|
||||
// Производство материалов
|
||||
// TODO: test on real values
|
||||
func (p *Planet) IncreaseMaterial() {
|
||||
p.Material += p.ProductionCapacity() * p.Industry
|
||||
}
|
||||
|
||||
// Автоматическое увеличение населения на каждом ходу
|
||||
func (p *Planet) IncreasePopulation() {
|
||||
p.Population *= 1.08
|
||||
var extraPopulation = p.Size - p.Population
|
||||
if extraPopulation > 0 {
|
||||
p.Colonists += extraPopulation / 8
|
||||
p.Population -= extraPopulation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package game
|
||||
|
||||
type PlanetProduction string
|
||||
|
||||
const (
|
||||
ProductionNone PlanetProduction = "NONE"
|
||||
ProductionMaterial PlanetProduction = "MAT"
|
||||
ProductionCapital PlanetProduction = "CAP"
|
||||
ProductionDrive PlanetProduction = "DRIVE"
|
||||
ProductionWeapons PlanetProduction = "WEAPONS"
|
||||
ProductionShields PlanetProduction = "SHIELDS"
|
||||
ProductionCargo PlanetProduction = "CARGO"
|
||||
|
||||
ProductionScience PlanetProduction = "SCIENCE"
|
||||
ProductionShip PlanetProduction = "SHIP"
|
||||
)
|
||||
|
||||
type ProductionType struct {
|
||||
Production PlanetProduction
|
||||
SubjectName string
|
||||
}
|
||||
|
||||
func (p PlanetProduction) AsType(subject string) ProductionType {
|
||||
switch p {
|
||||
case ProductionScience, ProductionShip:
|
||||
return ProductionType{Production: p, SubjectName: subject}
|
||||
default:
|
||||
return ProductionType{Production: p}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package game
|
||||
|
||||
type Race struct {
|
||||
Name string
|
||||
Killed bool
|
||||
|
||||
Votes float32
|
||||
VoteFor string
|
||||
|
||||
Drive float32
|
||||
Weapons float32
|
||||
Shields float32
|
||||
Cargo float32
|
||||
}
|
||||
|
||||
func (r Race) FlightDistance() float32 {
|
||||
return r.Drive * 40
|
||||
}
|
||||
|
||||
func (r Race) VisibilityDistance() float32 {
|
||||
return r.Drive * 30
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package game
|
||||
|
||||
import "math"
|
||||
|
||||
type Ship struct {
|
||||
TypeName string
|
||||
}
|
||||
|
||||
type ShipType struct {
|
||||
Name string
|
||||
Drive float64 // [0], [1...]
|
||||
Armament uint
|
||||
Weapons float64 // [0], [1...]
|
||||
Shields float64 // [0], [1...]
|
||||
Cargo float64 // [0], [1...]
|
||||
}
|
||||
|
||||
type ShipGroup struct {
|
||||
Type ShipType
|
||||
Number uint
|
||||
State string // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade
|
||||
Load float64 // Cargo loaded - "Масса груза"
|
||||
Drive float64
|
||||
Weapons float64
|
||||
Shields float64
|
||||
Cargo float64
|
||||
}
|
||||
|
||||
type Fleet struct {
|
||||
ShipGroups []ShipGroup
|
||||
}
|
||||
|
||||
// TODO: test on real values
|
||||
func (st ShipType) EmptyMass() float64 {
|
||||
shipMass := st.DriveMass() + st.ShieldsMass() + st.CargoMass() + st.WeaponsMass()
|
||||
return shipMass
|
||||
}
|
||||
|
||||
func (st ShipType) DriveMass() float64 {
|
||||
return st.Drive
|
||||
}
|
||||
|
||||
func (st ShipType) ShieldsMass() float64 {
|
||||
return st.Shields
|
||||
}
|
||||
|
||||
func (st ShipType) CargoMass() float64 {
|
||||
return st.Cargo
|
||||
}
|
||||
|
||||
func (st ShipType) WeaponsMass() float64 {
|
||||
return float64(st.Armament)*(st.Weapons/2) + st.Weapons/2
|
||||
}
|
||||
|
||||
// Грузоподъёмность
|
||||
func (sg ShipGroup) CargoCapacity() float64 {
|
||||
return sg.Drive * (sg.Type.Cargo + (sg.Type.Cargo*sg.Type.Cargo)/20)
|
||||
}
|
||||
|
||||
// "Масса перевозимого груза"
|
||||
func (sg ShipGroup) CarryingMass() float64 {
|
||||
return sg.Load / sg.Cargo
|
||||
}
|
||||
|
||||
func (sg ShipGroup) FullMass() float64 {
|
||||
return sg.Type.EmptyMass() + sg.CarryingMass()
|
||||
}
|
||||
|
||||
// "Эффективность двигателя"
|
||||
// равна мощности Двигателей умноженной на текущий технологический уровень блока Двигателей
|
||||
func (sg ShipGroup) DriveEffective() float64 {
|
||||
return sg.Type.Drive * sg.Drive
|
||||
}
|
||||
|
||||
// TODO: test this
|
||||
func (sg ShipGroup) Speed() float64 {
|
||||
return sg.DriveEffective() * 20 / sg.FullMass()
|
||||
}
|
||||
|
||||
func (sg ShipGroup) UpgradeDriveCost(drive float64) float64 {
|
||||
return (1 - sg.Drive/drive) * 10 * sg.Type.Drive
|
||||
}
|
||||
|
||||
// TODO: test on other values
|
||||
func (sg ShipGroup) UpgradeWeaponsCost(weapons float64) float64 {
|
||||
return (1 - sg.Weapons/weapons) * 10 * sg.Type.WeaponsMass()
|
||||
}
|
||||
|
||||
func (sg ShipGroup) UpgradeShieldsCost(shields float64) float64 {
|
||||
return (1 - sg.Shields/shields) * 10 * sg.Type.Shields
|
||||
}
|
||||
|
||||
func (sg ShipGroup) UpgradeCargoCost(cargo float64) float64 {
|
||||
return (1 - sg.Cargo/cargo) * 10 * sg.Type.Cargo
|
||||
}
|
||||
|
||||
// Мощность бомбардировки
|
||||
// TODO: maybe rounding must be done only for display?
|
||||
func (sg ShipGroup) BombingPower() float64 {
|
||||
// return math.Sqrt(sg.Type.Weapons * sg.Weapons)
|
||||
result := (math.Sqrt(sg.Type.Weapons*sg.Weapons)/10. + 1.) *
|
||||
sg.Type.Weapons *
|
||||
sg.Weapons *
|
||||
float64(sg.Type.Armament) *
|
||||
float64(sg.Number)
|
||||
return toFixed3(result)
|
||||
}
|
||||
|
||||
// TODO: test this
|
||||
func (fl Fleet) Speed() float64 {
|
||||
result := math.MaxFloat64
|
||||
for _, sg := range fl.ShipGroups {
|
||||
if sg.Speed() < result {
|
||||
result = sg.Speed()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toFixed3(num float64) float64 {
|
||||
return toFixed(num, 3)
|
||||
}
|
||||
|
||||
// TODO: move to more common place
|
||||
func toFixed(num float64, precision int) float64 {
|
||||
output := math.Pow(10, float64(precision))
|
||||
return float64(round(num*output)) / output
|
||||
}
|
||||
|
||||
func round(num float64) int {
|
||||
return int(num + math.Copysign(0.5, num))
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPerm = 0o644
|
||||
lockFile = ".lock"
|
||||
oldFileSuffix = ".old"
|
||||
newFileSuffix = ".new"
|
||||
)
|
||||
|
||||
type fs struct {
|
||||
root string
|
||||
lock *os.File
|
||||
}
|
||||
|
||||
func NewFileStorage(path string) (*fs, error) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("path %s invalid: %s", path, err)
|
||||
}
|
||||
if ok, err := dirExists(absPath); err != nil {
|
||||
return nil, fmt.Errorf("check dir exist: %s", err)
|
||||
} else if !ok {
|
||||
return nil, errors.New("directory does not exist: " + absPath)
|
||||
}
|
||||
|
||||
if ok, err := writable(absPath); err != nil {
|
||||
return nil, fmt.Errorf("check dir access: %s", err)
|
||||
} else if !ok {
|
||||
return nil, errors.New("directory should have read-write access: " + absPath)
|
||||
}
|
||||
|
||||
fs := &fs{
|
||||
root: path,
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
func (f *fs) Lock() (func() error, error) {
|
||||
lockPath := f.lockFilePath()
|
||||
exists, err := fileExists(lockPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check lock file exists: %s", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, errors.New("lock file already exists")
|
||||
}
|
||||
fd, err := os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create lock file: %s", err)
|
||||
}
|
||||
f.lock = fd
|
||||
unlock := func() error {
|
||||
if err := f.lock.Close(); err != nil {
|
||||
return fmt.Errorf("close lock file: %s", err)
|
||||
}
|
||||
if err := os.Remove(f.lock.Name()); err != nil {
|
||||
return fmt.Errorf("remove lock file: %s", err)
|
||||
}
|
||||
f.lock = nil
|
||||
return nil
|
||||
}
|
||||
if _, err := f.lock.Write(big.NewInt(time.Now().UnixMilli()).Bytes()); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("write lock file: %s", err), unlock())
|
||||
}
|
||||
return unlock, nil
|
||||
}
|
||||
|
||||
func (f *fs) Write(path string, data []byte) error {
|
||||
if f.lock == nil {
|
||||
return errors.New("lock must be acquired before write")
|
||||
}
|
||||
|
||||
targetFilePath := filepath.Join(f.root, path)
|
||||
if targetFilePath == f.lockFilePath() {
|
||||
return errors.New("can't write to the lock file")
|
||||
}
|
||||
|
||||
targetDir := filepath.Dir(targetFilePath)
|
||||
if targetDir != f.root {
|
||||
ok, err := dirExists(targetDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check target dir exists: %s", err)
|
||||
}
|
||||
if !ok {
|
||||
err := os.MkdirAll(targetDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create target dirs: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
oldFilePath := targetFilePath + oldFileSuffix
|
||||
|
||||
targetExists, err := fileExists(targetFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check target file exists: %s", err)
|
||||
}
|
||||
if targetExists {
|
||||
oldFileExists, err := fileExists(oldFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check old file exists: %s", err)
|
||||
}
|
||||
if oldFileExists {
|
||||
return fmt.Errorf("old file exists at: %s", oldFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
newFilePath := targetFilePath + newFileSuffix
|
||||
newFileExists, err := fileExists(newFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check new file exists: %s", err)
|
||||
}
|
||||
if newFileExists {
|
||||
return fmt.Errorf("new file exists at: %s", oldFilePath)
|
||||
}
|
||||
|
||||
err = os.WriteFile(newFilePath, data, defaultPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write data to the new file: %s", err)
|
||||
}
|
||||
|
||||
if targetExists {
|
||||
if err := os.Rename(targetFilePath, oldFilePath); err != nil {
|
||||
return fmt.Errorf("rename target file to the old file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(newFilePath, targetFilePath); err != nil {
|
||||
return fmt.Errorf("rename new file to the target file: %s", err)
|
||||
}
|
||||
|
||||
if targetExists {
|
||||
err := os.Remove(oldFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove old file: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fs) Read(path string) ([]byte, error) {
|
||||
if f.lock != nil {
|
||||
return nil, errors.New("lock must be released before read")
|
||||
}
|
||||
|
||||
targetFilePath := filepath.Join(f.root, path)
|
||||
if targetFilePath == f.lockFilePath() {
|
||||
return nil, errors.New("can't read from the lock file")
|
||||
}
|
||||
|
||||
return os.ReadFile(targetFilePath)
|
||||
}
|
||||
|
||||
func (f *fs) lockFilePath() string {
|
||||
return filepath.Join(f.root, lockFile)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewFileStorageSuccess(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
_, err := NewFileStorage(root)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
fs, err := NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage")
|
||||
unlock, err := fs.Lock()
|
||||
assert.NoError(t, err, "acquire lock")
|
||||
exists, err := fileExists(filepath.Join(root, lockFile))
|
||||
assert.NoError(t, err, "check that the lock file should exist")
|
||||
assert.True(t, exists, "lock file must exists")
|
||||
err = unlock()
|
||||
assert.NoError(t, err, "unlocking existing lock")
|
||||
exists, err = fileExists(filepath.Join(root, lockFile))
|
||||
assert.NoError(t, err, "check that the lock file does not exist")
|
||||
assert.False(t, exists, "lock file must be removed")
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
fs, err := NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage: %s", err)
|
||||
unlock, err := fs.Lock()
|
||||
assert.NoError(t, err, "acquire lock: %s", err)
|
||||
|
||||
dirName := "some-dir"
|
||||
if err := os.Mkdir(filepath.Join(root, dirName), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
err string
|
||||
}{
|
||||
{path: "file-1.ext"},
|
||||
{path: "/dir/file-2.ext"},
|
||||
{path: "dir/subdir/file-3.ext"},
|
||||
{path: lockFile, err: "write to the lock file"},
|
||||
{path: dirName, err: "wrong type"},
|
||||
{path: "/" + dirName, err: "wrong type"},
|
||||
} {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
err = fs.Write(tc.path, []byte{0, 1, 2, 3})
|
||||
if tc.err == "" {
|
||||
if err != nil {
|
||||
assert.Fail(t, "not expecting an error", "write to file %s: %s", tc.path, err)
|
||||
} else {
|
||||
exists, err := fileExists(filepath.Join(root, tc.path))
|
||||
assert.NoError(t, err, "check is written file exists")
|
||||
assert.True(t, exists, "the written file should exist")
|
||||
}
|
||||
} else if tc.err != "" {
|
||||
if err == nil {
|
||||
assert.Fail(t, "expecting an error, got none", "write to file %s", tc.path)
|
||||
} else {
|
||||
assert.True(t, strings.Contains(err.Error(), tc.err), "expect: %q got: %q", tc.err, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err = unlock()
|
||||
assert.NoError(t, err, "unlocking existing lock")
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
fs, err := NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage: %s", err)
|
||||
|
||||
dirName := "some-dir"
|
||||
if err := os.Mkdir(filepath.Join(root, dirName), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fileName := "some-file.ext"
|
||||
if err := os.WriteFile(filepath.Join(root, fileName), []byte{1, 2, 3, 4}, os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
lock bool
|
||||
err string
|
||||
}{
|
||||
{path: fileName},
|
||||
{path: "/" + fileName},
|
||||
{path: fileName, lock: true, err: "lock must be released"},
|
||||
{path: lockFile, err: "read from the lock file"},
|
||||
{path: "dir/subdir/file-3.ext", err: "no such file"},
|
||||
{path: lockFile, err: "read from the lock file"},
|
||||
{path: dirName, err: "is a directory"},
|
||||
} {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
if tc.lock {
|
||||
unlock, err := fs.Lock()
|
||||
if err != nil {
|
||||
t.Fatalf("acquire lock: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := unlock(); err != nil {
|
||||
t.Fatalf("release lock: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
_, err = fs.Read(tc.path)
|
||||
if tc.err == "" {
|
||||
if err != nil {
|
||||
assert.Fail(t, "read: not expecting an error, got: "+err.Error())
|
||||
} else {
|
||||
exists, err := fileExists(filepath.Join(root, tc.path))
|
||||
assert.NoError(t, err, "check is written file exists")
|
||||
assert.True(t, exists, "the written file should exist")
|
||||
}
|
||||
} else if tc.err != "" {
|
||||
if err == nil {
|
||||
assert.Fail(t, "read: expecting an error, got none")
|
||||
} else {
|
||||
assert.True(t, strings.Contains(err.Error(), tc.err), "expect: %q got: %q", tc.err, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteErrorWithoutLock(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
fs, err := NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage")
|
||||
err = fs.Write("some/path", []byte{0, 1, 2, 3})
|
||||
assert.Error(t, err, "should return error when no lock acquired")
|
||||
assert.True(t, strings.Contains(err.Error(), "lock must be acquired"), "should return missing lock error")
|
||||
}
|
||||
|
||||
func TestNewFileStorageErrorNotExists(t *testing.T) {
|
||||
_, err := NewFileStorage(filepath.Join(os.TempDir(), "non-existent-dir"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewFileStorageErrorNotADirectory(t *testing.T) {
|
||||
f, err := os.CreateTemp("", "fs-test-file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = NewFileStorage(f.Name())
|
||||
assert.Error(t, err)
|
||||
if err := os.Remove(f.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileStorageErrorNoAccess(t *testing.T) {
|
||||
_, err := NewFileStorage(nonWritableDir)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
nonWritableDir = "/usr/lib"
|
||||
)
|
||||
|
||||
func createWorkDir(t *testing.T) (string, func()) {
|
||||
t.Helper()
|
||||
dir, err := os.MkdirTemp("", "fs-test-workdir")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %s", err)
|
||||
}
|
||||
return dir, func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Fatalf("remove temp dir: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build !windows
|
||||
|
||||
// for windows builds func [writable] should be refactored
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func dirExists(path string) (bool, error) {
|
||||
return pathExists(path, true)
|
||||
}
|
||||
|
||||
func fileExists(path string) (bool, error) {
|
||||
return pathExists(path, false)
|
||||
}
|
||||
|
||||
func pathExists(path string, isDir bool) (bool, error) {
|
||||
if fi, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
} else {
|
||||
if isDir != fi.IsDir() {
|
||||
return false, fmt.Errorf("wrong type: "+path+" mode=%s isDir=%t", fi.Mode(), isDir)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func writable(filepath string) (bool, error) {
|
||||
return unix.Access(filepath, unix.W_OK) == nil, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPathExists(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
testDirExistsFunc(t, root, func(s string) (bool, error) { return pathExists(s, true) })
|
||||
testFileExistsFunc(t, root, func(s string) (bool, error) { return pathExists(s, false) })
|
||||
}
|
||||
|
||||
func TestDirExists(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
testDirExistsFunc(t, root, dirExists)
|
||||
}
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
testFileExistsFunc(t, root, fileExists)
|
||||
}
|
||||
|
||||
func TestWritable(t *testing.T) {
|
||||
root, cleanup := createWorkDir(t)
|
||||
defer cleanup()
|
||||
ok, err := writable(root)
|
||||
assert.NoError(t, err, "directory writable check")
|
||||
assert.True(t, ok, "directory should be writable")
|
||||
|
||||
ok, err = writable(nonWritableDir)
|
||||
assert.NoError(t, err, "system directory writable check")
|
||||
assert.False(t, ok, "system directory should not be writable")
|
||||
}
|
||||
|
||||
func testDirExistsFunc(t *testing.T, root string, dirCheck func(string) (bool, error)) {
|
||||
exists, err := dirCheck(root)
|
||||
assert.NoError(t, err, "directory existence check")
|
||||
assert.True(t, exists, "directory should exist")
|
||||
nonExistentDir := filepath.Join(root, uuid.New().String())
|
||||
exists, err = dirCheck(nonExistentDir)
|
||||
assert.NoError(t, err, "non-existent directory existence check")
|
||||
assert.False(t, exists, "non-existent directory should not exist")
|
||||
}
|
||||
|
||||
func testFileExistsFunc(t *testing.T, root string, fileCheck func(string) (bool, error)) {
|
||||
fpath := createTempFile(t, root)
|
||||
exists, err := fileCheck(fpath)
|
||||
assert.NoError(t, err, "file existence check")
|
||||
assert.True(t, exists, "file should exist")
|
||||
nonExistentFile := filepath.Join(root, uuid.New().String())
|
||||
exists, err = fileCheck(nonExistentFile)
|
||||
assert.NoError(t, err, "non-existent file existence check")
|
||||
assert.False(t, exists, "non-existent file should not exist")
|
||||
}
|
||||
|
||||
func createTempFile(t *testing.T, root string) string {
|
||||
t.Helper()
|
||||
|
||||
fd, err := os.CreateTemp(root, "a-file")
|
||||
if err != nil {
|
||||
assert.FailNow(t, "create temporary file", err)
|
||||
return ""
|
||||
}
|
||||
if err := fd.Close(); err != nil {
|
||||
assert.FailNow(t, "close temporary file", err)
|
||||
return ""
|
||||
}
|
||||
return fd.Name()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package repo
|
||||
|
||||
import "github.com/iliadenisov/galaxy/pkg/repo/fs"
|
||||
|
||||
type Storage interface {
|
||||
Lock() (func() error, error)
|
||||
Write(string, []byte) error
|
||||
}
|
||||
|
||||
type repo struct {
|
||||
s Storage
|
||||
}
|
||||
|
||||
func NewRepo(s Storage) (*repo, error) {
|
||||
r := &repo{
|
||||
s: s,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func NewFileRepo(path string) (*repo, error) {
|
||||
s, err := fs.NewFileStorage(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewRepo(s)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/iliadenisov/galaxy/pkg/game"
|
||||
"github.com/iliadenisov/galaxy/pkg/generator"
|
||||
"github.com/iliadenisov/galaxy/pkg/storage"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
storage storage.Storage
|
||||
}
|
||||
|
||||
func New(storage storage.Storage) Server {
|
||||
return Server{storage: storage}
|
||||
}
|
||||
|
||||
func (s Server) CreateGame(gameParam game.GameParameter) (game.Game, error) {
|
||||
_, _ = generator.Generate()
|
||||
return game.Game{}, errors.New("not yet implemented")
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package storage
|
||||
|
||||
import "github.com/iliadenisov/galaxy/pkg/game"
|
||||
|
||||
// games/
|
||||
// data.json - id, name, turn, schedule, status
|
||||
// game123/
|
||||
// race/<race_id>/data.json - account_id, name, status, war/peace(?), last_order, etc.
|
||||
// order/<turn>/<race_id>/0.json - incoming orders
|
||||
// turn/12/log/ - ?
|
||||
// turn/12/order/<race_id>/0.json - processed orders
|
||||
// turn/12/state/0.json - initital, contains <battle_numbers> for planet
|
||||
// state/<1...N>.json - instant commands changes state
|
||||
// turn/12/report/global.json
|
||||
// report/<race_id>.json
|
||||
// turn/12/battle/<planet_id>/<battle_number>.json
|
||||
|
||||
type Storage interface {
|
||||
GenerateRaceId() game.RaceIdentifier
|
||||
GenerateGameId() game.GameIdentifier
|
||||
|
||||
CreateGame(game.GameParameter) (game.Game, error)
|
||||
ListGames() ([]game.GameIdentifier, error)
|
||||
|
||||
LoadRace(game_id game.GameIdentifier, race_id game.RaceIdentifier) (game.Race, error)
|
||||
SaveRace(game_id game.GameIdentifier, race game.Race) error
|
||||
|
||||
LoadState(game_id game.GameIdentifier) (game.Game, error)
|
||||
SaveState(game game.Game) error
|
||||
}
|
||||
Reference in New Issue
Block a user