new game, fs repo layer

This commit is contained in:
Ilia Denisov
2025-09-23 18:36:22 +03:00
parent 84578dc61c
commit 4d733ae741
18 changed files with 880 additions and 54 deletions
+82 -1
View File
@@ -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
+10 -1
View File
@@ -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)
}
}
}
+9
View File
@@ -0,0 +1,9 @@
package game
import "github.com/google/uuid"
type Game struct {
ID uuid.UUID
Map Map
Race []Race
}
+8
View File
@@ -0,0 +1,8 @@
package game
type Map struct {
Width uint32
Height uint32
Planet []Planet
}
+55
View File
@@ -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
}
}
+30
View File
@@ -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}
}
}
+22
View File
@@ -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
}
+132
View File
@@ -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))
}
+164
View File
@@ -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)
}
+178
View File
@@ -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)
}
+23
View File
@@ -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)
}
}
}
+37
View File
@@ -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
}
+77
View File
@@ -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()
}
+27
View File
@@ -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)
}
-22
View File
@@ -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")
}
-30
View File
@@ -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
}