Documentation
¶
Index ¶
- Constants
- func GetAllCounts(counts []TroopCount) map[int]int
- func GetCountForSlot(counts []TroopCount, slotX int) int
- func GetSlotActivityRatioStatic(screen gocv.Mat, x, y, screenW int) float64
- func GetStrategyUnitNames(s *strategy.DynamicStrategy) []string
- func NextEdgeIndex() string
- 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) 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 HeroManager
- 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) 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) DeployBudgetExhausted() bool
- func (t *TapExecutor) DeployBudgetRemaining() time.Duration
- func (t *TapExecutor) HumanSleep(baseMs, stdDevMs int)
- func (t *TapExecutor) SetSlotResolver(fn func(unitName string) *TrackedSlot)
- 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) TapHeroAbility(slot *TrackedSlot)
- func (t *TapExecutor) TapSlot(slot *TrackedSlot, jitterPx int)
- func (t *TapExecutor) WaitForSettle(duration time.Duration)
- type TrackedSlot
- type TroopCount
- type TroopCounter
- type TroopSlot
- type UnitPlan
- type Verifier
- type VerifyConfig
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.
Variables ¶
This section is empty.
Functions ¶
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 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 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.
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) 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 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, 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
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.
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) 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) 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) 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) 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) HumanSleep ¶
func (t *TapExecutor) HumanSleep(baseMs, stdDevMs int)
HumanSleep wraps client HumanSleep.
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) 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) 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) 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
}
TrackedSlot extends TroopSlot with state tracking.
type TroopCount ¶
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) 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
Retry RetryPolicy
}
UnitPlan represents a resolved deployment plan for a single unit.
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.
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.