attack

package
v0.2.0-beta Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 22 Imported by: 0

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

View Source
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.

View Source
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

func ClassifySide(p1, p2 image.Point, screenW, screenH int) string

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

func GetSlotActivityRatioStatic(screen gocv.Mat, x, y, screenW int) float64

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

func MirrorForSide(p image.Point, fromSide, toSide string, screenW, screenH int) image.Point

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

func SideOfPoint(x, y, w, h int) string

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

func SidesForCorner(edge string) []string

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 (e *Executor) CalculateInBetween(edge string, offset int, bT, bB, bL, bR, fT, fB, fL, fR image.Point) (p1, p2 image.Point)

func (*Executor) DeployDynamic

func (e *Executor) DeployDynamic(s *strategy.DynamicStrategy, screen gocv.Mat) (int, error)

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) EndBattle

func (e *Executor) EndBattle() error

func (*Executor) GetSlotActivityRatio

func (e *Executor) GetSlotActivityRatio(screen gocv.Mat, x, y int) float64

GetSlotActivityRatio is the exported wrapper for debug scripts.

func (*Executor) GetSlotY

func (e *Executor) GetSlotY(h, mBarY int) int

GetSlotY returns the Y coordinate used for slot detection.

func (*Executor) GetTemplates

func (e *Executor) GetTemplates() map[string]gocv.Mat

func (*Executor) IsSlotEmpty

func (e *Executor) IsSlotEmpty(screen gocv.Mat, x, y int) bool

func (*Executor) MaximizeLineSpread

func (e *Executor) MaximizeLineSpread(p1, p2 image.Point, w, mBarY int) (image.Point, image.Point)

func (*Executor) ParseLayout

func (e *Executor) ParseLayout(screen gocv.Mat, pCfg PrecisionConfig, w, h, mBarY int) []TroopSlot

func (*Executor) ReturnHome

func (e *Executor) ReturnHome() error

func (*Executor) SetClassifier

func (e *Executor) SetClassifier(fn func(gocv.Mat) (game.GameState, int))

func (*Executor) SweepRemainingSlots

func (e *Executor) SweepRemainingSlots(screen gocv.Mat, pCfg PrecisionConfig, targetEdge string, w, h int, mBarY int, usedSlots map[int]bool, siegeXs []int, allSlots []TroopSlot, slotY int)

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

func (e *Executor) WaitForBattleEnd(timeout time.Duration) bool

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:

  1. 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.
  2. Main pass fires exactly `count` taps on the formula/pinned or legacy edge line.
  3. 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

type ManualEdge struct {
	P1 image.Point `json:"p1"`
	P2 image.Point `json:"p2"`
}

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 PhasePlan

type PhasePlan struct {
	Phase     strategy.Phase
	UnitPlans []UnitPlan
	Edge      string
}

PhasePlan represents a resolved deployment plan for a phase.

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

type RetryPolicy struct {
	MaxRetries  int
	RetryDelay  time.Duration
	VerifyAfter bool
}

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 SlotState

type SlotState int

SlotState tracks lifecycle of each detected slot.

const (
	SlotDetected   SlotState = iota // Found on bar
	SlotIdentified                  // Unit name resolved
	SlotAttempted                   // Tap attempted
	SlotDeployed                    // Confirmed empty
	SlotFailed                      // Retries exhausted
)

func (SlotState) String

func (s SlotState) String() string

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:

  1. Formula entry (if loaded). Replaces ALL legacy logic including the isRage special-case so the user-pinned geometry wins.
  2. FourSides legacy fallback.
  3. Point pattern with a configured spell target.
  4. Line pattern (or default): Line A for rage, Line B otherwise.

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"`
}

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

func (sw *Sweeper) Sweep(strategyUnitNames []string, troopCounts map[int]int) int

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 TroopSlot

type TroopSlot struct {
	X        int
	Y        int
	Category string // "Troop", "Siege", "Hero", "Spell", "CC"
}

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

func ResolveAbilityTargets(plan PhasePlan) []UnitPlan

ResolveAbilityTargets returns ability units from a phase plan.

func ResolveHeroTargets

func ResolveHeroTargets(plan PhasePlan) []UnitPlan

ResolveHeroTargets returns hero units from a phase plan.

func ResolveSiegeTargets

func ResolveSiegeTargets(plan PhasePlan) []UnitPlan

ResolveSiegeTargets returns siege units from a phase plan.

func ResolveSpellTargets

func ResolveSpellTargets(plan PhasePlan) []UnitPlan

ResolveSpellTargets returns spell units from a phase plan.

func ResolveTroopTargets

func ResolveTroopTargets(plan PhasePlan) []UnitPlan

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.

func (*Verifier) VerifyAll

func (v *Verifier) VerifyAll() int

VerifyAll runs comprehensive post-attack verification. Returns number of remaining undeployed slots.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL