feat: gamemaster

This commit is contained in:
Ilia Denisov
2026-05-03 07:59:03 +02:00
committed by GitHub
parent a7cee15115
commit 3e2622757e
229 changed files with 41521 additions and 1098 deletions
+9
View File
@@ -19,6 +19,15 @@ func (c Controller) RaceID(actor string) (uuid.UUID, error) {
return c.Cache.g.Race[ri].ID, nil
}
func (c Controller) RaceBanish(actor string) error {
ri, err := c.Cache.validRace(actor)
if err != nil {
return err
}
c.Cache.g.Race[ri].Extinct = true
return nil
}
func (c Controller) RaceQuit(actor string) error {
ri, err := c.Cache.validRace(actor)
if err != nil {
+23 -4
View File
@@ -134,6 +134,14 @@ func ValidateOrder(configure func(*Param), actor string, cmd ...order.DecodableC
return ec.validateOrder(actor, cmd...)
}
func BanishRace(configure func(*Param), actor string) error {
ec, err := NewRepoController(configure)
if err != nil {
return err
}
return ec.banishRace(actor)
}
func GameState(configure func(*Param)) (s game.State, err error) {
ec, err := NewRepoController(configure)
if err != nil {
@@ -146,10 +154,11 @@ func GameState(configure func(*Param)) (s game.State, err error) {
}
result := &game.State{
ID: g.ID,
Turn: g.Turn,
Stage: g.Stage,
Players: make([]game.PlayerState, len(g.Race)),
ID: g.ID,
Turn: g.Turn,
Stage: g.Stage,
Finished: g.Finished(),
Players: make([]game.PlayerState, len(g.Race)),
}
planetCount := make(map[uuid.UUID]uint)
@@ -243,6 +252,16 @@ func (ec *RepoController) executeCommand(consumer func(*Controller) error) (err
})
}
func (ec *RepoController) banishRace(actor string) (err error) {
return ec.executeLocked(func(c *Controller) error {
err = c.RaceBanish(actor)
if err != nil {
return err
}
return c.saveState()
})
}
func (ec *RepoController) executeSafe(consumer func(uint, *Controller) error) (err error) {
g, err := ec.Repo.LoadStateSafe()
if err != nil {
+1 -1
View File
@@ -118,7 +118,7 @@ func (c *Cache) raceTechLevel(ri int, t game.Tech, v float64) {
func (c *Cache) TurnWipeExtinctRaces() {
for i := range c.listRaceActingIdx() {
if c.g.Race[i].TTL == 0 {
if (c.g.Race[i].Extinct && c.g.Race[i].TTL > 0) || (!c.g.Race[i].Extinct && c.g.Race[i].TTL == 0) {
c.wipeRace(i)
}
}
+5 -4
View File
@@ -3,10 +3,11 @@ package game
import "github.com/google/uuid"
type State struct {
ID uuid.UUID
Turn uint
Stage uint
Players []PlayerState
ID uuid.UUID
Turn uint
Stage uint
Players []PlayerState
Finished bool
}
type PlayerState struct {
+45
View File
@@ -0,0 +1,45 @@
package router_test
import (
"net/http"
"net/http/httptest"
"testing"
"galaxy/model/rest"
"github.com/stretchr/testify/assert"
)
const apiBanishPath = "/api/v1/admin/race/banish"
func TestBanishHappyPath(t *testing.T) {
r := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, apiBanishPath, asBody(rest.BanishRequest{RaceName: "Aelinari"}))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNoContent, w.Code, w.Body)
assert.Empty(t, w.Body.String())
}
func TestBanishValidation(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
description string
body any
}{
{"missing race_name", struct{}{}},
{"empty race_name", rest.BanishRequest{RaceName: ""}},
{"blank race_name", rest.BanishRequest{RaceName: " "}},
} {
t.Run(tc.description, func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, apiBanishPath, asBody(tc.body))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
})
}
}
+22
View File
@@ -0,0 +1,22 @@
package handler
import (
"net/http"
"galaxy/model/rest"
"github.com/gin-gonic/gin"
)
func BanishHandler(c *gin.Context, executor CommandExecutor) {
var req rest.BanishRequest
if errorResponse(c, c.ShouldBindJSON(&req)) {
return
}
if errorResponse(c, executor.BanishRace(req.RaceName)) {
return
}
c.Status(http.StatusNoContent)
}
+10 -4
View File
@@ -23,6 +23,7 @@ type CommandExecutor interface {
GenerateGame([]string) (rest.StateResponse, error)
GenerateTurn() (rest.StateResponse, error)
GameState() (rest.StateResponse, error)
BanishRace(string) error
LoadReport(actor string, turn uint) (*report.Report, error)
Execute(cmd ...Command) error
ValidateOrder(actor string, cmd ...order.DecodableCommand) error
@@ -103,16 +104,21 @@ func (e *executor) GameState() (rest.StateResponse, error) {
return stateResponse(s), nil
}
func (e *executor) BanishRace(raceName string) error {
return controller.BanishRace(e.cfg, raceName)
}
func (e *executor) LoadReport(actor string, turn uint) (*report.Report, error) {
return controller.LoadReport(e.cfg, actor, turn)
}
func stateResponse(s game.State) rest.StateResponse {
result := &rest.StateResponse{
ID: s.ID,
Turn: s.Turn,
Stage: s.Stage,
Players: make([]rest.PlayerState, len(s.Players)),
ID: s.ID,
Turn: s.Turn,
Stage: s.Stage,
Finished: s.Finished,
Players: make([]rest.PlayerState, len(s.Players)),
}
for i := range s.Players {
result.Players[i].ID = s.Players[i].ID
+1 -1
View File
@@ -8,7 +8,7 @@ import (
// HealthzHandler is the technical liveness probe used by Runtime Manager
// and operator tooling. It returns 200 with {"status":"ok"} regardless
// of whether the engine has been initialised through POST /api/v1/init.
// of whether the engine has been initialised through POST /api/v1/admin/init.
func HealthzHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
+2 -2
View File
@@ -27,7 +27,7 @@ func TestInit(t *testing.T) {
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
@@ -42,7 +42,7 @@ func TestInitValidators(t *testing.T) {
payload := generateInitRequest(9)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
+1 -1
View File
@@ -24,7 +24,7 @@ func TestGetReport(t *testing.T) {
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
+6 -3
View File
@@ -67,12 +67,15 @@ func setupRouter(executor handler.CommandExecutor) *gin.Engine {
groupV1 := r.Group("/api/v1")
groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) })
groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) })
groupAdmin := groupV1.Group("/admin")
groupAdmin.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) })
groupAdmin.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) })
groupAdmin.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) })
groupAdmin.POST("/race/banish", func(ctx *gin.Context) { handler.BanishHandler(ctx, executor) })
groupV1.GET("/report", func(ctx *gin.Context) { handler.ReportHandler(ctx, executor) })
groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) })
groupV1.PUT("/order", func(ctx *gin.Context) { handler.OrderHandler(ctx, executor) })
groupV1.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) })
return r
}
@@ -52,6 +52,10 @@ func (e *dummyExecutor) GenerateTurn() (rest.StateResponse, error) {
return rest.StateResponse{}, nil
}
func (e *dummyExecutor) BanishRace(raceName string) error {
return nil
}
func (e *dummyExecutor) GameState() (rest.StateResponse, error) {
return rest.StateResponse{}, nil
}
+3 -2
View File
@@ -27,7 +27,7 @@ func TestGetStatus(t *testing.T) {
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
@@ -37,7 +37,7 @@ func TestGetStatus(t *testing.T) {
assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String()))
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/api/v1/status", nil)
req, _ = http.NewRequest("GET", "/api/v1/admin/status", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, w.Body)
@@ -47,6 +47,7 @@ func TestGetStatus(t *testing.T) {
assert.Equal(t, initResponse.ID, stateResponse.ID)
assert.Equal(t, uint(0), stateResponse.Turn)
assert.Equal(t, uint(0), stateResponse.Stage)
assert.False(t, stateResponse.Finished)
assert.Len(t, stateResponse.Players, 10)
for i := range stateResponse.Players {
assert.NoError(t, uuid.Validate(stateResponse.Players[i].ID.String()))
+3 -3
View File
@@ -29,7 +29,7 @@ func TestGetTurn(t *testing.T) {
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
@@ -50,7 +50,7 @@ func TestGetTurn(t *testing.T) {
// generate next turn
w = httptest.NewRecorder()
req, _ = http.NewRequest("PUT", "/api/v1/turn", nil)
req, _ = http.NewRequest("PUT", "/api/v1/admin/turn", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, w.Body)
@@ -72,7 +72,7 @@ func TestGetTurn(t *testing.T) {
// validate status
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/api/v1/status", nil)
req, _ = http.NewRequest("GET", "/api/v1/admin/status", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, w.Body)