Documentation
¶
Index ¶
- Constants
- Variables
- func BandTop() int
- func ClampToDeployBand(p image.Point, uiCutoff int) image.Point
- func GetAllCounts(counts []TroopCount) map[int]int
- func GetCountForSlot(counts []TroopCount, slotX int) int
- func GetCountForSlotMeasured(counts []TroopCount, slotX int) (int, bool)
- func GetSlotActivityRatioStatic(screen gocv.Mat, x, y, screenW int) float64
- func GetStrategyUnitNames(s *strategy.DynamicStrategy) []string
- func HeroHPFraction(screen gocv.Mat, cardCX, slotY int, cal *game.Calibration) float64
- func NextEdgeIndex() string
- func ReadableCounts(counts []TroopCount) (readable, total int)
- func SlotProbeSize(screenW int) int
- func UICutoff(h int) int
- func YTopMin() int
- type DeployLine
- type DeployLineCalculator
- type DeployPlanner
- type Executor
- func (e *Executor) CalculateInBetween(edge string, offset int, bT, bB, bL, bR, fT, fB, fL, fR image.Point) (p1, p2 image.Point)
- func (e *Executor) DeployDynamic(s *strategy.DynamicStrategy, screen gocv.Mat) (int, error)
- func (e *Executor) DeployDynamicV2(s *strategy.DynamicStrategy, screen gocv.Mat, strategyPath string) (int, error)
- func (e *Executor) EndBattle() error
- func (e *Executor) GetSlotActivityRatio(screen gocv.Mat, x, y int) float64
- func (e *Executor) GetSlotY(h, mBarY int) int
- func (e *Executor) GetTemplates() map[string]gocv.Mat
- func (e *Executor) IsSlotEmpty(screen gocv.Mat, x, y int) bool
- func (e *Executor) LastDestructionPercent() int
- func (e *Executor) MaximizeLineSpread(p1, p2 image.Point, w, mBarY int) (image.Point, image.Point)
- func (e *Executor) ParseLayout(screen gocv.Mat, pCfg PrecisionConfig, w, h, mBarY int) []TroopSlot
- func (e *Executor) ResetBattleOutcome()
- func (e *Executor) ReturnHome() error
- func (e *Executor) SetActiveStrategy(s *strategy.DynamicStrategy)
- func (e *Executor) SetClassifier(fn func(gocv.Mat) (game.GameState, int))
- func (e *Executor) SetDeploymentContext(ctx context.Context, taps *TapExecutor) func()
- func (e *Executor) SetFullArmyRequired(required bool)
- func (e *Executor) SetRuntimeContext(ctx context.Context)
- func (e *Executor) SweepRemainingSlots(screen gocv.Mat, pCfg PrecisionConfig, targetEdge string, w, h int, mBarY int, ...)
- func (e *Executor) ThDestroyed() bool
- func (e *Executor) UpdateConfig(cfg *config.AttackConfig)
- func (e *Executor) Validate(s *strategy.DynamicStrategy) error
- func (e *Executor) WaitForBattleEnd(timeout time.Duration) bool
- func (e *Executor) WaitForBattleEndCtx(ctx context.Context, timeout time.Duration) bool
- type HeroDeployment
- type HeroHPMonitor
- type HeroManager
- func (hm *HeroManager) DeployHeroes(heroUnits []strategy.Unit, screen gocv.Mat) []*TrackedSlot
- func (hm *HeroManager) DeploySiege(unit strategy.Unit, slot *TrackedSlot) bool
- func (hm *HeroManager) DeployTroops(unit strategy.Unit, slot *TrackedSlot, pattern string, offset int, ...) bool
- func (hm *HeroManager) SetActivateAbility(v bool)
- func (hm *HeroManager) SetPinnedGround(v bool)
- func (hm *HeroManager) SetRedZone(z RedZone)
- type ManualEdge
- type PhasePlan
- type PrecisionConfig
- type RedLineDetector
- type RedZone
- type RetryPolicy
- type RotationState
- type SlotManager
- func (sm *SlotManager) GetAllSlots() []*TrackedSlot
- func (sm *SlotManager) GetBarY() int
- func (sm *SlotManager) GetEventTroops(strategyUnitNames []string) []*TrackedSlot
- func (sm *SlotManager) GetSlot(unitName string) *TrackedSlot
- func (sm *SlotManager) GetSlotY() int
- func (sm *SlotManager) GetUndeployedSlots() []*TrackedSlot
- func (sm *SlotManager) MarkDeployed(unitName string)
- func (sm *SlotManager) MarkFailed(unitName string)
- func (sm *SlotManager) MarkSlotDeployed(slot *TrackedSlot)
- func (sm *SlotManager) MarkSlotFailed(slot *TrackedSlot)
- func (sm *SlotManager) RecordAttempt(unitName string, success bool)
- type SlotState
- type SpellDeployer
- func (sd *SpellDeployer) DeploySpell(unit strategy.Unit, slot *TrackedSlot, targetEdge string, phasePattern string) bool
- func (sd *SpellDeployer) SetCounter(counter *TroopCounter, barY int)
- func (sd *SpellDeployer) SetPinnedGround(v bool)
- func (sd *SpellDeployer) VerifyAndReconcile(unit strategy.Unit, slot *TrackedSlot, targetEdge, phasePattern string, ...) (int, bool)
- type StallConfig
- type Sweeper
- type TapExecutor
- func (t *TapExecutor) CaptureFresh() (gocv.Mat, error)
- func (t *TapExecutor) CaptureSettledSlotRatio(screenW int, slot *TrackedSlot) (float64, bool)
- func (t *TapExecutor) DeployBudgetExhausted() bool
- func (t *TapExecutor) DeployBudgetRemaining() time.Duration
- func (t *TapExecutor) HeroSpotTapBudgetLeft(slot *TrackedSlot) bool
- func (t *TapExecutor) HumanSleep(baseMs, stdDevMs int)
- func (t *TapExecutor) InputErr() error
- func (t *TapExecutor) PlaceSiege(slot *TrackedSlot, pt image.Point, stdDev float64) bool
- func (t *TapExecutor) RearmSlot(slot *TrackedSlot) bool
- func (t *TapExecutor) RestoreSelection() bool
- func (t *TapExecutor) SetSlotResolver(fn func(unitName string) *TrackedSlot)
- func (t *TapExecutor) SiegeGround() (image.Point, bool)
- func (t *TapExecutor) SiegeNudges() int
- func (t *TapExecutor) SiegePlacements() int
- func (t *TapExecutor) StartDeployBudget()
- func (t *TapExecutor) StartDeployBudgetAt(deadline time.Time)
- func (t *TapExecutor) TapDeployFourSides(pCfg PrecisionConfig, targetEdge string, countPerSide int, jitterPx int)
- func (t *TapExecutor) TapDeployLine(p1, p2 image.Point, count int, jitterPx int)
- func (t *TapExecutor) TapDeployPoint(pt image.Point, count int, jitterPx int)
- func (t *TapExecutor) TapFast_ok(x, y int) bool
- func (t *TapExecutor) TapField(x, y int, stdDev float64) error
- func (t *TapExecutor) TapHeroAbility(slot *TrackedSlot) bool
- func (t *TapExecutor) TapSlot(slot *TrackedSlot, jitterPx int) bool
- func (t *TapExecutor) TapUnitField(pt image.Point, stdDev float64)
- func (t *TapExecutor) WaitForSettle(duration time.Duration)
- type TrackedSlot
- type TroopCount
- type TroopCounter
- func (tc *TroopCounter) BarLegible(screen gocv.Mat, barY, slotY int) bool
- func (tc *TroopCounter) DetectCount(screen gocv.Mat, slot *TrackedSlot, barY int) (int, bool)
- func (tc *TroopCounter) DetectCounts(screen gocv.Mat, slots []*TrackedSlot, barY int) []TroopCount
- func (tc *TroopCounter) DetectCountsMerged(frames []gocv.Mat, slots []*TrackedSlot, barY int) []TroopCount
- func (tc *TroopCounter) HasDigitTemplates() bool
- func (tc *TroopCounter) SlotHasCountLabel(screen gocv.Mat, slotX, slotY, barY int) bool
- type TroopSlot
- type UnitPlan
- type Verifier
- type VerifyConfig
- type WatchedHero
Constants ¶
const DeployBudget = 170 * time.Second
DeployBudget bounds the entire deploy phase (strategy phases + spell / hero reconcile + sweep + verify) from the moment DeployDynamicV2 starts. A CoC battle lasts 3 minutes from the first drop; every reconcile/sweep loop below consults DeployBudgetExhausted so a slot whose "empty" check keeps failing (observed live: a spent earthquake/spell card stuck in its cooldown read as visually non-empty forever) can never keep firing taps past the battle timer — the stuck-sweep symptom that burned minutes and hundreds of taps after the battle was already over. The budget is ~2x the longest legitimately-observed full deployment (~100s) so real attacks are never cut short.
const DeployGoal = 7 * time.Second
DeployGoal is the wall-clock target for the placement phases themselves (troops, siege, heroes, spells — from the first phase's first tap to the last spell cast), as opposed to DeployBudget, which is the hard stop that keeps a stuck slot from tapping past the battle timer. It exists so the run states its own number: every phase logs its elapsed time against this goal, and the window is reported whether or not it is met, because the only way to move a wall clock is to measure it.
Variables ¶
var ErrBattleFinished = errors.New("battle ended during deployment")
Functions ¶
func BandTop ¶
func BandTop() int
BandTop is the first row a tap reliably reaches. yTopMin is the codebase's definition of where the resource HUD ends, but the HUD's last row lands a few pixels below that, so the field really starts at yTopMin + bandTopInset. The picker warns against the same number, so what the tool calls safe is exactly what the bot leaves alone.
func ClampToDeployBand ¶
ClampToDeployBand pulls a coordinate back into the strip of field a tap can actually reach.
The top resource HUD (loot counters, battle clock, settings) covers roughly the first yTopMin rows, and the troop bar begins at uiCutoff. A tap on either is consumed by the chrome instead of deploying, so a pinned point up there is never what the user meant: they aimed at the field and the picker recorded a spot under the overlay. Left alone it is silently thrown away, which is how a pinned side can produce a battle where nothing deploys.
Only y is touched. The left/right pads are a placement preference rather than a UI boundary, and a line hugging the screen edge is a legitimate plan.
func GetAllCounts ¶
func GetAllCounts(counts []TroopCount) map[int]int
GetAllCounts returns a map of slot X -> count for the slots whose read was a trustworthy measurement. Slots with no readable count are omitted rather than reported as 0, so a blind read can never be mistaken downstream for a measured empty card.
func GetCountForSlot ¶
func GetCountForSlot(counts []TroopCount, slotX int) int
GetCountForSlot returns the detected count for a specific slot X coordinate. A slot whose read was not trustworthy reports 0 — callers that need to tell "empty" from "unreadable" must use DetectCounts and inspect TroopCount.OK.
func GetCountForSlotMeasured ¶
func GetCountForSlotMeasured(counts []TroopCount, slotX int) (int, bool)
GetCountForSlotMeasured returns the slot's battle-start count together with whether the reader actually measured it. The count alone cannot say: an unreadable card and an empty one both come back 0. Callers that use the number to decide a tap count need the difference (see HeroManager.DeployTroops's detectedTrusted argument).
func GetSlotActivityRatioStatic ¶
GetSlotActivityRatioStatic returns the ratio of active content pixels in a slot region.
func GetStrategyUnitNames ¶
func GetStrategyUnitNames(s *strategy.DynamicStrategy) []string
GetStrategyUnitNames returns all unit names from a strategy.
func HeroHPFraction ¶
HeroHPFraction returns the fill fraction (0..1) of a deployed hero's HP bar: the share of strip columns containing green pixels. Green = g>110 and dominant over r/b by 25, same rule as the reference bot.
func NextEdgeIndex ¶
func NextEdgeIndex() string
NextEdgeIndex atomically advances the persistent rotation counter and returns the next corner name in the cycle.
Failure modes (all degraded gracefully, never panic, never block):
- File missing: first call returns index 0 (TopLeft) and persists the new state.
- File empty: same as missing.
- File corrupted (invalid JSON): same as missing; the next call overwrites with valid JSON.
- File with out-of-range LastIndex (e.g. -1, 99): defensively reset to 0, then advance normally — no crash on weird persistent state.
- File write error (disk full, permission denied): log to stderr, still return the computed next index. Cross-restart continuity is lost in this case but the bot keeps cycling.
The call is wrapped in rotationMu.Lock/Unlock so concurrent invocations from the bot's parallel attack goroutines don't drop increments or interleave read/write.
func ReadableCounts ¶
func ReadableCounts(counts []TroopCount) (readable, total int)
ReadableCounts reports how many of these reads were trustworthy measurements out of the slots present.
func SlotProbeSize ¶
SlotProbeSize is the half-width in pixels of the square region slotActivity samples around a slot centre at a given screen width (25px at the 860px reference layout, scaled by the screen width otherwise).
Exported so the hero/sweep/verify failure logs can report the exact geometry they measured. That matters because the hero "deployed?" test is a delta on this region's activity ratio: if the square does not actually cover the card (wrong centre, or a size that spills onto neighbouring slots and the bar chrome), the delta is noise and a hero that deployed fine fails its own check — which is what makes the bot fire an extra retry tap.
func UICutoff ¶
UICutoff is the first row of the game's own chrome at the bottom of the frame (the troop bar). Everything above it is field; a tap below it is given to the bar instead of the battlefield. One definition, shared by the red-line detector, the band clamp and the hero ground ladder, because a disagreement here is what puts a tap on the UI.
func YTopMin ¶
func YTopMin() int
YTopMin is the top of the deployable band: the deploy line never places a point above it, because the resource HUD occupies the rows above. Exported so the tools that render or verify a plan (cmd/attack_verify) and the formula clamp share one definition of the band instead of each keeping its own percentage of the screen.
Types ¶
type DeployLine ¶
DeployLine represents a calculated deployment line.
type DeployLineCalculator ¶
type DeployLineCalculator struct {
// contains filtered or unexported fields
}
DeployLineCalculator computes deployment lines dynamically.
func NewDeployLineCalculator ¶
func NewDeployLineCalculator(logger zerolog.Logger) *DeployLineCalculator
NewDeployLineCalculator creates calculator.
func (*DeployLineCalculator) Calculate ¶
func (d *DeployLineCalculator) Calculate( zone RedZone, screenW, screenH, uiCutoff int, preferSide string, count int, ) DeployLine
Calculate returns a deployment line outside the red zone. Picks edge with most free space, places line 80px outside red zone.
type DeployPlanner ¶
type DeployPlanner struct {
// contains filtered or unexported fields
}
DeployPlanner resolves strategy YAML into concrete deployment plans.
func NewDeployPlanner ¶
func NewDeployPlanner( slotManager *SlotManager, pCfg PrecisionConfig, targetEdge string, w, h int, logger zerolog.Logger, ) *DeployPlanner
NewDeployPlanner creates a new deployment planner.
func (*DeployPlanner) PlanDeployment ¶
func (dp *DeployPlanner) PlanDeployment(s *strategy.DynamicStrategy) []PhasePlan
PlanDeployment resolves all phases into concrete deployment plans.
type Executor ¶
type Executor struct {
OnPhaseStart func(phase string, edge string)
OnUnitDeploy func(unit string, slotX int, slotY int)
OnDukePick func(targetEdge string, chosenEdge string)
// contains filtered or unexported fields
}
func NewExecutor ¶
func NewExecutor(client *adb.Client, cal *game.Calibration, cfg *config.AttackConfig, logger zerolog.Logger) *Executor
func (*Executor) CalculateInBetween ¶
func (*Executor) DeployDynamic ¶
func (*Executor) DeployDynamicV2 ¶
func (e *Executor) DeployDynamicV2(s *strategy.DynamicStrategy, screen gocv.Mat, strategyPath string) (int, error)
DeployDynamicV2 deploys troops using dynamic red line detection. No hardcoded precision_config.json needed - detects deployment boundary live.
strategyPath is the on-disk YAML path. The orchestrator uses it to find the matching formula.json (loaded as <stem>_formula.json next to the YAML). Pass "" to skip formula lookup entirely.
func (*Executor) GetSlotActivityRatio ¶
GetSlotActivityRatio is the exported wrapper for debug scripts.
func (*Executor) LastDestructionPercent ¶
LastDestructionPercent returns the highest destruction percentage the battle-end wait measured from the stall ROI (0 when nothing was read).
func (*Executor) MaximizeLineSpread ¶
func (*Executor) ParseLayout ¶
func (*Executor) ResetBattleOutcome ¶
func (e *Executor) ResetBattleOutcome()
ResetBattleOutcome clears the per-battle destruction/TH state. Called at the top of WaitForBattleEndCtx so a misread from a previous battle can never bleed into the next battle's star computation (observed live: battle 1's garbage 381% was still latched on the shared Executor when battle 2's result was parsed, turning a 32% defeat into "3 stars").
func (*Executor) ReturnHome ¶
func (*Executor) SetActiveStrategy ¶
func (e *Executor) SetActiveStrategy(s *strategy.DynamicStrategy)
SetActiveStrategy records the strategy being executed so battle-end waiting can honor per-strategy knobs (end_at_percent). Pass nil to clear.
func (*Executor) SetClassifier ¶
func (*Executor) SetDeploymentContext ¶
func (e *Executor) SetDeploymentContext(ctx context.Context, taps *TapExecutor) func()
SetDeploymentContext binds deployment taps to the bot lifecycle and starts fresh-frame battle-end monitoring. Clear it after deployment returns.
func (*Executor) SetFullArmyRequired ¶
SetFullArmyRequired enables the positive-evidence full-army gate at deploy start (see the fullArmyRequired field for the exact semantics).
func (*Executor) SetRuntimeContext ¶
SetRuntimeContext records the bot lifecycle context so a Stop is observed by the deployment guard even outside the client's own input paths. Pass nil for standalone tools (background context).
func (*Executor) SweepRemainingSlots ¶
func (*Executor) ThDestroyed ¶
ThDestroyed reports whether the golden Town Hall destroyed banner was seen during the wait (false when no th_banner_zone is configured, i.e. the TH state is unknown).
func (*Executor) UpdateConfig ¶
func (e *Executor) UpdateConfig(cfg *config.AttackConfig)
func (*Executor) Validate ¶
func (e *Executor) Validate(s *strategy.DynamicStrategy) error
Validate ensures all required templates for the strategy exist or are covered by manual labels
func (*Executor) WaitForBattleEnd ¶
WaitForBattleEnd is the context-free variant of WaitForBattleEndCtx. Kept as the stable entry point for callers (CLI, tests) that don't need cancellation.
type HeroDeployment ¶
type HeroDeployment struct {
Unit strategy.Unit
Slot *TrackedSlot
IsAbility bool
}
HeroDeployment represents a resolved hero deployment.
type HeroHPMonitor ¶
type HeroHPMonitor struct {
// contains filtered or unexported fields
}
HeroHPMonitor polls watched heroes' HP strips and fires each ability once: on low HP, or proactively for the warden shortly after deploy.
func NewHeroHPMonitor ¶
func NewHeroHPMonitor(heroes []*WatchedHero) *HeroHPMonitor
func (*HeroHPMonitor) AllDone ¶
func (m *HeroHPMonitor) AllDone() bool
AllDone reports every watched hero resolved (ability fired or confirmed dead).
func (*HeroHPMonitor) Poll ¶
func (m *HeroHPMonitor) Poll(screen gocv.Mat, cal *game.Calibration, now time.Time, tap func(h *WatchedHero) bool)
Poll reads each unresolved hero once against screen and taps for the ability when due. tap receives the hero (X/SlotY card point, Warden flag for the card's Y offset); the caller owns transport.
func (*HeroHPMonitor) Track ¶
func (m *HeroHPMonitor) Track(slot *TrackedSlot)
Track adds a deployed hero that still holds its ability tap (SpotTaps < maxHeroSpotTaps: one tap places, one more fires the ability) so the battle watcher can fire it on low HP. Heroes whose budget is spent, fallback- labeled bonus troops, non-hero slots, and already-tracked (X, SlotY) positions are ignored.
type HeroManager ¶
type HeroManager struct {
OnDukeDeployed func(targetEdge string)
// contains filtered or unexported fields
}
HeroManager handles hero-specific deployment logic.
func NewHeroManager ¶
func NewHeroManager( executor *TapExecutor, slotManager *SlotManager, pCfg PrecisionConfig, targetEdge string, w, h int, formula *formula.Formula, troopCounter *TroopCounter, deployLine DeployLine, logger zerolog.Logger, ) *HeroManager
NewHeroManager creates a new hero manager. formula may be nil; when non-nil, per-unit formula entries override the legacy pCfg.Edges / dynamic red-zone deploy coordinates so the user can pin exact side positions via cmd/design_attack. troopCounter may also be nil; when non-nil, DeployTroops uses it to live-OCR the slot's per-card count at deploy time AND after the main tap pass — so balloons/EDs (and any "amount: All" troop) always reach a true empty state before being marked deployed.
func (*HeroManager) DeployHeroes ¶
func (hm *HeroManager) DeployHeroes(heroUnits []strategy.Unit, screen gocv.Mat) []*TrackedSlot
DeployHeroes deploys all heroes and activates abilities. Returns list of deployed hero slots for ability tracking.
func (*HeroManager) DeploySiege ¶
func (hm *HeroManager) DeploySiege(unit strategy.Unit, slot *TrackedSlot) bool
func (*HeroManager) DeployTroops ¶
func (hm *HeroManager) DeployTroops( unit strategy.Unit, slot *TrackedSlot, pattern string, offset int, phasePattern string, screen gocv.Mat, detectedCount int, detectedTrusted bool, ) bool
DeployTroops deploys a group of regular troops (non-hero, non-spell).
Root-cause fix for the "balloons/EDs sometimes don't all get placed" user-reported bug: the previous path fired `count` taps and either returned success (formula-driven) or did a single visual-empty check, then marked the slot SlotDeployed regardless of whether troop icons remained. When the cached detectedCount was wrong (template-OCR is brittle across themes / emulator sizes), troops were left behind without ever being re-counted.
The new path:
- Live-OCR the slot's per-card count BEFORE the main tap pass. Live count wins over detectedCount/YAML when > 0. When OCR fails AND the slot is visually empty, the deploy is a true no-op and we mark deployed without firing taps.
- Main pass fires exactly `count` taps on the formula/pinned or legacy edge line.
- Reconcile loop: up to reconcileRounds rounds, each one captures a fresh screen, live-OCRs + visual-empty checks, and re-selects + fires compensating taps if there's still count > 0. The slot is only MarkDeployed after a (live OCR == 0 AND visual-empty) confirmation — or after the reconcile budget runs out, in which case we record SlotAttempted so the sweep phase retries with its own reconcile loop.
func (*HeroManager) SetActivateAbility ¶
func (hm *HeroManager) SetActivateAbility(v bool)
SetActivateAbility enables the legacy post-deploy auto-activation loop. The dynamic V2 orchestrator leaves it off: abilities run in the dedicated "Abilities" phase, after spells. Only the hero-phase bulk-activation path (attack.go) opts in.
func (*HeroManager) SetPinnedGround ¶
func (hm *HeroManager) SetPinnedGround(v bool)
SetPinnedGround marks the battle's deploy line as the USER'S pin from precision_config.json (written by cmd/pick_coords), which makes it authoritative for this hero manager: the strategy formula no longer supplies the troop ground and the strategy `offset` no longer applies to it.
Both matter. The formula override sent every tap to the formula's line even when the user had pinned a different one, and `offset` (the strategy's "push the line this far toward the middle") moved a pinned line up to 40% of the way to the screen centre — straight into the base's no-deploy area, which is the ground the game refuses with "You cannot deploy troops on the Red area!" (captured live 2026-09-27).
func (*HeroManager) SetRedZone ¶
func (hm *HeroManager) SetRedZone(z RedZone)
SetRedZone wires the battle's detected red no-deploy line so hero drops can avoid ground the game refuses to accept (see hero_ground.go). Called once per battle by the orchestrator; a zero-value zone is legal and means "unchecked".
type ManualEdge ¶
func ClampEdgeToDeployBand ¶
func ClampEdgeToDeployBand(e ManualEdge, uiCutoff int) ManualEdge
ClampEdgeToDeployBand applies ClampToDeployBand to both endpoints of a pin.
func ScaleEdge ¶
func ScaleEdge(e ManualEdge, refW, refH, curW, curH int) ManualEdge
type PrecisionConfig ¶
type PrecisionConfig struct {
Edges map[string]ManualEdge `json:"edges"`
Sides map[string]ManualEdge `json:"sides,omitempty"`
SpellEdgesA map[string]ManualEdge `json:"spell_edges_a"`
SpellEdgesB map[string]ManualEdge `json:"spell_edges_b"`
HeroTargets map[string]image.Point `json:"hero_targets"`
SpellTargets map[string]image.Point `json:"spell_targets"`
BarY int `json:"bar_y"`
Width int `json:"width"`
Height int `json:"height"`
}
type RedLineDetector ¶
type RedLineDetector struct {
// contains filtered or unexported fields
}
RedLineDetector finds the red deployment boundary on screen.
func NewRedLineDetector ¶
func NewRedLineDetector(logger zerolog.Logger) *RedLineDetector
NewRedLineDetector creates detector.
func (*RedLineDetector) Detect ¶
func (r *RedLineDetector) Detect(screen gocv.Mat, uiCutoff int) RedZone
Detect finds the red deployment boundary in the screenshot. Returns bounding box of the red zone (the no-deploy area). Troops must deploy OUTSIDE this box.
func (*RedLineDetector) Mask ¶
Mask returns the closed binary mask Detect searches: the union of the red/orange HSV windows after the same morphology, cropped to uiCutoff. It exists for the review tools (cmd/attack_verify, cmd/see), which have to show a human *what* the detector is calling red. Nothing rendered the mask before, which is how a false "red zone" spanning the playfield survived: the report said "valid=true, bbox 63,0 1217x612" and read like a measurement.
The caller owns the returned Mat and must Close it. Its top-left corner is the frame's top-left, so mask pixels overlay the screenshot at the same coordinates.
type RedZone ¶
type RedZone struct {
BBox image.Rectangle
Valid bool
Contours int
// Polygon is the outline of the red dashed line itself, as a closed loop
// in live-frame coordinates. The BBox is a bounding box and says nothing
// about the shape: the deployable strip in a corner lies well inside the
// bbox while being perfectly legal, so only the outline can answer "is this
// tap inside the no-deploy area". That question is not academic — a live
// 720p run with a user-pinned formula line along the bottom-left corner got
// the game's own "You cannot deploy troops on the Red area!" banner on
// screen for the whole deploy, because the pinned line bypasses the
// red-zone-aware dynamic line entirely.
Polygon []image.Point
}
RedZone represents detected deployment boundary.
type RetryPolicy ¶
RetryPolicy defines retry behavior for a unit deployment.
type RotationState ¶
type RotationState struct {
LastIndex int `json:"last_index"`
}
RotationState is the on-disk schema for the persistent rotation index. Survives process restarts so the bot distributes attacks evenly across the 4 sides over time (not "always start at TopLeft on launch").
type SlotManager ¶
type SlotManager struct {
// contains filtered or unexported fields
}
SlotManager handles slot detection, classification, identity resolution, and state tracking.
func NewSlotManager ¶
func NewSlotManager( screen gocv.Mat, cal *game.Calibration, pCfg PrecisionConfig, w, h, mBarY int, templates map[string]gocv.Mat, classify func(gocv.Mat) (game.GameState, int), logger zerolog.Logger, ) *SlotManager
NewSlotManager detects active slots, resolves identities via template matching + manual labels.
func (*SlotManager) GetAllSlots ¶
func (sm *SlotManager) GetAllSlots() []*TrackedSlot
GetAllSlots returns all tracked slots.
func (*SlotManager) GetBarY ¶
func (sm *SlotManager) GetBarY() int
GetBarY returns the Y coordinate of the troop-bar top (where deck counts are printed above each card). HeroManager / Sweeper / Verifier use this to live-OCR the per-card count above each slot.
func (*SlotManager) GetEventTroops ¶
func (sm *SlotManager) GetEventTroops(strategyUnitNames []string) []*TrackedSlot
GetEventTroops returns slots with unit names not in the given strategy unit list.
"Event" here means any Troop/Spell/CC slot that the strategy did NOT declare. Bonus/seasonal event troops change names every season and may template-match to nothing (empty UnitName), so we deliberately:
- include empty-UnitName slots (an unlabelled troop card is still a card the user wants placed),
- include every Troop/Spell/CC slot whose name is not a strategy unit (covers seasonal names that never appear in the YAML),
- include fallback-labeled slots REGARDLESS of category/name — the label comes from manual_labels.json (an old army) and can be a hero name ("archer queen") that collides with a strategy unit. If template matching couldn't ID the card, it's probably a bonus troop, so don't let a stale hero label hide it.
Slots already Deployed/Failed are skipped so the sweep doesn't re-fire slots the strategy phase already drained. Siege is included because a non-strategy siege slot (e.g. a seasonal bonus troop that got fallback-labeled "siege machine") is still a card the user wants placed; the strategy's OWN siege is excluded by its declared name.
func (*SlotManager) GetSlot ¶
func (sm *SlotManager) GetSlot(unitName string) *TrackedSlot
GetSlot returns the tracked slot for a unit name (case-insensitive).
func (*SlotManager) GetSlotY ¶
func (sm *SlotManager) GetSlotY() int
GetSlotY returns the Y coordinate used for slot detection.
func (*SlotManager) GetUndeployedSlots ¶
func (sm *SlotManager) GetUndeployedSlots() []*TrackedSlot
GetUndeployedSlots returns slots not in Deployed or Failed state.
func (*SlotManager) MarkDeployed ¶
func (sm *SlotManager) MarkDeployed(unitName string)
MarkDeployed marks a slot as successfully deployed.
func (*SlotManager) MarkFailed ¶
func (sm *SlotManager) MarkFailed(unitName string)
MarkFailed marks a slot as failed after exhausting retries.
func (*SlotManager) MarkSlotDeployed ¶
func (sm *SlotManager) MarkSlotDeployed(slot *TrackedSlot)
MarkSlotDeployed marks a specific slot as successfully deployed. Used for fallback-labeled/event slots where the UnitName may collide with a template-matched slot (e.g. a bonus troop stale-labeled "archer queen" shares a name with the real hero), so name-based lookup is unsafe.
func (*SlotManager) MarkSlotFailed ¶
func (sm *SlotManager) MarkSlotFailed(slot *TrackedSlot)
MarkSlotFailed marks a specific slot as failed. Pointer-based variant for fallback-labeled/event slots whose UnitName may collide.
func (*SlotManager) RecordAttempt ¶
func (sm *SlotManager) RecordAttempt(unitName string, success bool)
RecordAttempt records a deployment attempt for a slot.
type SpellDeployer ¶
type SpellDeployer struct {
// contains filtered or unexported fields
}
SpellDeployer handles spell-specific deployment logic.
func NewSpellDeployer ¶
func NewSpellDeployer(executor *TapExecutor, pCfg PrecisionConfig, formula *formula.Formula, w, h int, logger zerolog.Logger) *SpellDeployer
NewSpellDeployer creates a new spell deployer. formula may be nil; when non-nil, a formula unit entry completely replaces the legacy pCfg.SpellEdges{A,B} / pCfg.SpellTargets / isRage-special logic so the user-pinned geometry wins. slotCounts may be nil (legacy behavior: Amount=="All" resolves to the default 5 taps).
func NewSpellDeployerWithCounts ¶
func NewSpellDeployerWithCounts(executor *TapExecutor, pCfg PrecisionConfig, formula *formula.Formula, w, h int, counts map[int]int, logger zerolog.Logger) *SpellDeployer
NewSpellDeployerWithCounts is the count-aware constructor. counts is the map returned by GetAllCounts(troopCounts): slot X -> detected count for that card.
func (*SpellDeployer) DeploySpell ¶
func (sd *SpellDeployer) DeploySpell(unit strategy.Unit, slot *TrackedSlot, targetEdge string, phasePattern string) bool
DeploySpell deploys a spell unit according to its pattern.
Precedence:
- Formula entry (if loaded). Replaces ALL legacy logic including the isRage special-case so the user-pinned geometry wins.
- FourSides legacy fallback.
- Point pattern with a configured spell target.
- Line pattern (or default): Line A for rage, Line B otherwise.
func (*SpellDeployer) SetCounter ¶
func (sd *SpellDeployer) SetCounter(counter *TroopCounter, barY int)
SetCounter enables post-deploy verification. counter is the shared TroopCounter already used by HeroManager/Sweeper; barY is the troop-bar top so DetectCount samples the correct card-number ROI.
func (*SpellDeployer) SetPinnedGround ¶
func (sd *SpellDeployer) SetPinnedGround(v bool)
SetPinnedGround marks this side's spell pins as authoritative, so the strategy formula no longer replaces the pinned spell lines (see HeroManager.SetPinnedGround for the rationale).
func (*SpellDeployer) VerifyAndReconcile ¶
func (sd *SpellDeployer) VerifyAndReconcile(unit strategy.Unit, slot *TrackedSlot, targetEdge, phasePattern string, maxRounds int) (int, bool)
VerifyAndReconcile re-reads the spell's slot state after deployment and re-fires the difference along the same geometry until the slot is empty or maxRounds is exhausted. Mirrors HeroManager's troop reconcile loop. A slot is only "confirmed empty" when BOTH the OCR count reads 0 and the visual empty check passes; a disagreement keeps reconciling.
Returns (extra spells fired, slot confirmed empty). Only runs when a counter was registered via SetCounter; otherwise (0, false).
type StallConfig ¶
type StallConfig struct {
PercentROI image.Rectangle `json:"percent_roi"`
EndButton image.Point `json:"end_button"`
ConfirmBtn image.Point `json:"confirm_btn"`
RefWidth int `json:"ref_width"`
RefHeight int `json:"ref_height"`
// ThBannerZone is an OPTIONAL reference-resolution region to scan for
// the persistent golden "Town Hall destroyed" banner that appears above
// the troop bar once the TH falls. When configured, the battle wait
// latches ThDestroyed the first tick the zone contains at least
// ThBannerGold gold pixels. Leave empty to treat the TH as unknown
// (star counting then relies on destruction percent alone; the zone
// was not yet calibrated from a live TH-destroyed capture).
ThBannerZone image.Rectangle `json:"th_banner_zone,omitempty"`
ThBannerGold int `json:"th_banner_gold"`
}
type Sweeper ¶
type Sweeper struct {
// contains filtered or unexported fields
}
Sweeper handles final sweep to catch undeployed troops.
func NewSweeper ¶
func NewSweeper( executor *TapExecutor, slotManager *SlotManager, pCfg PrecisionConfig, deployLine DeployLine, w, h int, f *formula.Formula, troopCounter *TroopCounter, autoEvent bool, logger zerolog.Logger, ) *Sweeper
NewSweeper creates a new sweeper. formula may be nil; when non-nil, the sweeper consults it FIRST so user-pinned _event_troop / _event_spell coordinates win over the dynamic red-zone fallback. Without this, even with a per-unit formula authored, the sweep phase re-tapped along the old red-zone line, scattering event troops to the wrong side. troopCounter may also be nil; when non-nil, the sweeper uses it to live-OCR the slot's per-card count at retry time AND runs the reconcile loop until the slot is truly empty (live count 0 AND visual-empty). This is the belt-and-braces fix for the "balloons/EDs sometimes don't all get placed" user-reported bug. autoEvent gates the event-troop dump; true (the default) places ALL bonus/seasonal troops that the strategy didn't declare.
func (*Sweeper) SetPinnedGround ¶
SetPinnedGround marks the deploy line as the user's pin, which makes it authoritative for every sweep tap (see HeroManager.SetPinnedGround).
func (*Sweeper) SetRedZone ¶
SetRedZone wires the battle's detected red no-deploy line so a swept hero retry aims at ground the game accepts instead of a stale pinned line's midpoint (see hero_ground.go; live run10 the whole hero phase was refused on that midpoint and the sweep had to re-do it). A zero-value zone is legal and means "unchecked".
func (*Sweeper) Sweep ¶
Sweep deploys any remaining undeployed slots. Uses FRESH screen capture for each slot check (fixes stale screen bug).
Empty-slot guard added between batches inside deploySlot: a slot that emptied mid-batch short-circuits without firing the remaining taps. Defaults are now 1 tap (not 12) when neither troop detection nor the formula gave a count, so we don't over-deploy when the slot only held 5 troops. The previous 12 default silently wasted ~7 taps per slot.
type TapExecutor ¶
type TapExecutor struct {
// contains filtered or unexported fields
}
TapExecutor handles all tap operations, screen capture, and timing.
func NewTapExecutor ¶
func NewTapExecutor(client *adb.Client, cal *game.Calibration, logger zerolog.Logger) *TapExecutor
NewTapExecutor creates a new tap executor.
func (*TapExecutor) CaptureFresh ¶
func (t *TapExecutor) CaptureFresh() (gocv.Mat, error)
CaptureFresh captures a fresh screen from the device.
func (*TapExecutor) CaptureSettledSlotRatio ¶
func (t *TapExecutor) CaptureSettledSlotRatio(screenW int, slot *TrackedSlot) (float64, bool)
CaptureSettledSlotRatio reads slot's activity ratio once its card has stopped animating, so a deploy's cooldown silhouette is measured instead of whatever frame its animation was on. Returns (ratio, capturedOK); capturedOK is false only when the screen could not be captured at all.
func (*TapExecutor) DeployBudgetExhausted ¶
func (t *TapExecutor) DeployBudgetExhausted() bool
DeployBudgetExhausted reports whether the deploy phase may keep firing taps. It is the single abort check every reconcile/sweep/verify loop consults between rounds: once true, remaining units are reported undeployed and the bot moves on to the battle-end wait instead of tapping into (or past) the battle timer.
func (*TapExecutor) DeployBudgetRemaining ¶
func (t *TapExecutor) DeployBudgetRemaining() time.Duration
DeployBudgetRemaining returns the time left in the deploy budget (0 when exhausted or never armed).
func (*TapExecutor) HeroSpotTapBudgetLeft ¶
func (t *TapExecutor) HeroSpotTapBudgetLeft(slot *TrackedSlot) bool
HeroSpotTapBudgetLeft reports whether the hero slot may still take a card tap. Non-hero slots always report true.
func (*TapExecutor) HumanSleep ¶
func (t *TapExecutor) HumanSleep(baseMs, stdDevMs int)
HumanSleep wraps client HumanSleep.
func (*TapExecutor) InputErr ¶
func (t *TapExecutor) InputErr() error
InputErr returns the first definite tap transport failure observed this battle, or nil.
func (*TapExecutor) PlaceSiege ¶
func (t *TapExecutor) PlaceSiege(slot *TrackedSlot, pt image.Point, stdDev float64) bool
PlaceSiege fires the ONE field tap that drops a siege machine, or refuses to fire at all.
This is the only function any deploy path may use to place a siege machine. A siege leaves the bar on its first field tap; a second tap lands on the machine that is already standing there and destroys it, so the count of placement taps per battle must be exactly one no matter how many phases, sweeps and verifier retries look at the slot afterwards. Putting the tap and the bookkeeping in one function is what makes that checkable: every caller gets the same answer, and a path that tries again is refused rather than trusted to remember.
Returns true when the placement tap was fired.
func (*TapExecutor) RearmSlot ¶
func (t *TapExecutor) RearmSlot(slot *TrackedSlot) bool
RearmSlot re-selects a hero's card after a placement the game did not register, so the next field tap is aimed at a SELECTED card. See maxHeroRearms for the measurement that makes this necessary. It deliberately does not consume the placement budget: nothing was placed, so the hero still has one placement and one ability to spend.
func (*TapExecutor) RestoreSelection ¶
func (t *TapExecutor) RestoreSelection() bool
RestoreSelection re-selects the troop/spell card the deploy most recently chose. Used after a hero-ability tap so the ability's card tap cannot leave a hero selected while the next phase aims field taps at troop ground.
func (*TapExecutor) SetSlotResolver ¶
func (t *TapExecutor) SetSlotResolver(fn func(unitName string) *TrackedSlot)
SetSlotResolver wires the unit-name -> slot lookup used by deployers that need to correlate a unit with its slot (e.g. live OCR counts).
func (*TapExecutor) SiegeGround ¶
func (t *TapExecutor) SiegeGround() (image.Point, bool)
SiegeGround returns the tile of the siege machine this battle has placed.
func (*TapExecutor) SiegeNudges ¶
func (t *TapExecutor) SiegeNudges() int
SiegeNudges reports how many placement taps were moved off the machine's tile.
func (*TapExecutor) SiegePlacements ¶
func (t *TapExecutor) SiegePlacements() int
SiegePlacements reports how many siege machines were placed this battle. Any value above 1 is a bug the run must be able to see.
func (*TapExecutor) StartDeployBudget ¶
func (t *TapExecutor) StartDeployBudget()
StartDeployBudget arms the deploy deadline at DeployBudget from now. Call once per battle before any phase runs (DeployDynamicV2).
func (*TapExecutor) StartDeployBudgetAt ¶
func (t *TapExecutor) StartDeployBudgetAt(deadline time.Time)
StartDeployBudgetAt arms the deploy deadline at an explicit instant (used by tests to simulate an already-expired budget).
func (*TapExecutor) TapDeployFourSides ¶
func (t *TapExecutor) TapDeployFourSides(pCfg PrecisionConfig, targetEdge string, countPerSide int, jitterPx int)
TapDeployFourSides performs rapid 4-side spam deployment.
func (*TapExecutor) TapDeployLine ¶
func (t *TapExecutor) TapDeployLine(p1, p2 image.Point, count int, jitterPx int)
TapDeployLine distributes taps along a line from p1 to p2. Direction alternates per call (boustrophedon): down the line, then back up the next call, so consecutive passes never restart at the top.
func (*TapExecutor) TapDeployPoint ¶
func (t *TapExecutor) TapDeployPoint(pt image.Point, count int, jitterPx int)
TapDeployPoint clusters taps around a single point.
func (*TapExecutor) TapFast_ok ¶
func (t *TapExecutor) TapFast_ok(x, y int) bool
func (*TapExecutor) TapField ¶
func (t *TapExecutor) TapField(x, y int, stdDev float64) error
TapField fires one raw field tap through the recording choke point, so deployers that call the client directly (spell casts) feed the same transport verdict as every other path.
func (*TapExecutor) TapHeroAbility ¶
func (t *TapExecutor) TapHeroAbility(slot *TrackedSlot) bool
TapHeroAbility taps a hero slot for ability activation. It shares the same 2-tap card budget as placement, so a hero that already spent both taps (place + ability, or a placement retry) is never tapped a third time. Returns false when the budget is spent and no tap was fired.
func (*TapExecutor) TapSlot ¶
func (t *TapExecutor) TapSlot(slot *TrackedSlot, jitterPx int) bool
TapSlot selects a slot with jitter for human-like behavior. It returns false (and fires nothing) when the slot is a hero card whose 2-tap budget is already spent, so callers can skip their follow-up field tap instead of firing at empty ground.
A siege slot that has already placed its machine is refused for the same reason: re-selecting its card is the first half of a re-deploy, and the field tap that would follow destroys the machine (see TrackedSlot.SiegePlaced).
func (*TapExecutor) TapUnitField ¶
func (t *TapExecutor) TapUnitField(pt image.Point, stdDev float64)
TapUnitField fires ONE unit placement tap on the field, moved clear of any siege machine already standing there.
Every single-tap placement path goes through here (hero drops, hero field-tap retries, sweep hero taps), so the siege tile is protected in one place rather than in each caller. Jitter belongs OUTSIDE this call: the clearance check has to be the last thing between the caller's aim and the wire.
func (*TapExecutor) WaitForSettle ¶
func (t *TapExecutor) WaitForSettle(duration time.Duration)
WaitForSettle waits for deployment to settle.
type TrackedSlot ¶
type TrackedSlot struct {
TroopSlot
State SlotState `json:"state"`
UnitName string `json:"unit_name"`
Confidence float64 `json:"confidence"`
Attempts int `json:"attempts"`
LastTapAt time.Time `json:"last_tap_at"`
IsEmpty bool `json:"is_empty"`
FallbackLabeled bool `json:"fallback_labeled"` // name came from stale manual_labels.json, not a template match
// SpotTaps counts how many times the bot has tapped this slot's CARD
// (not the field). A hero card is capped at maxHeroSpotTaps: one tap
// selects/places the hero and one more activates its ability. Every
// deploy path shares this one counter (main drop, sweep, verifier,
// ability pass) so no combination of retries can ever exceed the cap.
SpotTaps int `json:"spot_taps"`
// SpotRearms counts REPLACEMENT card taps made after a placement the game
// refused (which deselects the card). Bounded by maxHeroRearms; kept apart
// from SpotTaps so a re-arm can never steal the ability's tap.
SpotRearms int `json:"spot_rearms"`
// DeployedAt stamps when the slot reached SlotDeployed (zero when never).
// The battle-end HP watch uses it for the warden's proactive ability
// timing; see internal/attack/hero_hp.go.
DeployedAt time.Time `json:"deployed_at"`
// SiegePlaced records that this battle's siege machine has already taken
// the ONE field tap that places it (see TapExecutor.PlaceSiege).
//
// A siege machine is not a troop card: it leaves the bar on the FIRST tap,
// and every tap after that lands on the machine now standing on the field,
// which destroys it (user-reported). A flag on the slot — rather than a
// convention each call site keeps — is what makes "exactly one placement
// tap" true no matter which path (main phase, sweep, event pass, verifier)
// gets there first: every one of them checks it, and the field-tap choke
// point refuses to fire a second time.
SiegePlaced bool `json:"siege_placed"`
}
TrackedSlot extends TroopSlot with state tracking.
type TroopCount ¶
TroopCount is one slot's read.
OK is the field callers must branch on. It reports that this read is a MEASUREMENT, not that a count was found: a card whose label is absent because the card is spent is read as Count=0, OK=true (the reader looked in the right place, and there is demonstrably nothing there). OK=false means the read is unknown — the reader found no digits anywhere on the bar, so a zero here carries no information and must not be treated as "the card is empty".
type TroopCounter ¶
type TroopCounter struct {
// contains filtered or unexported fields
}
Troop-count reading, and the two facts that make it work at 720p.
Where the number is. CoC draws the per-card count as a small "x8"-style label at the card's TOP edge, i.e. at or just BELOW the slot's identity point (`slotY`), not above the card. Measured on real 1280x720 battle frames with an army loaded, the glyphs of every count label occupy y 683..694 for slotY=671 — that is slotY+12 .. slotY+23, and the band below (countBandTopRef/countBandBotRef) is that measurement with slack on both sides. This code previously sampled [barY-5, slotY-25] — y 614..638 at 720p, roughly 45px ABOVE the label — so it read an empty patch of bar on every slot of every frame and reported count=0 forever. Everything downstream inherited that: the reconciler could never confirm a drain, and both the sweep's "OCR reads zero, so the card is spent" write-off and HeroManager's "visual-empty-but-OCR-zero" bail fired on cards that were still full.
How to read it. The label glyphs are ~11-13px tall at 720p. Comparing a binary glyph crop against a template resized to whatever box the contour produced scored 0.05-0.45 for CORRECT digits and picked `digit_1` for essentially every input, because the correlation was dominated by how much of the crop was background. Instead every candidate glyph and every template is cropped to its own ink and resized to the same canonical box (digitCanonW x digitCanonH); TM_CCOEFF_NORMED over that pair is a shape comparison that is independent of glyph width, of how much padding surrounded it, and of the absolute pixel size. Measured on the same real frames: correct digits score 0.54-0.72, junk 0.12-0.45.
func NewTroopCounter ¶
func NewTroopCounter(cal *game.Calibration, refW, refH int, logger zerolog.Logger) *TroopCounter
NewTroopCounter creates a new troop counter with digit templates.
func (*TroopCounter) BarLegible ¶
func (tc *TroopCounter) BarLegible(screen gocv.Mat, barY, slotY int) bool
BarLegible reports whether the count row of the troop bar is readable at all in this frame.
This is what separates the two meanings of "no digits above this card": a spent card (the label is gone because the card is empty) versus a frame the reader cannot read (dimmed by a dialog, a resize mid-render, a theme whose labels do not match the templates). Callers must only trust a zero count when the reader is demonstrably working on this frame — otherwise they are re-deriving the original bug, where a permanently-blind reader was indistinguishable from a bar full of empty cards.
One readable digit is the bar: it says the reader's thresholds work at this frame's brightness, scale and theme. (A stricter rule — two digits a card apart — was tried and reverted: it breaks the deliberate contract that a bar carrying a single readable label is legible, which is the normal state of a nearly-spent bar. The residual risk it was aimed at, a hero card's LEVEL number being read as a count and legitimising zeros elsewhere, is real in the logs but unproven in the pixels; see the note in docs/SEEING.md.)
func (*TroopCounter) DetectCount ¶
func (tc *TroopCounter) DetectCount(screen gocv.Mat, slot *TrackedSlot, barY int) (int, bool)
DetectCount reads the count above a single slot's card in the provided screen, and reports whether the read is a trustworthy measurement.
`ok=false` means "unknown", not "zero": the caller must not conclude the card is spent. `ok=true, count=0` means the card shows no count label while other cards in the bar still do — i.e. the card is spent or locked.
func (*TroopCounter) DetectCounts ¶
func (tc *TroopCounter) DetectCounts(screen gocv.Mat, slots []*TrackedSlot, barY int) []TroopCount
DetectCounts detects troop counts for all slots on the bar.
This is the one call that sees the whole bar, so it is also where the label row is located: it measures the row from the frame (measureLabelRow) and remembers it, and every later per-slot read — the deploy reconcile, the sweep, the verifier — reuses that row. That matters because the row cannot be derived from the slot point alone: the live run's slot point comes from manual_slots.json, whose `slot_y` is a reference-space value used verbatim, and it sat 11px away from the row the geometry implies. Anchored blindly to it, the search window started below the glyphs, every label was clipped by the window edge, and the bar read as nine unreadable cards plus one lucky hit from the wide fallback band.
func (*TroopCounter) DetectCountsMerged ¶
func (tc *TroopCounter) DetectCountsMerged(frames []gocv.Mat, slots []*TrackedSlot, barY int) []TroopCount
DetectCountsMerged reads the bar across several frames and reports the count a card's frames agree on.
A single capture is the wrong unit for this read. The troop bar animates in at the start of a battle, and a card caught mid-animation can carry no glyph the reader can find at all: on one live 720p deploy frame the reader found count labels on three of ten cards and read one number, while the SAME bar read four of six labels clearly on a saved frame a second later — and the corpus already documents a card "drawn over-exposed" mid-deployment with no glyph on its label row. The count is a stable property of the card until the first drop, so the frame is the noise and the number is the signal.
The merge keeps the reader's promise that it never reports a number a card does not carry:
- positive reads that AGREE are taken;
- positive reads that DISAGREE mean the bar cannot be trusted, so the slot is reported unknown rather than guessed;
- no positive read but a label seen on some frame means the card has a number the reader could not settle: unknown, never a zero;
- no positive read, no label anywhere, and at least one frame where the reader could see the bar means the card is spent: a measured zero.
func (*TroopCounter) HasDigitTemplates ¶
func (tc *TroopCounter) HasDigitTemplates() bool
HasDigitTemplates returns true if digit templates are loaded.
func (*TroopCounter) SlotHasCountLabel ¶
func (tc *TroopCounter) SlotHasCountLabel(screen gocv.Mat, slotX, slotY, barY int) bool
detectSlotCountDetailed is detectSlotCount plus the third outcome the merge needs: whether a count LABEL was seen on this card even though its digits could not be settled. "No label" and "a label I could not read" are both OK=false, and a merge that cannot tell them apart would let a card with an unreadable number be written off as spent on a frame where the number happened to be invisible. SlotHasCountLabel reports whether a count badge is drawn on the card at (slotX, slotY).
It is the "there is a card here holding units" signal: a card with troops always carries its `xN` badge, while an EMPTY position in the troop bar carries nothing at all. That is the difference between a seasonal/bonus card whose count could not be read — which the sweep must still try to place — and empty bar space, which the sweep must not tap ~24 times (live 2026-09-25, run11: the positions at x=1048/x=1148 took blind sweeps from both the troop pass and the event pass).
A hero card has no `xN` badge either (it shows a level), so callers must not use this as a general "is there a card" test: identify by name first.
type UnitPlan ¶
type UnitPlan struct {
Unit strategy.Unit
Slot *TrackedSlot
IsSpell bool
IsHero bool
IsSiege bool
IsAbility bool
Priority int
Retry RetryPolicy
}
UnitPlan represents a resolved deployment plan for a single unit.
func ResolveAbilityTargets ¶
ResolveAbilityTargets returns ability units from a phase plan.
func ResolveHeroTargets ¶
ResolveHeroTargets returns hero units from a phase plan.
func ResolveSiegeTargets ¶
ResolveSiegeTargets returns siege units from a phase plan.
func ResolveSpellTargets ¶
ResolveSpellTargets returns spell units from a phase plan.
func ResolveTroopTargets ¶
ResolveTroopTargets returns non-hero, non-spell, non-ability, non-siege units from a phase plan.
Siege machines (Stone Slammer, Battle Blimp, etc.) are intentionally EXCLUDED here. They have their own DeploySiege path with the precise touch sequence CoC expects, and including them caused the historical "siege deployed twice" bug — once as a regular troop and once as a siege machine — which double-spends the unit and confuses the verifier.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier handles post-deployment verification.
func NewVerifier ¶
func NewVerifier( executor *TapExecutor, slotManager *SlotManager, pCfg PrecisionConfig, targetEdge string, w, h int, config VerifyConfig, troopCounter *TroopCounter, logger zerolog.Logger, ) *Verifier
NewVerifier creates a new verifier. troopCounter may be nil; when non-nil, retryDeploy uses it to live-OCR the slot before re-firing so we never under-spot a slot whose cards still hold troops.
func (*Verifier) SetRedZone ¶
SetRedZone wires the battle's detected red no-deploy line so a verifier hero retry aims at ground the game accepts (see hero_ground.go). A zero-value zone is legal and means "unchecked".
type VerifyConfig ¶
type VerifyConfig struct {
EmptyThreshold float64
AbilityThreshold float64
MaxRetryAttempts int
RetryDelay time.Duration
SettleWait time.Duration
}
VerifyConfig holds verification thresholds.
func DefaultVerifyConfig ¶
func DefaultVerifyConfig() VerifyConfig
DefaultVerifyConfig returns default verification config.
type WatchedHero ¶
type WatchedHero struct {
X int // card centre x (live px)
SlotY int // card centre y (live px)
Slot *TrackedSlot
Warden bool
DeployedAt time.Time
Done bool
// contains filtered or unexported fields
}
WatchedHero is one deployed hero whose ability tap is still unspent.