Stage 17: backend defect fixes (nudge code, TG name, robot names/timing, multi-device push, move-duration metric + admin analytics)
- #3 nudge-on-own-turn: distinct result code nudge_own_turn + i18n (was reused 'not_your_turn') - #2 sanitize connector registration name to the editable format; Player/Игрок-XXXXX fallback - #5 variant-aware robot name pools (composed full/colloquial first + surname forms; ru gets <=20% latin) - #4 move-number-aware robot move timing (early 1-5min -> late 10-90min, skew k=4) - #7 emit move event to the actor too (multi-device sync); opponent_moved stays in-app only - #1 live game_move_duration{variant,phase} histogram + admin console per-user min/avg/max columns and an inline-SVG move-time-by-move-number chart (offline from the journal) - ProvisionRobot bypasses editor name validation (system names like 'Peter J.')
This commit is contained in:
@@ -23,17 +23,27 @@ const (
|
||||
// human wins about 60% of games (docs/ARCHITECTURE.md §7).
|
||||
playToWinPercent = 40
|
||||
|
||||
// delayMinMinutes and delayMaxMinutes bound a move delay; delaySkew shapes the
|
||||
// right-skewed distribution (short delays frequent). With skew 3.5 the median
|
||||
// is about 10 minutes and the mean about 20, with a tail out to the maximum.
|
||||
delayMinMinutes = 2.0
|
||||
delayMaxMinutes = 90.0
|
||||
delaySkew = 3.5
|
||||
// The robot's think time depends on how far the game has progressed: early moves
|
||||
// are quick and late moves can be long (endgame deliberation). The delay is drawn
|
||||
// from a band that interpolates with the move count from [delayEarlyLoMinutes,
|
||||
// delayEarlyHiMinutes] at the first move to [delayLateLoMinutes, delayLateHiMinutes]
|
||||
// by avgGameMoves, then right-skewed by delaySkew (a larger exponent concentrates
|
||||
// delays near the band's floor — an active player). The result is clamped to
|
||||
// [delayHardMinMinutes, delayHardMaxMinutes]. The numbers are deliberate estimates,
|
||||
// to be retuned once real play statistics arrive (docs/ARCHITECTURE.md §7).
|
||||
delayEarlyLoMinutes = 1.0
|
||||
delayEarlyHiMinutes = 5.0
|
||||
delayLateLoMinutes = 10.0
|
||||
delayLateHiMinutes = 90.0
|
||||
delaySkew = 4.0
|
||||
avgGameMoves = 28.0
|
||||
delayHardMinMinutes = 1.0
|
||||
delayHardMaxMinutes = 90.0
|
||||
|
||||
// nudgeReplyMinMinutes and nudgeReplyMaxMinutes bound how soon the robot
|
||||
// answers a daytime nudge on its turn.
|
||||
nudgeReplyMinMinutes = 2.0
|
||||
nudgeReplyMaxMinutes = 10.0
|
||||
// nudgeReplySpreadMinutes is the width of the quick window, anchored at the move's
|
||||
// lower band (delayBand's lo), within which the robot answers a daytime nudge on
|
||||
// its turn — so a nudged robot replies near the floor of its think time.
|
||||
nudgeReplySpreadMinutes = 5.0
|
||||
|
||||
// sleepStartHour and sleepEndHour bound the robot's nightly sleep in its
|
||||
// (opponent-anchored, drifted) local time: it makes no move and sends no nudge
|
||||
@@ -104,19 +114,48 @@ func playToWin(seed int64) bool {
|
||||
return mix(seed, "win")%100 < playToWinPercent
|
||||
}
|
||||
|
||||
// moveDelay is the robot's think time for the move at moveCount, sampled from the
|
||||
// right-skewed distribution and bounded to [delayMinMinutes, delayMaxMinutes).
|
||||
// delayBand returns the lower and upper bounds, in minutes, of the move-delay band
|
||||
// for the move at moveCount. It interpolates linearly with game progress (the move
|
||||
// count over avgGameMoves, capped at 1): early moves sit in a short band and late
|
||||
// moves in a long one.
|
||||
func delayBand(moveCount int) (lo, hi float64) {
|
||||
p := float64(moveCount) / avgGameMoves
|
||||
if p > 1 {
|
||||
p = 1
|
||||
}
|
||||
lo = delayEarlyLoMinutes + (delayLateLoMinutes-delayEarlyLoMinutes)*p
|
||||
hi = delayEarlyHiMinutes + (delayLateHiMinutes-delayEarlyHiMinutes)*p
|
||||
return lo, hi
|
||||
}
|
||||
|
||||
// moveDelay is the robot's think time for the move at moveCount: a right-skewed
|
||||
// sample from the move's delayBand, clamped to the hard bounds. The skew (delaySkew
|
||||
// > 1) makes short delays frequent and long ones rare, with a tail to the band's top.
|
||||
func moveDelay(seed int64, moveCount int) time.Duration {
|
||||
lo, hi := delayBand(moveCount)
|
||||
u := unitFloat(mix(seed, "delay", moveCount))
|
||||
mins := delayMinMinutes + (delayMaxMinutes-delayMinMinutes)*math.Pow(u, delaySkew)
|
||||
return time.Duration(mins * float64(time.Minute))
|
||||
return clampMinutes(lo + (hi-lo)*math.Pow(u, delaySkew))
|
||||
}
|
||||
|
||||
// nudgeReplyDelay is how soon after a daytime nudge the robot answers the move at
|
||||
// moveCount, sampled uniformly from [nudgeReplyMinMinutes, nudgeReplyMaxMinutes).
|
||||
// moveCount: a uniform sample from the quick window [lo, lo+nudgeReplySpreadMinutes],
|
||||
// where lo is the move's lower band — so a nudge pulls the move in near the floor of
|
||||
// the robot's think time.
|
||||
func nudgeReplyDelay(seed int64, moveCount int) time.Duration {
|
||||
lo, _ := delayBand(moveCount)
|
||||
u := unitFloat(mix(seed, "nudge", moveCount))
|
||||
mins := nudgeReplyMinMinutes + (nudgeReplyMaxMinutes-nudgeReplyMinMinutes)*u
|
||||
return clampMinutes(lo + nudgeReplySpreadMinutes*u)
|
||||
}
|
||||
|
||||
// clampMinutes converts a minute count to a duration, clamping it to the hard delay
|
||||
// bounds so an out-of-range band can never produce an absurd think time.
|
||||
func clampMinutes(mins float64) time.Duration {
|
||||
if mins < delayHardMinMinutes {
|
||||
mins = delayHardMinMinutes
|
||||
}
|
||||
if mins > delayHardMaxMinutes {
|
||||
mins = delayHardMaxMinutes
|
||||
}
|
||||
return time.Duration(mins * float64(time.Minute))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user