Documentation
¶
Overview ¶
plan.go computes the deployment plan for a strategy against a single screenshot WITHOUT issuing any taps. The math mirrors what the live Executor / HeroManager / SpellDeployer would do given the same (PrecisionConfig, RedZone, DeployLine, TargetEdge) inputs, so the output reveals what the bot WOULD have done — invaluable for debugging "attacks in the corner on 2 sides" regressions without burning live attacks.
Pure compute, no I/O. The validator command reads a PNG, builds a Planner, calls Plan() once, and renders the result. No ADB, no goroutines, no time.Sleep.
spots.go computes deployment coordinates for the 4 strict sides of the attack (top / right / bottom / left). The math is fully symmetric — the same SpotsForSide call works regardless of which side is being attacked, which IS the "invert" property the caller asked for:
define a placement once on one axis, apply to any side via SpotsForSide.
Sides are stored in precision_config.json under "sides" as up to 4 straight line segments, one per side. Endpoints in JSON are at the calibrated reference (860x732 by default); runtime callers pass live screenW / screenH so the helper scales correctly.
Index ¶
- Constants
- func ClassifySide(p1, p2 image.Point, screenW, screenH int) string
- func GetAllCounts(counts []TroopCount) map[int]int
- func GetCountForSlot(counts []TroopCount, slotX int) int
- func GetDeploymentEdge(unit UnitPlan, targetEdge string, pCfg PrecisionConfig, w, h int) (image.Point, image.Point)
- func GetSlotActivityRatioStatic(screen gocv.Mat, x, y, screenW int) float64
- func GetStrategyUnitNames(s *strategy.DynamicStrategy) []string
- func MirrorForSide(p image.Point, fromSide, toSide string, screenW, screenH int) image.Point
- func NextEdgeIndex() string
- func SideOfPoint(x, y, w, h int) string
- func SidesForCorner(edge string) []string
- func SpotsForSide(pCfg PrecisionConfig, side string, count, screenW, screenH int) []image.Point
- type DeployLine
- type DeployLineCalculator
- type DeployPlanner
- type DiagonalFlag
- 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) 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) ReturnHome() error
- func (e *Executor) SetClassifier(fn func(gocv.Mat) (game.GameState, int))
- func (e *Executor) SweepRemainingSlots(screen gocv.Mat, pCfg PrecisionConfig, targetEdge string, w, h int, mBarY int, ...)
- func (e *Executor) UpdateConfig(cfg *config.AttackConfig)
- func (e *Executor) Validate(s *strategy.DynamicStrategy) error
- func (e *Executor) WaitForBattleEnd(timeout time.Duration) bool
- type HeroDeployment
- type HeroManager
- type ManualEdge
- type PhasePlan
- type PlanMismatch
- type PlanPhaseSummary
- type PlanReport
- type PlanTap
- type Planner
- type PrecisionConfig
- type RedLineDetector
- type RedZone
- type RetryPolicy
- type RotationState
- type SlotManager
- func (sm *SlotManager) GetActiveCount() int
- func (sm *SlotManager) GetAllSlots() []*TrackedSlot
- func (sm *SlotManager) GetBarY() int
- func (sm *SlotManager) GetDeploymentCount() int
- func (sm *SlotManager) GetEventTroops(strategyUnitNames []string) []*TrackedSlot
- func (sm *SlotManager) GetSlot(unitName string) *TrackedSlot
- func (sm *SlotManager) GetSlotByX(x int) *TrackedSlot
- func (sm *SlotManager) GetSlotY() int
- func (sm *SlotManager) GetSlotsByCategory(category string) []*TrackedSlot
- func (sm *SlotManager) GetUndeployedSlots() []*TrackedSlot
- func (sm *SlotManager) IsDeployed(unitName string) bool
- func (sm *SlotManager) MarkDeployed(unitName string)
- func (sm *SlotManager) MarkFailed(unitName string)
- func (sm *SlotManager) RecordAttempt(unitName string, success bool)
- func (sm *SlotManager) RefreshSlotState(screen gocv.Mat, unitName string) bool
- type SlotState
- type SpellDeployer
- type StallConfig
- type Sweeper
- type TapExecutor
- func (t *TapExecutor) CaptureFresh() (gocv.Mat, error)
- func (t *TapExecutor) HumanSleep(baseMs, stdDevMs int)
- func (t *TapExecutor) TapBulkAbilities(slots []*TrackedSlot, delayMs int)
- 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) TapHeroAbility(slot *TrackedSlot)
- func (t *TapExecutor) TapSlot(slot *TrackedSlot, jitterPx int)
- func (t *TapExecutor) TapSlotAt(x, y, jitterPx int)
- func (t *TapExecutor) WaitForSettle(duration time.Duration)
- func (t *TapExecutor) WaitForSlotEmpty(slot *TrackedSlot, timeout time.Duration) bool
- type TrackedSlot
- type TroopCount
- type TroopCounter
- type TroopSlot
- type UnitPlan
- type Verifier
- type VerifyConfig
Constants ¶
const ( SideTop = "top" SideRight = "right" SideBottom = "bottom" SideLeft = "left" )
Side name constants. Mirroring across an axis (top↔bottom, left↔right) is the canonical "invert" transform — see MirrorForSide.
const DefaultSpotsCount = 15
DefaultSpotsCount is the default number of even-spaced tap points to emit per side. Matches linePoints in deploy_line.go so dots line up with the existing red-zone-aware executor's expectations.
Variables ¶
This section is empty.
Functions ¶
func ClassifySide ¶
ClassifySide names a line segment by orientation + position so users (or pick_coords -mode=four) can click sides in any order. Returns one of SideTop / SideRight / SideBottom / SideLeft.
Mostly horizontal → "top" if avgY < midY else "bottom" Mostly vertical → "left" if avgX < midX else "right" Diagonal → by whichever centroid axis is farther from screen center
func GetAllCounts ¶
func GetAllCounts(counts []TroopCount) map[int]int
GetAllCounts returns a map of slot X -> count.
func GetCountForSlot ¶
func GetCountForSlot(counts []TroopCount, slotX int) int
GetCountForSlot returns the detected count for a specific slot X coordinate.
func GetDeploymentEdge ¶
func GetDeploymentEdge(unit UnitPlan, targetEdge string, pCfg PrecisionConfig, w, h int) (image.Point, image.Point)
GetDeploymentEdge returns the edge for a given unit.
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 MirrorForSide ¶
MirrorForSide returns the symmetric tap point of p across the screen center for the requested target side. This is the explicit "invert" helper — define a placement on one axis, receive the equivalent placement on the opposite axis:
top ↔ bottom : (x, y) → (x, screenH - y) left ↔ right : (x, y) → (screenW - x, y) same side : (x, y) → (x, y) (identity short-circuit) cross axis : (x, y) → (x, y) (no-op; 90° rotation out of scope)
Same-side identity short-circuits before any flip so callers passing `MirrorForSide(p, "top", "top", ...)` get p back unchanged. Cross-axis requests (top↔left) deliberately no-op since that would require a 90° rotation, which this helper does not implement.
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 SideOfPoint ¶
SideOfPoint classifies a screen coordinate into ONE of the four compass directions ("top", "right", "bottom", "left") by a strict half-screen rule: the Y axis wins, X is the tiebreaker. For points in the bottom half of the screen, the result is "bottom" regardless of where on the X axis they land. For points exactly on the horizontal midline (rare), the X axis decides.
Why strict half-screen rather than majority-dominance: it matches the user's intuition ("a tap at y=420 is on the BOTTOM half"). A diagonal corner line (92,411)→(300,564) is then ENTIRELY in the bottom half — taps classify as "bottom" — and all match SidesForCorner("BottomLeft"). The diagonality of the LINE itself is surfaced separately via DiagonalCorners in PlanReport below so configuration errors aren't hidden behind per-tap "all-green" runs.
func SidesForCorner ¶
SidesForCorner maps a legacy corner key to its compass-direction envelope so MatchSide becomes a set-membership check. Corners are inherently ambiguous (TopLeft is BOTH top AND left); we surface both and the validator's diff count shows whether a tap landed off the chosen corner entirely.
Strict-side keys (top/right/bottom/left) map to single-element envelopes; anything else maps to an empty envelope (the report flags ALL taps as mismatched — caught at the cfg loader).
func SpotsForSide ¶
func SpotsForSide(pCfg PrecisionConfig, side string, count, screenW, screenH int) []image.Point
SpotsForSide returns `count` evenly-distributed tap points along the side's deployment line. The line endpoints come from pCfg.Sides[side] (a key precision_config.json gained to make the 4 strict sides first-class).
CONTRACT: pCfg.Sides endpoints must already be in LIVE screen coordinates. Both DeployDynamic (legacy JSON-load) and the orchestrator's redZone override pre-scale Sides to live screen dims, so this function returns live-coord tap points directly. screenW / screenH are accepted for API symmetry with potential future callers that need clamping to the screen rect, but the math currently does not use them.
Returns nil on:
- count <= 0
- pCfg.Sides nil or missing key for `side`
Types ¶
type DeployLine ¶
type DeployLine struct {
Points []image.Point // Tap coordinates
Side string // "left", "right", "top", "bottom"
Anchor image.Point // Center of line (for spells)
Outside bool // Whether line is outside red zone
}
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.
func (*DeployLineCalculator) SpellLine ¶
func (d *DeployLineCalculator) SpellLine( anchor image.Point, screenW, uiCutoff int, count int, depthPct float64, ) []image.Point
SpellLine calculates spell deployment points along a line into the base. Spells go from anchor point TOWARD the base center, offset left/right.
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 DiagonalFlag ¶
type DiagonalFlag struct {
Key string `json:"key"`
P1X int `json:"p1_x"`
P1Y int `json:"p1_y"`
P1Side string `json:"p1_side"`
P2X int `json:"p2_x"`
P2Y int `json:"p2_y"`
P2Side string `json:"p2_side"`
AngleDeg int `json:"angle_deg"`
AngleReason string `json:"angle_reason"` // "diagonal", "horizontal", "vertical"
}
DiagonalFlag captures one pCfg.Edges entry whose endpoints sit on different screen halves. surfacing "EndpointA is on screen-side X, EndpointB is on screen-side Y" lets the user see "my pinned line is actually a diagonal — that's why troops scatter" without hand-tracing JSON coords.
type Executor ¶
type Executor struct {
// Debug callbacks — nil when not debugging
OnPhaseStart func(phase string, edge string)
OnUnitDeploy func(unit string, slotX int, slotY int)
// OnDukePick (debug-only). When non-nil, the legacy deployUnit
// Dragon Duke branch fires it after the adjacent-corner random
// pick — chosen is one of {TopLeft, TopRight, BottomLeft,
// BottomRight}. The new path (HeroManager.resolveHeroTarget) does
// NOT fire this — Duke falls through to the chosen edge there.
// Either path is observable via the structured log line
// "Dragon Duke adjacent-edge placement" already emitted by
// deployUnit; this callback exists so a downstream tool can pin
// every Duke pick to disk for after-the-fact corpus analysis.
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) MaximizeLineSpread ¶
func (*Executor) ParseLayout ¶
func (*Executor) ReturnHome ¶
func (*Executor) SetClassifier ¶
func (*Executor) SweepRemainingSlots ¶
func (*Executor) UpdateConfig ¶
func (e *Executor) UpdateConfig(cfg *config.AttackConfig)
type HeroDeployment ¶
type HeroDeployment struct {
Unit strategy.Unit
Slot *TrackedSlot
IsAbility bool
}
HeroDeployment represents a resolved hero deployment.
type HeroManager ¶
type HeroManager struct {
// OnDukeDeployed (debug-only). When non-nil and the unit being
// deployed is the Dragon Duke, fire after resolveHeroTarget. The
// orchestrator wires this to Executor.OnDukePick so legacy + new
// paths funnel through a single observer. chosenEdge is always
// equal to targetEdge in the current HeroManager behavior — Duke
// falls through to the chosen edge with a random point along it.
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, 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
DeploySiege deploys a siege machine.
Formula takes precedence: when the user authored a "point" or "line" entry for this siege (e.g. stone_slammer → {"type":"point","p":{...}}), the user-pinned geometry wins and the legacy pCfg.Edges path is skipped. Falls back to the dynamic red-zone edge line otherwise.
Live-test fix: ON SUCCESS the slot is marked deployed via slot.UnitName (canonical key in unitIndex), NOT via unit.Name. Template matching may identify the slot under a slightly different spelling than the strategy YAML uses, so the strategy-derived `unit.Name` key often fails GetSlot() lookup and MarkDeployed becomes a silent no-op. That left the slot in a non-terminal state and the sweeper picked it up — 3 deploySlot retries × ~12 taps each = ~36 wasted taps per attack.
Live-test fix #2: the legacy path no longer polls isSlotEmptyStatic after the drop. Siege machines in CoC NEVER transition the troop-bar slot back to a clean "empty" state on success — the slot visually persists as the next queued icon (often a CC-troop icon) or a skeleton silhouette until the next production cycle. Trusting the tap and marking deployed directly is the only safe path.
func (*HeroManager) DeployTroops ¶
func (hm *HeroManager) DeployTroops( unit strategy.Unit, slot *TrackedSlot, pattern string, offset int, phasePattern string, screen gocv.Mat, detectedCount int, ) 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.
type ManualEdge ¶
func ScaleEdge ¶
func ScaleEdge(e ManualEdge, refW, refH, curW, curH int) ManualEdge
func ScaleEdgeForPhase ¶
func ScaleEdgeForPhase(edge ManualEdge, pCfg PrecisionConfig, w, h int) ManualEdge
ScaleEdgeForPhase scales an edge to current screen dimensions.
type PlanMismatch ¶
type PlanMismatch struct {
Unit string `json:"unit"`
Phase string `json:"phase"`
TargetEdge string `json:"target_edge"`
TapSide string `json:"tap_side"`
ExpectedSides []string `json:"expected_sides"`
X int `json:"x"`
Y int `json:"y"`
Note string `json:"note"`
}
PlanMismatch is a single off-side tap. The validator highlights these in red on the overlay so the user can see exactly which unit strayed.
type PlanPhaseSummary ¶
type PlanPhaseSummary struct {
Name string `json:"name"`
TargetEdge string `json:"target_edge"`
Pattern string `json:"pattern"`
Taps []PlanTap `json:"taps"`
}
PlanPhaseSummary rolls up the per-unit taps for one YAML phase.
type PlanReport ¶
type PlanReport struct {
Screen struct {
W int `json:"w"`
H int `json:"h"`
} `json:"screen"`
RedZoneValid bool `json:"red_zone_valid"`
RedZoneBBox image.Rectangle `json:"red_zone_bbox"`
DeploySide string `json:"deploy_side"`
DecidedTargetEdge string `json:"decided_target_edge"`
Corners map[string]image.Rectangle `json:"corners_after_override"`
Phases []PlanPhaseSummary `json:"phases"`
Mismatches []PlanMismatch `json:"mismatches"`
// DiagonalCorners lists every pCfg.Edges key whose endpoint pair
// spans MULTIPLE screen sides. This is the actual signal the user
// is hunting — a "pinpointed" line like BottomLeft=(92,411)→(300,564)
// is all-bottom-classified by SideOfPoint, so per-tap classification
// alone misses the bug. DiagonalCorners surfaces the geometry
// directly: P1 is at (92,411) on screen-side "bottom", P2 is at
// (300,564) ALSO on screen-side "bottom". Both match semi-axial
// half-screen classification, but the LINE ANGLE (atan2(208,153)
// ≈ 53°) tells the user "this is a diagonal line, your troops
// will visibly scatter across both left and bottom halves of the
// screen even though per-tap classification says 'all green'".
DiagonalCorners []DiagonalFlag `json:"diagonal_corners"`
}
PlanReport is the top-level validator output. Marshal to JSON for the plan.json artifact.
type PlanTap ¶
type PlanTap struct {
Unit string `json:"unit"`
Phase string `json:"phase"`
TargetEdge string `json:"target_edge"`
X int `json:"x"`
Y int `json:"y"`
Side string `json:"side"`
MatchSide bool `json:"match_side"`
Note string `json:"note"`
}
PlanTap captures ONE planned tap point and its side-classification.
Side is computed by SideOfPoint against the live screen dims so the validator can flag every tap that lands off the target edge's compass direction (e.g. a tap at (430, 700) classified "bottom" while the target is "TopLeft" — a real bug). Note records WHY this tap was planned ("troop-line", "hero-p1", "foursides", "duke-chosen") so surfacing the bug maps back to the deployUnit / FourSides / Hero branch that emitted it.
type Planner ¶
type Planner struct {
PCfg PrecisionConfig
Strategy *strategy.DynamicStrategy
RedZone RedZone
DeployLine DeployLine
TargetEdge string
W, H int
}
Planner holds the immutable inputs needed to plan a deployment.
func NewPlanner ¶
func NewPlanner(pCfg PrecisionConfig, s *strategy.DynamicStrategy, redZone RedZone, line DeployLine, targetEdge string, w, h int) *Planner
NewPlanner assembles the inputs. TargetEdge is the resolved edge ("Random" must already be picked by the caller), and PCfg must be post-orchestrator-override (DeployDynamicV2 writes the red-zone line into all 4 corner keys before deployment, so the validator does the same — see cmd/validate_strategy/main.go).
func (*Planner) Plan ¶
func (p *Planner) Plan() PlanReport
Plan walks every (phase, unit) tuple, computes the planned taps, classifies each by SideOfPoint, and folds them into PlanReport. Taps whose Side is not in SidesForCorner(TargetEdge) are added to Mismatches AND kept in their phase's Taps (so the overlay can render every tap, color-coded).
type PrecisionConfig ¶
type PrecisionConfig struct {
Edges map[string]ManualEdge `json:"edges"`
// Sides holds the 4 STRICT deploy lines (top/right/bottom/left).
// Optional — populated by pick_coords -mode=four and consumed by
// SpotsForSide in internal/attack/spots.go so each side is a fully
// first-class deployment target instead of an awkward corner-pair.
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) GetFreeSpace ¶
func (r *RedLineDetector) GetFreeSpace(zone RedZone, screenW, screenH, uiCutoff int) map[string]int
GetFreeSpace returns free space in pixels on each edge of the red zone.
func (*RedLineDetector) IsInsideRedZone ¶
func (r *RedLineDetector) IsInsideRedZone(zone RedZone, x, y, margin int) bool
IsInsideRedZone checks if a point is inside the red zone with margin.
type RedZone ¶
type RedZone struct {
BBox image.Rectangle // Bounding box of red zone
Valid bool // Whether detection succeeded
Contours int // Number of contours found
}
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, 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) GetActiveCount ¶
func (sm *SlotManager) GetActiveCount() int
GetActiveCount returns number of non-empty slots (detected but not deployed).
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) GetDeploymentCount ¶
func (sm *SlotManager) GetDeploymentCount() int
GetDeploymentCount returns number of deployed slots.
func (*SlotManager) GetEventTroops ¶
func (sm *SlotManager) GetEventTroops(strategyUnitNames []string) []*TrackedSlot
GetEventTroops returns slots with unit names not in the given strategy unit list.
func (*SlotManager) GetSlot ¶
func (sm *SlotManager) GetSlot(unitName string) *TrackedSlot
GetSlot returns the tracked slot for a unit name (case-insensitive).
func (*SlotManager) GetSlotByX ¶
func (sm *SlotManager) GetSlotByX(x int) *TrackedSlot
GetSlotByX returns the tracked slot at a given X coordinate.
func (*SlotManager) GetSlotY ¶
func (sm *SlotManager) GetSlotY() int
GetSlotY returns the Y coordinate used for slot detection.
func (*SlotManager) GetSlotsByCategory ¶
func (sm *SlotManager) GetSlotsByCategory(category string) []*TrackedSlot
GetSlotsByCategory returns slots filtered by category.
func (*SlotManager) GetUndeployedSlots ¶
func (sm *SlotManager) GetUndeployedSlots() []*TrackedSlot
GetUndeployedSlots returns slots not in Deployed or Failed state.
func (*SlotManager) IsDeployed ¶
func (sm *SlotManager) IsDeployed(unitName string) bool
IsDeployed returns true if the slot is confirmed deployed.
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) RecordAttempt ¶
func (sm *SlotManager) RecordAttempt(unitName string, success bool)
RecordAttempt records a deployment attempt for a slot.
func (*SlotManager) RefreshSlotState ¶
func (sm *SlotManager) RefreshSlotState(screen gocv.Mat, unitName string) bool
RefreshSlotState checks if a slot is now empty after deployment attempt.
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.
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.
type StallConfig ¶
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, 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.
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) HumanSleep ¶
func (t *TapExecutor) HumanSleep(baseMs, stdDevMs int)
HumanSleep wraps client HumanSleep.
func (*TapExecutor) TapBulkAbilities ¶
func (t *TapExecutor) TapBulkAbilities(slots []*TrackedSlot, delayMs int)
TapBulkAbilities activates abilities for multiple heroes with delays.
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.
func (*TapExecutor) TapDeployPoint ¶
func (t *TapExecutor) TapDeployPoint(pt image.Point, count int, jitterPx int)
TapDeployPoint clusters taps around a single point.
func (*TapExecutor) TapHeroAbility ¶
func (t *TapExecutor) TapHeroAbility(slot *TrackedSlot)
TapHeroAbility taps a hero slot for ability activation.
func (*TapExecutor) TapSlot ¶
func (t *TapExecutor) TapSlot(slot *TrackedSlot, jitterPx int)
TapSlot selects a slot with jitter for human-like behavior.
func (*TapExecutor) TapSlotAt ¶
func (t *TapExecutor) TapSlotAt(x, y, jitterPx int)
TapSlotAt taps a specific coordinate with jitter.
func (*TapExecutor) WaitForSettle ¶
func (t *TapExecutor) WaitForSettle(duration time.Duration)
WaitForSettle waits for deployment to settle.
func (*TapExecutor) WaitForSlotEmpty ¶
func (t *TapExecutor) WaitForSlotEmpty(slot *TrackedSlot, timeout time.Duration) bool
WaitForSlotEmpty polls until a slot is empty or timeout.
type TrackedSlot ¶
type TrackedSlot struct {
TroopSlot // Embedded: X, Y, Category
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"` // Last known emptiness
}
TrackedSlot extends TroopSlot with state tracking.
type TroopCount ¶
type TroopCount struct {
X int // Slot X coordinate
Count int // Detected count (0 = unknown)
Confidence float64 // Average confidence of digit matches
Digits []int // Individual digits detected
}
TroopCount represents a detected troop count for a slot.
type TroopCounter ¶
type TroopCounter struct {
// contains filtered or unexported fields
}
TroopCounter detects troop count numbers above each card slot. Uses template matching on digit_0..digit_9 templates to read the count.
func NewTroopCounter ¶
func NewTroopCounter(refW, refH int, logger zerolog.Logger) *TroopCounter
NewTroopCounter creates a new troop counter with digit templates.
func (*TroopCounter) Close ¶
func (tc *TroopCounter) Close()
Close releases all OpenCV buffers held by the counter (digit templates and any cached scaled copies). Safe to call multiple times.
func (*TroopCounter) DetectCount ¶
func (tc *TroopCounter) DetectCount(screen gocv.Mat, slot *TrackedSlot, barY int) int
DetectCount returns the live detected count above a single slot's card in the provided screen. Convenience wrapper around the per-slot OCR so HeroManager / Sweeper / Verifier can re-read counts at deploy time instead of trusting the once-cached snapshot.
Returns 0 when OCR fails or the count read is 0 — caller should treat 0 as "unknown" and combine with a visual empty check to decide whether the slot is actually empty.
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. The count number appears above each card in the troop bar.
func (*TroopCounter) HasDigitTemplates ¶
func (tc *TroopCounter) HasDigitTemplates() bool
HasDigitTemplates returns true if digit templates are loaded.
type UnitPlan ¶
type UnitPlan struct {
Unit strategy.Unit
Slot *TrackedSlot
IsSpell bool
IsHero bool
IsSiege bool
IsAbility bool
Priority int // 0=spell, 1=regular, 2=ability
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) CheckSlotEmpty ¶
func (v *Verifier) CheckSlotEmpty(slot *TrackedSlot) bool
CheckSlotEmpty checks if a slot is empty using the verifier's config.
type VerifyConfig ¶
type VerifyConfig struct {
EmptyThreshold float64 // 0.08 - ratio below which slot is empty
AbilityThreshold float64 // 0.4 - ratio below which is ability icon
MaxRetryAttempts int // 3
RetryDelay time.Duration // 500ms
SettleWait time.Duration // 2s
}
VerifyConfig holds verification thresholds.
func DefaultVerifyConfig ¶
func DefaultVerifyConfig() VerifyConfig
DefaultVerifyConfig returns default verification config.