Stage 5: robot opponent (pool, seed-derived strategy, move driver, matchmaker substitution)
- internal/robot: durable kind='robot' account pool (migration 00004); every per-game and per-turn choice derived deterministically from the game seed (restart-stable FNV mix); a background move driver; margin targeting (band 1-30, closest-to-band); right-skewed [2,90]min delays (median ~10m); opponent-anchored sleep with +/-3h drift; daytime nudge reply + proactive 12h nudge; friend/chat blocked via profile toggles. - engine.Candidates (decoded ranked plays); game.Candidates + RobotTurns; social.LastNudgeAt. - matchmaker: 10s wait then robot substitution (reaper) + Poll delivery seam. - config (BACKEND_ROBOT_DRIVE_INTERVAL, BACKEND_LOBBY_ROBOT_WAIT, BACKEND_LOBBY_REAPER_INTERVAL); main wiring + boot-time pool provisioning. - metrics: robot account_stats (authoritative balance) + robot_games_finished_total OTel counter + per-finish log. - docs: PLAN, ARCHITECTURE, FUNCTIONAL(+ru), TESTING, README; account.go comment. - tests: robot strategy units, matchmaker reaper/Poll, engine.Candidates; inttest robot full-game / substitution / proactive-nudge.
This commit is contained in:
@@ -426,6 +426,43 @@ func (svc *Service) Hint(ctx context.Context, gameID, accountID uuid.UUID) (Hint
|
||||
return HintResult{Move: move, HintsRemaining: hintsRemaining(pre.HintsPerPlayer, used, walletAfter)}, nil
|
||||
}
|
||||
|
||||
// Candidates returns the to-move player's legal plays for a seated player on
|
||||
// their turn, ranked by descending score. It is the read the robot opponent uses
|
||||
// to choose a move by margin; it spends nothing and mutates no state. It returns
|
||||
// ErrNotAPlayer, ErrFinished or ErrNotYourTurn like the other turn-scoped reads.
|
||||
func (svc *Service) Candidates(ctx context.Context, gameID, accountID uuid.UUID) ([]engine.MoveRecord, error) {
|
||||
pre, err := svc.store.GetGame(ctx, gameID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seat, ok := pre.seatOf(accountID)
|
||||
if !ok {
|
||||
return nil, ErrNotAPlayer
|
||||
}
|
||||
if pre.Status != StatusActive {
|
||||
return nil, ErrFinished
|
||||
}
|
||||
if pre.ToMove != seat {
|
||||
return nil, ErrNotYourTurn
|
||||
}
|
||||
|
||||
unlock := svc.locks.lock(gameID)
|
||||
defer unlock()
|
||||
g, err := svc.liveGame(ctx, pre)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g.Candidates(), nil
|
||||
}
|
||||
|
||||
// RobotTurns returns the robot driver's view of every active game seating one of
|
||||
// robotIDs. It is the robot scheduler's periodic scan, mirroring the timeout
|
||||
// sweeper's ActiveGames read; the driver derives each robot's deadline from the
|
||||
// returned seed and turn cursor.
|
||||
func (svc *Service) RobotTurns(ctx context.Context, robotIDs []uuid.UUID) ([]RobotTurn, error) {
|
||||
return svc.store.RobotTurns(ctx, robotIDs)
|
||||
}
|
||||
|
||||
// GameState returns a seated player's view of the game: the shared summary plus
|
||||
// their private rack, the bag size and their remaining hint budget.
|
||||
func (svc *Service) GameState(ctx context.Context, gameID, accountID uuid.UUID) (StateView, error) {
|
||||
|
||||
Reference in New Issue
Block a user