services

package
v0.0.0-...-6723bfa Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: GPL-3.0 Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultTileWidth  = 64.0
	DefaultTileHeight = 64.0
	DefaultYIncrement = 48.0
	MaxUnits          = 20
)
View Source
const APP_ID = "lilbattle"
View Source
const SQRT3 = 1.732050808 // sqrt(3)

Variables

View Source
var CoordFromInt32 = lib.CoordFromInt32
View Source
var CubeDistance = lib.CubeDistance
View Source
var DirectionToLongString = lib.DirectionToLongString
View Source
var DirectionToString = lib.DirectionToString
View Source
var ErrNoSuchEntity = errors.New("entity not found")
View Source
var ErrNotImplemented = errors.New("operation not implemented")

ErrNotImplemented is returned when an operation is not supported in the current context

View Source
var ExtractPathCoords = lib.ExtractPathCoords
View Source
var FormatPathCompact = lib.FormatPathCompact
View Source
var FormatPathDetailed = lib.FormatPathDetailed
View Source
var GetDirection = lib.GetDirection
View Source
var HexToRowCol = lib.HexToRowCol
View Source
var ParseDirection = lib.ParseDirection
View Source
var ReconstructPath = lib.ReconstructPath
View Source
var RowColToHex = lib.RowColToHex

Functions

func Max

func Max(a, b int) int

max returns the maximum of two integers

func NormalizeWorldID

func NormalizeWorldID(id string) string

NormalizeWorldID lowercases world IDs for consistent lookup. All backends should call this at the entry of methods that accept a world ID.

func OptionToMove

func OptionToMove(opt *v1.GameOption) *v1.GameMove

OptionToMove wraps a GameOption variant in the parallel GameMove variant. The underlying action protos (MoveUnitAction, AttackUnitAction, etc.) are shared between the two parents — only the wrapper changes.

Types

type AuthFlow

type AuthFlow struct {
	BaseModel

	Id string `datastore:"id" json:"id"`

	// Kind of login being done
	Provider string `datastore:"provider" json:"provider"`

	// When this Auth session expires;
	ExpiresIn time.Time `datastore:"expiresIn" json:"expiresIn"`

	// Handler that will continue the flow after a successful AuthFlow.
	HandlerName string `datastore:"handlerName" json:"handlerName"`

	// Parameters for the handler to continue with.
	HandlerParams StringMapField `datastore:"handlerParams" json:"handlerParams"`
}

type AutoplayContextProvider

type AutoplayContextProvider func(ctx context.Context, currentPlayer int32) context.Context

AutoplayContextProvider returns the context to use for the next read/write against the GamesService. It receives the current player whose turn it is so callers can switch user identity per turn — needed for tests (where one caller drives all players) and for future dev-mode "swap as current player" behaviour. Pass nil to use the caller's ctx unchanged for every call.

type AxialCoord

type AxialCoord = lib.AxialCoord

type BackendGamesService

type BackendGamesService struct {
	BaseGamesService
	ClientMgr         *ClientMgr
	ScreenShotIndexer *ScreenShotIndexer
	GameStateUpdater  GameStateUpdater
	StorageProvider   GameStorageProvider // Set by concrete implementations

	// Cache configuration
	CacheEnabled bool // Set to true to enable in-memory caching
	// contains filtered or unexported fields
}

BackendGamesService provides shared caching and screenshot indexing logic for backend game services It embeds BaseGamesService and adds caching + screenshot management

func (*BackendGamesService) DeleteGame

DeleteGame deletes a game with transparent cache invalidation. Authorization: Only the game creator can delete a game.

func (*BackendGamesService) GetGame

GetGame returns game data, checking cache first then falling back to storage. This is the main entry point for reading game data - caching is transparent. If CacheEnabled is false, always loads from storage.

func (*BackendGamesService) GetRuntimeGame

func (s *BackendGamesService) GetRuntimeGame(game *v1.Game, gameState *v1.GameState) (*lib.Game, error)

GetRuntimeGame implements the GamesService interface

func (*BackendGamesService) GetRuntimeGameCached

func (s *BackendGamesService) GetRuntimeGameCached(id string, game *v1.Game, state *v1.GameState) *lib.Game

GetRuntimeGameCached returns a cached runtime game, creating one if needed If caching is disabled, always creates a new runtime game

func (*BackendGamesService) InitializeCache

func (s *BackendGamesService) InitializeCache()

InitializeCache sets up the in-memory cache maps and enables caching

func (*BackendGamesService) InitializePlayerStates

func (s *BackendGamesService) InitializePlayerStates(gameState *v1.GameState, config *v1.GameConfiguration)

InitializePlayerStates initializes the PlayerStates map in GameState from game config. This sets up initial coins (starting_coins + base income) for each player. Called during game creation by both fsbe and gormbe.

func (*BackendGamesService) InitializeScreenshotIndexer

func (s *BackendGamesService) InitializeScreenshotIndexer()

InitializeScreenshotIndexer wires up the screenshot indexer's background goroutine.

Precondition: s.ClientMgr must be set before calling. When ClientMgr is nil this is a deliberate "skip screenshots" signal (e.g. unit tests that pass nil to NewFSGamesService) and the goroutine is not started — otherwise it would eventually flush and dereference the nil ClientMgr in renderScreenshot, killing the test binary on an unrecovered panic. Send sites already guard on s.ScreenShotIndexer != nil so downstream code stays correct when the indexer is skipped.

If you set ClientMgr after this returns, call Initialize again — the no-op early return won't retroactively start the goroutine.

func (*BackendGamesService) InitializeSyncBroadcast

func (s *BackendGamesService) InitializeSyncBroadcast()

InitializeSyncBroadcast sets up the callback to broadcast moves to sync subscribers. Called by backend game services (fsbe, gormbe) after initialization.

func (*BackendGamesService) JoinGame

JoinGame allows an authenticated user to join an open player slot in a game. The player slot must have player_type = "open" to be joinable.

func (*BackendGamesService) SaveMoveGroup

func (s *BackendGamesService) SaveMoveGroup(ctx context.Context, gameId string, state *v1.GameState, group *v1.GameMoveGroup) error

SaveMoveGroup saves a move group with transparent cache update. It delegates move persistence to backend-specific SaveMoves, saves state, and updates cache.

func (*BackendGamesService) UpdateGame

UpdateGame updates an existing game with transparent caching. It loads current data, merges changes, saves via StorageProvider, and updates cache. Authorization: Only the game creator can update game metadata.

func (*BackendGamesService) ValidateCreateGameRequest

func (s *BackendGamesService) ValidateCreateGameRequest(game *v1.Game, worldData *v1.WorldData) error

ValidateCreateGameRequest validates a CreateGameRequest for common errors that apply to all backend implementations (fsbe, gormbe, etc.)

type BackendWorldsService

type BackendWorldsService struct {
	BaseWorldsService
	ClientMgr         *ClientMgr
	ScreenShotIndexer *ScreenShotIndexer
	WorldDataUpdater  WorldDataUpdater
}

BackendWorldsService provides shared screenshot indexing logic for backend world services It embeds BaseWorldsService and adds screenshot management

func (*BackendWorldsService) InitializeScreenshotIndexer

func (s *BackendWorldsService) InitializeScreenshotIndexer()

InitializeScreenshotIndexer wires up the screenshot indexer's background goroutine.

Precondition: s.ClientMgr must be set before calling. When ClientMgr is nil this is a deliberate "skip screenshots" signal (e.g. unit tests that pass nil to NewFSWorldsService) and the goroutine is not started — otherwise it would eventually flush and dereference the nil ClientMgr in renderScreenshot, killing the test binary on an unrecovered panic. Send sites already guard on s.ScreenShotIndexer != nil so downstream code stays correct when the indexer is skipped.

If you set ClientMgr after this returns, call Initialize again — the no-op early return won't retroactively start the goroutine.

type BaseBuildOptionsModal

type BaseBuildOptionsModal struct {
	PanelBase
	BuildOptions []*v1.BuildUnitAction
	Tile         *v1.Tile
	PlayerCoins  int32
}

func (*BaseBuildOptionsModal) Hide

func (*BaseBuildOptionsModal) Show

func (b *BaseBuildOptionsModal) Show(_ context.Context, tile *v1.Tile, buildOptions []*v1.BuildUnitAction, playerCoins int32)

type BaseCompactSummaryCardPanel

type BaseCompactSummaryCardPanel struct {
	PanelBase
	Tile *v1.Tile
	Unit *v1.Unit
}

func (*BaseCompactSummaryCardPanel) SetCurrentData

func (b *BaseCompactSummaryCardPanel) SetCurrentData(_ context.Context, tile *v1.Tile, unit *v1.Unit)

type BaseGameScene

type BaseGameScene struct {
	PanelBase
	CurrentPathsRequest      *v1.ShowPathRequest
	CurrentHighlightsRequest *v1.ShowHighlightsRequest
}

func (*BaseGameScene) ClearHighlights

func (b *BaseGameScene) ClearHighlights(_ context.Context, req *v1.ClearHighlightsRequest)

func (*BaseGameScene) ClearPaths

func (b *BaseGameScene) ClearPaths(context.Context)

func (*BaseGameScene) MoveUnit

Animation methods - no-ops for CLI

func (*BaseGameScene) RemoveUnitAt

func (*BaseGameScene) SetUnitAt

func (*BaseGameScene) ShowAttackEffect

func (*BaseGameScene) ShowCaptureEffect

func (*BaseGameScene) ShowHealEffect

func (*BaseGameScene) ShowHighlights

func (b *BaseGameScene) ShowHighlights(_ context.Context, h *v1.ShowHighlightsRequest)

func (*BaseGameScene) ShowPath

func (b *BaseGameScene) ShowPath(_ context.Context, p *v1.ShowPathRequest)

type BaseGameState

type BaseGameState struct {
	Game  *v1.Game
	State *v1.GameState
}

BaseGameState is a non-UI implementation of GameState interface Used for CLI and testing - stores game state without rendering

func (*BaseGameState) RemoveUnitAt

func (*BaseGameState) SetGameState

func (*BaseGameState) SetUnitAt

func (*BaseGameState) UpdateGameStatus

type BaseGameStatePanel

type BaseGameStatePanel struct {
	PanelBase
	Game                *v1.Game
	State               *v1.GameState
	PlayerStats         map[int32]*PlayerStats
	CurrentPlayerCoins  int32
	CurrentPlayerIncome int32
	IncomeBreakdown     string
	ViewerUserId        string // The user ID of the viewer (for showing Join buttons)
}

BaseGameStatePanel is a non-UI implementation of GameStatePanel

func (*BaseGameStatePanel) ComputeCurrentPlayerIncome

func (b *BaseGameStatePanel) ComputeCurrentPlayerIncome()

ComputeCurrentPlayerIncome calculates income for the current player

func (*BaseGameStatePanel) ComputePlayerStats

func (b *BaseGameStatePanel) ComputePlayerStats()

ComputePlayerStats calculates bases and units count per player from world data

func (*BaseGameStatePanel) GetPlayerColorHex

func (b *BaseGameStatePanel) GetPlayerColorHex(playerId int32) string

GetPlayerColorHex returns the primary hex color for a player from the theme

func (*BaseGameStatePanel) IsPlayerJoinable

func (b *BaseGameStatePanel) IsPlayerJoinable(player *v1.GamePlayer) bool

IsPlayerJoinable returns true if the player slot can be joined by the current viewer A slot is joinable if: 1. The player type is "open" 2. The viewer is logged in 3. The viewer is not already a player in this game

func (*BaseGameStatePanel) IsViewerLoggedIn

func (b *BaseGameStatePanel) IsViewerLoggedIn() bool

IsViewerLoggedIn returns true if the viewer is logged in

func (*BaseGameStatePanel) IsViewerPlayer

func (b *BaseGameStatePanel) IsViewerPlayer() bool

IsViewerPlayer returns true if the viewer is an assigned player in this game

func (*BaseGameStatePanel) Update

func (b *BaseGameStatePanel) Update(_ context.Context, game *v1.Game, state *v1.GameState)

Update refreshes the panel with current game state

type BaseGameViewPresenter

type BaseGameViewPresenter struct {
	GamesService GamesService
	RulesEngine  *v1.RulesEngine
	Theme        themes.Theme

	// All the "UI Elements" we will change state of
	GameState               GameState
	GameStatePanel          GameStatePanel
	TurnOptionsPanel        TurnOptionsPanel
	UnitStatsPanel          UnitStatsPanel
	DamageDistributionPanel DamageDistributionPanel
	TerrainStatsPanel       TerrainStatsPanel
	BuildOptionsModal       BuildOptionsModal
	CompactSummaryCardPanel CompactSummaryCardPanel
	GameScene               GameScene
	GameViewerPage          GameViewerPageClient // For mobile-specific RPC calls
	// contains filtered or unexported fields
}

type BaseGamesService

type BaseGamesService struct {
	Self         GamesService // The actual implementation
	OnMovesSaved MovesSavedCallback
}

func (*BaseGamesService) ApplyChangeResults

func (b *BaseGamesService) ApplyChangeResults(changes []*v1.GameMove, rtGame *lib.Game, game *v1.Game, state *v1.GameState) error

func (*BaseGamesService) CreateGame

func (s *BaseGamesService) CreateGame(ctx context.Context, req *v1.CreateGameRequest) (resp *v1.CreateGameResponse, err error)

func (*BaseGamesService) DeleteGame

func (s *BaseGamesService) DeleteGame(ctx context.Context, req *v1.DeleteGameRequest) (resp *v1.DeleteGameResponse, err error)

func (*BaseGamesService) GetGameState

func (s *BaseGamesService) GetGameState(ctx context.Context, req *v1.GetGameStateRequest) (resp *v1.GetGameStateResponse, err error)

Un implmeneted methods

func (*BaseGamesService) GetGames

func (s *BaseGamesService) GetGames(ctx context.Context, req *v1.GetGamesRequest) (resp *v1.GetGamesResponse, err error)

func (*BaseGamesService) GetOptionsAt

func (s *BaseGamesService) GetOptionsAt(ctx context.Context, req *v1.GetOptionsAtRequest) (out *v1.GetOptionsAtResponse, err error)

GetOptionsAt returns all available options at a specific position

func (*BaseGamesService) ListGames

func (s *BaseGamesService) ListGames(ctx context.Context, req *v1.ListGamesRequest) (resp *v1.ListGamesResponse, err error)

func (*BaseGamesService) ListMoves

func (s *BaseGamesService) ListMoves(ctx context.Context, req *v1.ListMovesRequest) (resp *v1.ListMovesResponse, err error)

func (*BaseGamesService) ProcessMoves

func (s *BaseGamesService) ProcessMoves(ctx context.Context, req *v1.ProcessMovesRequest) (resp *v1.ProcessMovesResponse, err error)

ProcessMoves processes moves for an existing game. It validates and applies moves, then delegates persistence to SaveMoveGroup. Authorization: User must be a player in the game AND it must be their turn.

func (*BaseGamesService) SimulateAttack

func (s *BaseGamesService) SimulateAttack(ctx context.Context, req *v1.SimulateAttackRequest) (resp *v1.SimulateAttackResponse, err error)

SimulateAttack simulates combat between two units and returns damage distributions

func (*BaseGamesService) SimulateFix

func (s *BaseGamesService) SimulateFix(ctx context.Context, req *v1.SimulateFixRequest) (resp *v1.SimulateFixResponse, err error)

SimulateFix simulates fix (repair) action and returns health restoration distribution

type BaseIndexerService

type BaseIndexerService struct {
	Self IndexerService // The actual implementation
	v1s.UnimplementedIndexerServiceServer
	// contains filtered or unexported fields
}

func (*BaseIndexerService) Bootstrap

func (b *BaseIndexerService) Bootstrap()

Starts the bootstrapping loop where we identify which records need indexing and post it to the indexer for eventual indexing. This will only run once. After this is run on startup, new creations/updates will come to the indexer directly. Ideally this should be limited in how many records it "finds". So as to not DDOS our indexer on startup. Only elements that are created or updated but not indexed since the last indexing should be considered

func (*BaseIndexerService) StartIndexer

func (b *BaseIndexerService) StartIndexer()

The indexer loop is key - it is the liason between the bookkeeper and the workers.

There are 2 main flows:

Reactive Flow: ==============

An entity is updated via an API (say Create or Update or even Delete) The API layer sends an "EnsureIndexState" call for this entity - ensure it has a updated_at field. If this is a new index state then the indexed_at would be null (or at 0) otherwise it would have updated_at > indexed_at. What this does is now indicate that we have these entries "ready" for indexing. The indexer loop runs periodically collecting items that are "ready" for indexing and proceeds to index them

Bootstrap Flow: ===============

The reactive flow is great when things are all running but we will have restarts (or the indexer will be restarted) or we may not even have a dedicated indexer and just run in batch mode manually

Here we need something like: Give me N entities that need to be indexed but do not have an entry in our Index table (or updated_at is missing). ie we could have a new entity that was created but it was never indexed so this would not exist in our table. Alternatively due to a bug the update never may have sent an update to the indexer service (so updated_at would never be > indexed_at)

So here the key is for us to search all entities at their source of truth, cross reference with index table. The naive way of doing this is:

for each item in Items;
	if item NOT IN indexerService:
    	indexerService.Ensure(item)		// we should do this for each index_type

Problem is this unnecessarily going through the entire dataset just to find which "new items" have to be reindexed (and only useful for cases when outages caused af few issues to be not indexed).

The other problem is this does not look at updated_at. For example if an item exists in the indexer but hasnt been updated (say item udpate failed after saving into source of truth but somehow the indexer wasnt available). here updated_at would < indexed_at but that is in correct. So we could update our loop above to be:

for each item in Items;
	if item NOT IN indexerService or item.updated_at > indexerService.getItem(item.id).updated_at:
    	indexerService.Ensure(item)		// we should do this for each index_type

Here we are checking the updated_at recorded time. This takes care of missing items but still need a full scan of our DB which is unnecesary (especially if items will change soon or for items that may never changes)

What is needed is a quick way to get items that would have changed - or atleast do so incrementally. We have a few options:

  1. Keep track of when the last indexing was (and on the indexer side we could only set this if no new items were found in a time window) and only get items that were updated (or created) after that time. Chances are those would be in need of an indexing (and ok to do so again). The way we could get this time is by simply querying the "latest" indexed item's indexed_at time

The problem is this. Consider the following:

On the indexer we have items A, B, C, D updated at T0, T1, T1, T2(increasing order) on the Db we have items with A, B, C, D with their updated_at being T1, T1, T1.5, T2 (ie only A and C have updated). If we pick T2 then since A and C have time "older" than T2 they wont be marked. But if we pick the "oldest" time - say T0 then what happens? here B and D will have to be re-indexed even though they have not changed. This in a way says every entry is behind by atmost X.

  1. Keep track of when an entity was last indexed for each index_type on the source of truth itself (as metadata) - then we could do a query where updated_at > indexed_at. This however does not exploit indexes due to comparing non static values. So we will have a calculated "needs_indexing" field that will be set each time we create/update an entity. Given there are "few" index types we could have an individual one for each field (and only update that when a particular type of work changes)

Then by having a partial index on this field we can find items needing indexing (even for specific types)

type BaseModel

type BaseModel struct {
	CreatedAt time.Time `datastore:"createdAt" json:"createdAt"`
	UpdatedAt time.Time `datastore:"updatedAt" json:"updatedAt"`
	Version   int       `datastore:"version" json:"version"` // used for optimistic locking
}

type BasePanel

type BasePanel interface {
	SetTheme(t themes.Theme)
	SetRulesEngine(t *v1.RulesEngine)
}

type BaseTilePanel

type BaseTilePanel struct {
	PanelBase
	Tile *v1.Tile
}

func (*BaseTilePanel) CurrentTile

func (b *BaseTilePanel) CurrentTile() *v1.Tile

func (*BaseTilePanel) SetCurrentTile

func (b *BaseTilePanel) SetCurrentTile(_ context.Context, u *v1.Tile)

type BaseTurnOptionsPanel

type BaseTurnOptionsPanel struct {
	BaseUnitPanel
	Options *v1.GetOptionsAtResponse
}

func (*BaseTurnOptionsPanel) CurrentOptions

func (b *BaseTurnOptionsPanel) CurrentOptions() *v1.GetOptionsAtResponse

func (*BaseTurnOptionsPanel) SetCurrentUnit

func (b *BaseTurnOptionsPanel) SetCurrentUnit(_ context.Context, unit *v1.Unit, options *v1.GetOptionsAtResponse)

type BaseUnitPanel

type BaseUnitPanel struct {
	PanelBase
	Unit *v1.Unit
}

func (*BaseUnitPanel) CurrentUnit

func (b *BaseUnitPanel) CurrentUnit() *v1.Unit

func (*BaseUnitPanel) SetCurrentUnit

func (b *BaseUnitPanel) SetCurrentUnit(_ context.Context, u *v1.Unit)

type BaseWorldsService

type BaseWorldsService struct {
	Self WorldsService // The actual implementation
}

func (*BaseWorldsService) CreateWorld

func (s *BaseWorldsService) CreateWorld(ctx context.Context, req *v1.CreateWorldRequest) (resp *v1.CreateWorldResponse, err error)

func (*BaseWorldsService) DeleteWorld

func (s *BaseWorldsService) DeleteWorld(ctx context.Context, req *v1.DeleteWorldRequest) (resp *v1.DeleteWorldResponse, err error)

func (*BaseWorldsService) GetWorld

func (s *BaseWorldsService) GetWorld(ctx context.Context, req *v1.GetWorldRequest) (resp *v1.GetWorldResponse, err error)

func (*BaseWorldsService) GetWorlds

func (s *BaseWorldsService) GetWorlds(ctx context.Context, req *v1.GetWorldsRequest) (resp *v1.GetWorldsResponse, err error)

func (*BaseWorldsService) ListWorlds

func (s *BaseWorldsService) ListWorlds(ctx context.Context, req *v1.ListWorldsRequest) (resp *v1.ListWorldsResponse, err error)

func (*BaseWorldsService) UpdateWorld

func (s *BaseWorldsService) UpdateWorld(ctx context.Context, req *v1.UpdateWorldRequest) (resp *v1.UpdateWorldResponse, err error)

type BuildOptionsModal

type BuildOptionsModal interface {
	BasePanel
	Show(context.Context, *v1.Tile, []*v1.BuildUnitAction, int32)
	Hide(context.Context)
}

type Channel

type Channel struct {
	BaseModel
	Provider string `datastore:"provider" json:"provider"`
	LoginId  string `datastore:"loginId" json:"loginId"`

	/**
	 * Credentials for this channel (like access tokens, passwords etc).
	 */
	Credentials StringMapField `datastore:"credentials" json:"credentials"`

	/**
	 * Profile as passed by the provider of the channel.
	 */
	Profile StringMapField `datastore:"profile" json:"profile"`

	/**
	 * When does this channel expire and needs another login/auth.
	 */
	ExpiresIn time.Time `datastore:"expiresIn" json:"expiresIn"`

	// The identity that this channel is verifying.
	IdentityKey string `datastore:"identityKey" json:"identityKey"`
}

*

  • Channel's represented federated verification objects. For example a Google
  • Signin would ensure that the user that goes through this flow will end up with
  • a Google signin Channel - which would verify a particular identity type.

func (*Channel) HasIdentity

func (c *Channel) HasIdentity() bool

func (*Channel) HasKey

func (c *Channel) HasKey() bool

func (*Channel) Key

func (c *Channel) Key() string

type ClientMgr

type ClientMgr struct {
	// contains filtered or unexported fields
}

func NewClientMgr

func NewClientMgr(svc_addr string) *ClientMgr

func (*ClientMgr) Address

func (c *ClientMgr) Address() string

func (*ClientMgr) ClientContext

func (c *ClientMgr) ClientContext(ctx context.Context, loggedInUserId string) context.Context

func (*ClientMgr) GetAuthService

func (c *ClientMgr) GetAuthService() *goalservices.AuthService

func (*ClientMgr) GetFileStoreSvcClient

func (c *ClientMgr) GetFileStoreSvcClient() (out v1s.FileStoreServiceClient)

func (*ClientMgr) GetGameSyncSvcClient

func (c *ClientMgr) GetGameSyncSvcClient() (out v1s.GameSyncServiceClient)

func (*ClientMgr) GetGamesSvcClient

func (c *ClientMgr) GetGamesSvcClient() (out v1s.GamesServiceClient)

func (*ClientMgr) GetIndexerSvcClient

func (c *ClientMgr) GetIndexerSvcClient() (out v1s.IndexerServiceClient)

func (*ClientMgr) GetWorldsSvcClient

func (c *ClientMgr) GetWorldsSvcClient() (out v1s.WorldsServiceClient)

type CompactSummaryCardPanel

type CompactSummaryCardPanel interface {
	BasePanel
	SetCurrentData(context.Context, *v1.Tile, *v1.Unit)
}

type DamageDistributionPanel

type DamageDistributionPanel interface {
	BasePanel
	CurrentUnit() *v1.Unit
	SetCurrentUnit(context.Context, *v1.Unit)
}

type GameStatePanel

type GameStatePanel interface {
	BasePanel
	Update(context.Context, *v1.Game, *v1.GameState)
}

type GameStateUpdater

type GameStateUpdater interface {
	// GetGameStateVersion retrieves GameState by ID and returns version
	GetGameStateVersion(ctx context.Context, id string) (version int64, err error)

	// UpdateGameStateScreenshotIndexInfo updates the GameState.ScreenshotIndexInfo with optimistic locking
	// Returns error if version mismatch (optimistic lock failure)
	UpdateGameStateScreenshotIndexInfo(ctx context.Context, id string, oldVersion int64, lastIndexedAt time.Time, needsIndexing bool) error
}

GameStateUpdater is an interface for updating GameState with optimistic locking

type GameStorageProvider

type GameStorageProvider interface {
	// Read operations - load directly from storage (no caching)
	LoadGame(ctx context.Context, id string) (*v1.Game, error)
	LoadGameState(ctx context.Context, id string) (*v1.GameState, error)
	LoadGameHistory(ctx context.Context, id string) (*v1.GameMoveHistory, error)

	// Write operations - save directly to storage
	SaveGame(ctx context.Context, id string, game *v1.Game) error
	SaveGameState(ctx context.Context, id string, state *v1.GameState) error
	SaveGameHistory(ctx context.Context, id string, history *v1.GameMoveHistory) error

	// SaveMoves saves a move group to storage - backend-specific implementation
	// For FS: appends to history file
	// For GORM: saves as individual rows with orphan cleanup
	SaveMoves(ctx context.Context, gameId string, group *v1.GameMoveGroup, currentGroupNumber int64) error

	// Delete operation
	DeleteFromStorage(ctx context.Context, id string) error
}

GameStorageProvider is implemented by concrete backends (fsbe, gormbe) to provide raw storage operations. BackendGamesService wraps these with caching. Concrete implementations should ONLY implement this interface - caching is handled transparently by BackendGamesService.

type GameSyncService

type GameSyncService struct {
	v1s.UnimplementedGameSyncServiceServer
	// contains filtered or unexported fields
}

GameSyncService handles real-time synchronization of game state across multiple connected clients for multiplayer gameplay.

This is a pure pub/sub service - it does NOT handle: - Move validation (that's GamesService) - RNG/seed management (that's lib/)

Architecture: - Uses gocurrent.AsyncFanOut for efficient per-game message broadcasting - GamesService calls Broadcast RPC after ProcessMoves succeeds - Subscribers receive GameUpdates via streaming RPC

func NewGameSyncService

func NewGameSyncService() *GameSyncService

NewGameSyncService creates a new sync service

func (*GameSyncService) Broadcast

Broadcast sends a GameUpdate to all subscribers of a game. Called by GamesService (via gRPC client) after ProcessMoves succeeds.

func (*GameSyncService) Subscribe

Subscribe streams game updates to a client. Supports reconnection via from_sequence.

func (*GameSyncService) SubscriberCount

func (s *GameSyncService) SubscriberCount(gameId string) int

SubscriberCount returns the number of subscribers for a game

type GameViewPresenter

type GameViewPresenter struct {
	BaseGameViewPresenter
	// contains filtered or unexported fields
}

func NewGameViewPresenter

func NewGameViewPresenter() *GameViewPresenter

NOTE - ONly API really needed here are "getters" and "move processors" so no Creations, Deletions, Listing or even GetGame needed - GetGame data is set when we create this

func (*GameViewPresenter) ApplyRemoteChanges

ApplyRemoteChanges applies WorldChanges from remote players (received via SyncService). Updates local game state and triggers UI updates.

func (*GameViewPresenter) BuildOptionClicked

func (s *GameViewPresenter) BuildOptionClicked(ctx context.Context, req *v1.BuildOptionClickedRequest) (resp *v1.BuildOptionClickedResponse, err error)

BuildOptionClicked handles when user clicks a build option in the BuildOptionsModal

func (*GameViewPresenter) ClientReady

func (s *GameViewPresenter) ClientReady(ctx context.Context, req *v1.ClientReadyRequest) (resp *v1.ClientReadyResponse, err error)

ClientReady is called by the browser after the UI/scene is fully initialized. This is when we can safely send visual updates like highlights.

func (*GameViewPresenter) EndTurnButtonClicked

func (s *GameViewPresenter) EndTurnButtonClicked(ctx context.Context, req *v1.EndTurnButtonClickedRequest) (resp *v1.EndTurnButtonClickedResponse, err error)

EndTurnButtonClicked handles when user clicks the end turn button

func (*GameViewPresenter) GetAllOptions

func (s *GameViewPresenter) GetAllOptions(ctx context.Context, gameId string) ([]*v1.GameOption, error)

GetAllOptions returns every actionable option available to the current player in the supplied game, aggregated across every unit and tile they own. "Actionable" excludes the global EndTurn option — drivers trigger EndTurn explicitly when the actionable pool is empty.

Options are returned in a deterministic order: positions sorted by (Q, R) ascending, then options in the order GetOptionsAt returned them per position. Determinism matters for seeded autoplay — same state + same picker seed must produce the same pick.

Returns an empty slice (not nil) when the player has nothing to do; the game-finished case returns an empty slice as well.

func (*GameViewPresenter) GetGame

func (s *GameViewPresenter) GetGame(ctx context.Context, gameId string) (resp *v1.GetGameResponse, err error)

func (*GameViewPresenter) InitializeGame

func (s *GameViewPresenter) InitializeGame(ctx context.Context, req *v1.InitializeGameRequest) (resp *v1.InitializeGameResponse, err error)

Our initial game loader

func (*GameViewPresenter) NextMove

func (s *GameViewPresenter) NextMove(ctx context.Context, gameId string) (*v1.GameOption, error)

NextMove returns one action the current player should take, or nil when the player has no further actionable options this turn (autoplay drivers interpret nil as the signal to call EndTurn).

Internally: collect the actionable pool via GetAllOptions, then hand it to the picker. The picker is internal to the presenter; drivers never see it. Swap the picker via SetPicker to change policy (Random / Heuristic / AI) without touching either driver. Deterministic under SetSeed.

func (*GameViewPresenter) SceneClicked

func (s *GameViewPresenter) SceneClicked(ctx context.Context, req *v1.SceneClickedRequest) (resp *v1.SceneClickedResponse, err error)

func (*GameViewPresenter) SetPicker

func (s *GameViewPresenter) SetPicker(p picker.Picker)

SetPicker replaces the policy NextMove uses. Useful for swapping in heuristic / AI implementations in tests or at runtime.

func (*GameViewPresenter) SetSeed

func (s *GameViewPresenter) SetSeed(seed int64)

SetSeed reseeds the picker RNG. Use for determinism in tests or for the CLI driver's --seed flag.

func (*GameViewPresenter) TurnOptionClicked

func (s *GameViewPresenter) TurnOptionClicked(ctx context.Context, req *v1.TurnOptionClickedRequest) (resp *v1.TurnOptionClickedResponse, err error)

TurnOptionClicked handles when user clicks on a turn option in the TurnOptionsPanel

type GameViewerPageClient

type GameViewerPageClient interface {
	SetAllowedPanels(context.Context, *v1.SetAllowedPanelsRequest) (*v1.SetAllowedPanelsResponse, error)
	SetCompactSummaryCard(context.Context, *v1.SetContentRequest) (*v1.SetContentResponse, error)
}

type GamesService

type GamesService interface {
	// Create a new game
	CreateGame(context.Context, *v1.CreateGameRequest) (*v1.CreateGameResponse, error)
	// *
	// Batch get multiple games by ID
	GetGames(context.Context, *v1.GetGamesRequest) (*v1.GetGamesResponse, error)
	// ListGames returns all available games
	ListGames(context.Context, *v1.ListGamesRequest) (*v1.ListGamesResponse, error)
	// GetGame returns a specific game with metadata
	GetGame(context.Context, *v1.GetGameRequest) (*v1.GetGameResponse, error)
	// *
	// Delete a particular game
	DeleteGame(context.Context, *v1.DeleteGameRequest) (*v1.DeleteGameResponse, error)
	// GetGame returns a specific game with metadata
	UpdateGame(context.Context, *v1.UpdateGameRequest) (*v1.UpdateGameResponse, error)
	// Gets the latest game state
	GetGameState(context.Context, *v1.GetGameStateRequest) (*v1.GetGameStateResponse, error)
	// List the moves for a game
	ListMoves(context.Context, *v1.ListMovesRequest) (*v1.ListMovesResponse, error)
	ProcessMoves(context.Context, *v1.ProcessMovesRequest) (*v1.ProcessMovesResponse, error)
	GetOptionsAt(context.Context, *v1.GetOptionsAtRequest) (*v1.GetOptionsAtResponse, error)
	// *
	// Simulates combat between two units to generate damage distributions
	// This is a stateless utility method that doesn't require game state
	SimulateAttack(context.Context, *v1.SimulateAttackRequest) (*v1.SimulateAttackResponse, error)
	// *
	// Simulates fix (repair) action to generate health restoration distributions
	// This is a stateless utility method that doesn't require game state
	SimulateFix(context.Context, *v1.SimulateFixRequest) (*v1.SimulateFixResponse, error)
	// Join a game as an open player slot
	JoinGame(context.Context, *v1.JoinGameRequest) (*v1.JoinGameResponse, error)
	GetRuntimeGame(game *v1.Game, gameState *v1.GameState) (*lib.Game, error)

	// SaveMoveGroup saves a move group atomically with the game state.
	// Each backend implements this with appropriate transactionality:
	// - GORM: uses database transaction
	// - FS: writes history then state (pseudo-atomic)
	// - Non-transactional: writes moves first, then commits via state update (checkpoint pattern)
	SaveMoveGroup(ctx context.Context, gameId string, state *v1.GameState, group *v1.GameMoveGroup) error
}

type GenID

type GenID struct {
	BaseModel
	Id        string `datastore:"id" json:"id"`
	OwnerId   string
	ExpiresAt time.Time
}

An ID Generated by our system

type Identity

type Identity struct {
	BaseModel
	IsActive bool `datastore:"isActive" json:"isActive"`

	// Type of identity being verified (eg email, phone etc).
	IdentityType string `datastore:"identityType" json:"identityType"`

	// The key specific to the identity (eg an email address or a phone number etc).
	//
	// type + key should be unique through out the system.
	IdentityKey string `datastore:"identityKey" json:"identityKey"`

	// The primary user that this identity can be associated with.
	// Identities do not need to be explicitly associted with a user especially
	// in systems where a single Identity can be used to front several users
	PrimaryUser string `datastore:"primaryUser" json:"primaryUser"`
}

*

  • An identify is a unique global "address" corresponding to a user.
  • For example the identify abc@example.com is a unique identify regardless
  • of which Channel is verifying it. Multiple channels can verify the same
  • entity, eg open auth by github, FB or Google can verify the same email
  • address.

func (*Identity) HasKey

func (i *Identity) HasKey() bool

func (*Identity) HasUser

func (i *Identity) HasUser() bool

func (*Identity) Key

func (i *Identity) Key() string

type IndexerService

type IndexerService interface {
	v1s.IndexerServiceServer
}

type MovesSavedCallback

type MovesSavedCallback func(ctx context.Context, gameId string, moves []*v1.GameMove, groupNumber int64)

MovesSavedCallback is called after moves are saved. Used by BackendGamesService to broadcast to sync subscribers.

type NeighborDirection

type NeighborDirection = lib.NeighborDirection

type OptionFormatter

type OptionFormatter struct {
	ShowPaths     bool // Whether to show movement paths
	DetailedPaths bool // Whether to show detailed path information
}

OptionFormatter provides utilities for formatting game options for display

func (*OptionFormatter) FormatAttackUnitAction

func (f *OptionFormatter) FormatAttackUnitAction(attackOpt *v1.AttackUnitAction) string

FormatAttackUnitAction formats an attack option with damage estimate

func (*OptionFormatter) FormatBuildUnitAction

func (f *OptionFormatter) FormatBuildUnitAction(buildOpt *v1.BuildUnitAction) string

FormatBuildUnitAction formats a build option

func (*OptionFormatter) FormatMoveUnitAction

func (f *OptionFormatter) FormatMoveUnitAction(moveOpt *v1.MoveUnitAction, allPaths *v1.AllPaths) string

FormatMoveUnitAction formats a movement option with optional path visualization

func (*OptionFormatter) FormatOption

func (f *OptionFormatter) FormatOption(option *v1.GameOption, allPaths *v1.AllPaths) string

FormatOption formats a single game option for display

func (*OptionFormatter) FormatOptions

func (f *OptionFormatter) FormatOptions(options []*v1.GameOption, allPaths *v1.AllPaths) []string

FormatOptions formats a list of game options

func (*OptionFormatter) FormatOptionsNumbered

func (f *OptionFormatter) FormatOptionsNumbered(options []*v1.GameOption, allPaths *v1.AllPaths) []string

FormatOptionsNumbered formats options with numbers for menu selection

type PanelBase

type PanelBase struct {
	Theme       themes.Theme
	RulesEngine *v1.RulesEngine
}

func (*PanelBase) SetRulesEngine

func (p *PanelBase) SetRulesEngine(r *v1.RulesEngine)

func (*PanelBase) SetTheme

func (p *PanelBase) SetTheme(t themes.Theme)

type PlayerStats

type PlayerStats struct {
	Bases int32
	Units int32
}

PlayerStats holds computed stats for a player (bases, units counts)

type Position

type Position = lib.Position

type RunAutoplayRequest

type RunAutoplayRequest struct {
	// Svc is the GamesService used both by the presenter (read-only via
	// GetGame / GetOptionsAt) and by the driver (write via ProcessMoves) so
	// a single consistent backend handles the whole loop. Pass a singleton
	// or FSGamesService for in-process tests; pass the CLI's wired service
	// in production.
	Svc GamesService

	// GameID identifies the target game in the service.
	GameID string

	// Seed pins the picker's RNG for determinism. Same (initial state, seed,
	// service) tuple must produce identical action sequences.
	Seed int64

	// MaxTurns is a runaway safety cap. When the loop observes this many
	// turn cycles without a Finished flip, it returns an error so a stuck
	// game can't burn CI time or RL collection budget. Pass <= 0 to use the
	// default of 200.
	MaxTurns int

	// MaxMoves bounds the number of ProcessMoves calls before the loop
	// returns normally (no error). Pass 0 or negative for "run to
	// completion." Useful for short deterministic test runs, replays, and
	// step-debugging.
	MaxMoves int

	// CtxFor lets callers swap the request context per turn (e.g. to inject
	// the current player's identity). Pass nil to use the base ctx
	// unchanged for every call.
	CtxFor AutoplayContextProvider
}

RunAutoplayRequest captures every parameter RunAutoplay needs. Follows the project's (ctx, *Request) → (*Response, error) convention so future additions (policy name, RL-format flag, observer hooks) extend the request rather than the function signature.

type RunAutoplayResponse

type RunAutoplayResponse struct {
	FinalState     *v1.GameState
	TurnsObserved  int
	HitSafetyCap   bool
	ActionsApplied int
}

RunAutoplayResponse is the terminal state captured at the end of a RunAutoplay invocation — either a finished game (Finished=true, WinningPlayer set) or the state at the bound if MaxMoves stopped the loop before the game ended.

func RunAutoplay

func RunAutoplay(ctx context.Context, req *RunAutoplayRequest) (*RunAutoplayResponse, error)

RunAutoplay drives the game to a terminal state via the presenter's NextMove policy. On each iteration it asks NextMove for an action; nil is interpreted as "end turn." Loops until GameState.Finished flips true, MaxTurns is exceeded, or MaxMoves is reached (whichever first).

Errors propagate without retry per CONSTRAINTS.md ("no defensive error handling") — autoplay halts loudly on the first failure rather than silently skipping moves and continuing on stale state.

type ScreenShotIndexer

type ScreenShotIndexer struct {
	ClientMgr *ClientMgr

	// Callback invoked after all screenshots for an item are complete
	OnComplete ScreenshotCompletionCallback
	// contains filtered or unexported fields
}

func NewScreenShotIndexer

func NewScreenShotIndexer(clientMgr *ClientMgr) *ScreenShotIndexer

func (*ScreenShotIndexer) Send

func (s *ScreenShotIndexer) Send(kind string, id string, version int64, worldData *v1.WorldData)

type ScreenShotItem

type ScreenShotItem struct {
	Kind      string
	Id        string
	Version   int64
	WorldData *v1.WorldData

	ThemeErrors map[string]error
	ThemeFiles  map[string]*v1.File
}

type ScreenshotCompletionCallback

type ScreenshotCompletionCallback func([]ScreenShotItem) error

ScreenshotCompletionCallback is called after all screenshots for an item are complete

type StringMapField

type StringMapField struct {
	Properties map[string]any
}

type Tag

type Tag struct {
	BaseModel
	Name           string
	NormalizedName string
	Description    string
	ImageUrl       string
	FirstUserId    string `datastore:"firstUser" json:"firstUser"`
	NumAppItems    int64
}

type TerrainStatsPanel

type TerrainStatsPanel interface {
	BasePanel
	CurrentTile() *v1.Tile
	SetCurrentTile(context.Context, *v1.Tile)
}

type TurnOptionsPanel

type TurnOptionsPanel interface {
	BasePanel
	CurrentOptions() *v1.GetOptionsAtResponse
	CurrentUnit() *v1.Unit
	SetCurrentUnit(context.Context, *v1.Unit, *v1.GetOptionsAtResponse)
}

type UnitStatsPanel

type UnitStatsPanel interface {
	BasePanel
	CurrentUnit() *v1.Unit
	SetCurrentUnit(context.Context, *v1.Unit)
}

type User

type User struct {
	BaseModel

	ID string `datastore:"id" json:"id"`

	IsActive bool `datastore:"isActive" json:"isActive"`
	// A globally unique user ID.  This User ID cannot be used as a login key.
	// Login's need to happen via the Identiites above and a username could be
	// one of the identities (which can be verified say via login/password mechanism)
	// Alternatively an email can be used as an identity that can also map to
	// a particular user.
	// Name    string `datastore:"name" json:"name"`
	// Email   string `datastore:"email" json:"email"`
	// Picture string `datastore:"picture" json:"picture"`
	ProfileInfo StringMapField `datastore:"profile" json:"profile"`
}

*

  • Once a channel has verified an Identity, the end result is a mapping to
  • a local user object that is the entry for authenticated actions within
  • the system. The User can also mean a user profile and can be extended
  • to be customized by the user of this library in their own specific app.

func (*User) Id

func (u *User) Id() string

func (*User) Profile

func (u *User) Profile() map[string]any

type WorldDataUpdater

type WorldDataUpdater interface {
	// GetWorldData retrieves WorldData by ID and returns version
	GetWorldData(ctx context.Context, id string) (version int64, err error)

	// UpdateWorldDataIndexInfo updates the IndexInfo with optimistic locking
	// Returns error if version mismatch (optimistic lock failure)
	UpdateWorldDataIndexInfo(ctx context.Context, id string, oldVersion int64, lastIndexedAt time.Time, needsIndexing bool) error
}

WorldDataUpdater is an interface for updating WorldData with optimistic locking

type WorldsService

type WorldsService interface {
	// *
	// Create a new world
	CreateWorld(context.Context, *v1.CreateWorldRequest) (*v1.CreateWorldResponse, error)
	// *
	// Batch get multiple worlds by ID
	GetWorlds(context.Context, *v1.GetWorldsRequest) (*v1.GetWorldsResponse, error)
	// ListWorlds returns all available worlds
	ListWorlds(context.Context, *v1.ListWorldsRequest) (*v1.ListWorldsResponse, error)
	// GetWorld returns a specific world with metadata
	GetWorld(context.Context, *v1.GetWorldRequest) (*v1.GetWorldResponse, error)
	// *
	// Delete a particular world
	DeleteWorld(context.Context, *v1.DeleteWorldRequest) (*v1.DeleteWorldResponse, error)
	// GetWorld returns a specific world with metadata
	UpdateWorld(context.Context, *v1.UpdateWorldRequest) (*v1.UpdateWorldResponse, error)
}

Directories

Path Synopsis
Package authz provides authorization utilities for LilBattle services.
Package authz provides authorization utilities for LilBattle services.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL