Files
SirRoboGarage/common_libs/guns/pattern_matcher.nim
T
SirStone 2cc2a3bd87 fix(ModularBot): ram loop prevention, dead-target guards, cleaner logging
- 30-tick cooldown after ghost-stuck/timeout ram exit prevents re-entry loop
- enemy_tracker.update() skips dead bots to prevent same-tick scan resurrection
- TFIL graphics cleared when ramming is active movement
- [config] logs: white base with green-highlighted changes only
- [ram:enter] logs trigger reason and key values on false→true transition
- [death] and [target-invalid] logs retained for diagnostics
2026-09-20 20:44:45 +02:00

156 lines
5.3 KiB
Nim

## Pattern-matching gun: searches movement history for a matching sequence,
## then plays it forward to predict future position.
## Reference: https://robowiki.net/wiki/Pattern_Matching
## Coordinate system: 0° = East, CCW positive (Tank Royale standard).
import std/math
import gun_harness/gun_interface
const
HistorySize* = 500
PatternLen* = 10 # ticks used as search key; ponytail: fixed, expose if tuning needed
type
MoveTick = object
velocity: float ## signed speed (px/tick)
headingDelta: float ## heading change in radians this tick
PatternMatcherGun* = object
buf: array[HistorySize, MoveTick]
head: int ## next write index (circular)
count: int ## filled entries (capped at HistorySize)
prevHeading: float
prevSpeed: float
prevTick: int
hasPrev: bool
# per-tick cache — avoid re-searching for multiple power bins
cacheTick: int
cacheX: float
cacheY: float
cacheValid: bool
debugGraphics*: bool
# --- circular buffer helpers ---
proc write(g: var PatternMatcherGun, m: MoveTick) {.inline.} =
g.buf[g.head] = m
g.head = (g.head + 1) mod HistorySize
if g.count < HistorySize: inc g.count
proc readAt(g: PatternMatcherGun, i: int): MoveTick {.inline.} =
## i = 0 is oldest, i = count-1 is newest
g.buf[(g.head - g.count + i + HistorySize * 2) mod HistorySize]
# --- linear fallback (same style as linear.nim) ---
proc linearPredict(state: WorldState, bulletSpeed: float): (float, float) =
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
let t = dist / bulletSpeed
let hRad = degToRad(state.enemyHeading)
let ex = clamp(state.enemyX + cos(hRad) * state.enemySpeed * t,
BotRadius, state.arenaWidth - BotRadius)
let ey = clamp(state.enemyY + sin(hRad) * state.enemySpeed * t,
BotRadius, state.arenaHeight - BotRadius)
(ex, ey)
# --- pattern search + play-forward ---
proc searchAndProject(g: PatternMatcherGun, state: WorldState,
bulletSpeed: float): (float, float) =
## Returns projected (x, y). Falls back to linear if history too short.
if g.count < PatternLen * 2:
return linearPredict(state, bulletSpeed)
# key = last PatternLen entries
let keyStart = g.count - PatternLen
# scan backwards for best match (exclude the key itself)
var bestScore = Inf
var bestMatch = -1
let scanEnd = g.count - PatternLen - 1 # last valid match start
for i in countdown(scanEnd, 0):
var score = 0.0
for k in 0 ..< PatternLen:
let a = g.readAt(keyStart + k)
let b = g.readAt(i + k)
let dv = a.velocity - b.velocity
let dh = a.headingDelta - b.headingDelta
score += dv * dv + dh * dh
if score < bestScore:
bestScore = score
bestMatch = i
if bestMatch < 0:
return linearPredict(state, bulletSpeed)
# play forward from bestMatch + PatternLen
let playStart = bestMatch + PatternLen
let playAvail = g.count - 1 - playStart # ticks we can replay
# iterative time estimate
let dist0 = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
var t = dist0 / bulletSpeed
var ex = state.enemyX
var ey = state.enemyY
for _ in 0..4:
let steps = min(int(t + 0.5), playAvail)
ex = state.enemyX
ey = state.enemyY
var heading = degToRad(state.enemyHeading)
var speed = state.enemySpeed
for s in 0 ..< steps:
let m = g.readAt(playStart + s)
heading += m.headingDelta
speed = m.velocity
ex += cos(heading) * speed
ey += sin(heading) * speed
# if we ran out of replay data, coast linearly from last simulated pos
let remaining = t - steps.float
if remaining > 0.0:
ex += cos(heading) * speed * remaining
ey += sin(heading) * speed * remaining
let ndx = ex - state.selfX
let ndy = ey - state.selfY
t = sqrt(ndx * ndx + ndy * ndy) / bulletSpeed
ex = clamp(ex, BotRadius, state.arenaWidth - BotRadius)
ey = clamp(ey, BotRadius, state.arenaHeight - BotRadius)
(ex, ey)
# --- Gun interface ---
proc predict*(g: var PatternMatcherGun, state: WorldState,
bulletSpeed: float): GunPrediction =
if bulletSpeed <= 0.0:
return GunPrediction(x: state.enemyX, y: state.enemyY)
# Update history once per tick
if g.hasPrev and state.tick > g.prevTick:
var dh = degToRad(state.enemyHeading) - degToRad(g.prevHeading)
# wrap to [-π, π]
while dh > PI: dh -= 2.0 * PI
while dh < -PI: dh += 2.0 * PI
g.write(MoveTick(velocity: g.prevSpeed, headingDelta: dh))
if not g.hasPrev or state.tick > g.prevTick:
g.prevHeading = state.enemyHeading
g.prevSpeed = state.enemySpeed
g.prevTick = state.tick
g.hasPrev = true
g.cacheValid = false # new tick invalidates cache
# Return cached result for same-tick calls (multiple power bins)
if g.cacheValid and state.tick == g.cacheTick:
return GunPrediction(x: g.cacheX, y: g.cacheY)
let (px, py) = g.searchAndProject(state, bulletSpeed)
g.cacheX = px
g.cacheY = py
g.cacheTick = state.tick
g.cacheValid = true
GunPrediction(x: px, y: py)
proc onResult*(g: var PatternMatcherGun, e: FeedbackEvent) =
discard # pattern matcher learns from movement observation, not feedback