feat(robot): occasional off-strategy deviation, strict in the endgame
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
The robot followed its per-game playToWin/lose intent on every move, which made the outcome too predictable. It now flips that intent for a single move on ~20% of opening/midgame turns (a winning robot eases off, a losing one surges ahead), so the chosen strategy may not pan out — which favours the human. The chance tapers linearly to 0 over the last 14 tiles in the bag and is 0 once the bag is empty, so the endgame follows the chosen strategy strictly. The decision is deterministic from the seed (mix(seed,"deviate",moveCount)) and applies to both robot paths via the shared selectMove; the per-game play-to-win intent the admin card shows is unchanged. Adds deviateProb/deviates helpers and unit tests (taper bounds + monotonicity, never-in-endgame, determinism, ~20% distribution); bakes the behaviour into ARCHITECTURE §7, FUNCTIONAL (+_ru), backend/README, PRERELEASE and PLAN Stage 5.
This commit is contained in:
@@ -67,7 +67,8 @@ func (s *Service) handle(ctx context.Context, rt game.RobotTurn, now time.Time)
|
||||
// Honest-AI game: the robot moves the instant it is its turn — no sleep window and
|
||||
// no proactive nudge (chat and nudge are disabled in these games, and the player
|
||||
// chose an opponent that "moves at once"). It still plays to the same per-game
|
||||
// strength (playToWin) and margin band as the human-mimicry path.
|
||||
// strength (playToWin), margin band and occasional off-strategy deviation as the
|
||||
// human-mimicry path.
|
||||
if rt.VsAI {
|
||||
if rt.ToMove == rt.RobotSeat {
|
||||
return s.act(ctx, rt, now)
|
||||
@@ -164,9 +165,11 @@ func (s *Service) maybeNudge(ctx context.Context, rt game.RobotTurn, now time.Ti
|
||||
return nil
|
||||
}
|
||||
|
||||
// act reads the live turn, chooses a move by margin and submits it. State that
|
||||
// has moved on since the scan (a finished game, a turn that is no longer the
|
||||
// robot's) surfaces as a benign error and is skipped.
|
||||
// act reads the live turn, chooses a move by margin — usually toward the robot's
|
||||
// per-game intent, but with an occasional off-strategy deviation that fades to none
|
||||
// as the bag empties — and submits it. State that has moved on since the scan (a
|
||||
// finished game, a turn that is no longer the robot's) surfaces as a benign error
|
||||
// and is skipped.
|
||||
func (s *Service) act(ctx context.Context, rt game.RobotTurn, now time.Time) error {
|
||||
st, err := s.games.GameState(ctx, rt.GameID, rt.RobotID)
|
||||
if err != nil {
|
||||
@@ -179,7 +182,11 @@ func (s *Service) act(ctx context.Context, rt game.RobotTurn, now time.Time) err
|
||||
|
||||
myScore := st.Game.Seats[st.Seat].Score
|
||||
oppScore := bestOpponentScore(st.Game.Seats, st.Seat)
|
||||
d := selectMove(cands, myScore, oppScore, playToWin(rt.Seed), defaultBand, st.Rack, st.BagLen)
|
||||
win := playToWin(rt.Seed)
|
||||
if deviates(rt.Seed, rt.MoveCount, st.BagLen) {
|
||||
win = !win // an occasional off-strategy move; never once the bag is empty
|
||||
}
|
||||
d := selectMove(cands, myScore, oppScore, win, defaultBand, st.Rack, st.BagLen)
|
||||
|
||||
var res game.MoveResult
|
||||
switch d.kind {
|
||||
|
||||
Reference in New Issue
Block a user