Make the AI a little smarter.
This commit is contained in:
+22
-4
@@ -9,16 +9,34 @@ import (
|
||||
)
|
||||
|
||||
// winScore converts a simulated battle outcome to a utility for mySeat:
|
||||
// win 1, draw 0.5 (nobody gains ground), loss 0.
|
||||
// win 1, draw 0.5 (nobody gains ground), loss 0, plus a small margin term.
|
||||
//
|
||||
// The margin — your surviving pets minus the enemy's — breaks ties among
|
||||
// outcomes that share a verdict: a loss where you took most of the enemy down
|
||||
// beats a wipe, and a decisive win beats a squeaker. It is capped well under
|
||||
// 0.25 so it can never reorder win above draw above loss; it only decides
|
||||
// between moves the coarse win/draw/loss signal rates identically. That
|
||||
// gradient is what makes the bot play on sensibly when every option looks
|
||||
// hopeless — fielding its strongest force instead of picking at random.
|
||||
func winScore(res *game.BattleResult, mySeat int) float64 {
|
||||
var base float64
|
||||
switch res.WinnerSeat {
|
||||
case mySeat:
|
||||
return 1
|
||||
base = 1
|
||||
case -1:
|
||||
return 0.5
|
||||
base = 0.5
|
||||
default:
|
||||
return 0
|
||||
base = 0
|
||||
}
|
||||
margin := 0
|
||||
for seat, s := range res.Survivors {
|
||||
if seat == mySeat {
|
||||
margin += s
|
||||
} else {
|
||||
margin -= s
|
||||
}
|
||||
}
|
||||
return base + 0.1*math.Tanh(float64(margin)/3)
|
||||
}
|
||||
|
||||
// winProb estimates the chance the arranged deck wins the upcoming battle by
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/greyson/super-auto-pets-board-game/internal/game"
|
||||
)
|
||||
|
||||
// TestWinScoreMarginBreaksTiesNotVerdicts pins down the margin tie-breaker:
|
||||
// within a single verdict it prefers the more decisive result (a win with pets
|
||||
// to spare, a loss that took most of the enemy down), but no margin, however
|
||||
// lopsided, may ever raise a loss above a draw or a draw above a win.
|
||||
func TestWinScoreMarginBreaksTiesNotVerdicts(t *testing.T) {
|
||||
mk := func(winner, mine, theirs int) *game.BattleResult {
|
||||
return &game.BattleResult{WinnerSeat: winner, Survivors: []int{mine, theirs}}
|
||||
}
|
||||
|
||||
decisiveWin := winScore(mk(0, 5, 0), 0)
|
||||
narrowWin := winScore(mk(0, 1, 0), 0)
|
||||
drawAhead := winScore(mk(-1, 3, 1), 0)
|
||||
evenDraw := winScore(mk(-1, 0, 0), 0)
|
||||
drawBehind := winScore(mk(-1, 1, 3), 0)
|
||||
narrowLoss := winScore(mk(1, 0, 1), 0)
|
||||
blowoutLoss := winScore(mk(1, 0, 6), 0)
|
||||
|
||||
// Ties within a verdict resolve toward the stronger finish.
|
||||
if !(decisiveWin > narrowWin) {
|
||||
t.Errorf("a win with survivors should beat a squeaker: %.4f !> %.4f", decisiveWin, narrowWin)
|
||||
}
|
||||
if !(drawAhead > evenDraw && evenDraw > drawBehind) {
|
||||
t.Errorf("draws should order by margin: ahead %.4f, even %.4f, behind %.4f", drawAhead, evenDraw, drawBehind)
|
||||
}
|
||||
if !(narrowLoss > blowoutLoss) {
|
||||
t.Errorf("a narrow loss should beat a blowout: %.4f !> %.4f", narrowLoss, blowoutLoss)
|
||||
}
|
||||
|
||||
// Verdict ordering is inviolable: the best-possible loss still ranks below
|
||||
// the worst-possible draw, and the best draw below the worst win.
|
||||
if !(narrowWin > drawAhead) {
|
||||
t.Errorf("the worst win must outrank the best draw: %.4f !> %.4f", narrowWin, drawAhead)
|
||||
}
|
||||
if !(drawBehind > narrowLoss) {
|
||||
t.Errorf("the worst draw must outrank the best loss: %.4f !> %.4f", drawBehind, narrowLoss)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user