formula

package
v0.6.0-beta Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package formula defines the per-unit deploy coordinate schema produced by the `design_attack` interactive tool and consumed by the deploy pipeline in `internal/attack`.

The schema is intentionally side-based (single endpoints along a chosen SIDE of the screen — top, bottom, left, right) rather than the legacy corner-based system in `precision_config.json`. The user can pinpoint exact tap coordinates for every unit type so the bot no longer has to guess based on red zone detection or random interpolation.

Schema (1 line per field, see examples in cmd/design_attack README):

{
  "name":  "EDrag side formula",
  "screen": {"w": 860, "h": 732},
  "units": {
    "balloon":          {"type":"line", "p1":{"x":60,"y":110}, "p2":{"x":60,"y":542}, "count":10, "jitter":3},
    "stone slammer":    {"type":"point", "p":{"x":400,"y":326}, "jitter":6},
    "rage spell":       {"type":"lines", "lines":[
      {"p1":{"x":80,"y":200},"p2":{"x":130,"y":280},"count":3,"jitter":3},
      {"p1":{"x":80,"y":280},"p2":{"x":130,"y":380},"count":2,"jitter":3}
    ]}
  }
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Formula

type Formula struct {
	Name            string                          `json:"name"`
	Screen          ScreenSize                      `json:"screen"`
	Units           map[string]UnitEntry            `json:"units"`
	CornerOverrides map[string]map[string]UnitEntry `json:"corner_overrides,omitempty"`
}

func AutoPickFor

func AutoPickFor(s *strategy.DynamicStrategy, screenW, screenH int, targetEdge string) *Formula

AutoPickFor builds a *Formula for every unit in the strategy using per-class pinned geometry. No manual clicks required - the side is chosen from targetEdge (Random falls back to Left for determinism).

Per-class rules (4-side aware):

  • Hero (Point pattern, phase name contains "hero") → midpoint of the chosen side's deploy line, jitter 5.
  • Siege (Point pattern, phase name contains "siege") → dead center of map, jitter 6.
  • Line troop (Line pattern, "balloon"/"dragon"/etc) → full edge line endpoints, count=10, jitter 3.
  • Rage spell (Line pattern, name contains "rage") → 2 sub-lines: first 15%-60% (3 taps), second 40%-85% (2 taps).
  • Other spell (Line pattern, name contains "spell") → single edge line pulled slightly inward, count=5, jitter 3.
  • FourSides → point cluster at map center, jitter 5.

The 20-80 percent rule guarantees auto-pick geometry mathematically never lands in a screen corner (the regression that motivated this helper).

func Load

func Load(strategyPath string) (*Formula, bool, error)

Load finds and reads a formula file. Strategy path is the YAML file the bot is loading — we look for `<stem>_formula.json` next to it, also probing `assets/strategies/<basename>` via paths.Resolve.

func LoadFile

func LoadFile(path string) (*Formula, error)

LoadFile reads a formula from a direct path. Use Load() instead when the path is a strategy YAML (Load auto-resolves <stem>_formula.json). Used by cmd/design_attack -verify to load a previously-saved formula for visual inspection of the 4-corner mirror.

func (*Formula) ApplyScreenScale deprecated

func (f *Formula) ApplyScreenScale(srcW, srcH, dstW, dstH int)

ApplyScreenScale multiplies every coordinate in the formula by (dstW/srcW, dstH/srcH) so a JSON calibrated on one screen size still lands correctly on a different live resolution. Does NOT scale Count, Jitter, or Type. No-op when src dims are zero.

Deprecated: this is the legacy "stretch to fill the framebuffer" model, and it is wrong for the game these formulas are authored against. CoC renders at a single uniform display scale about the viewport centre (docs/RESOLUTION.md item 1, verified live for HUD chrome at k=1.325 with 720p icon positions matching mapped predictions within 3 px). Between 860x732 and 1280x720 this function's factors are 1.4884 horizontally and 0.9836 vertically — a 34% anisotropy that moves an authored deploy point by 19-76 px, one to two village tiles, onto the wrong building. Use ProjectUniform with the calibration's display scale for anything that lands on the game world; this entry point survives only for the corner-mirror path in cmd/design_attack, which scales between two buffers of the same device.

func (*Formula) ClampY

func (f *Formula) ClampY(minY, maxY int) int

ClampY pulls every coordinate's Y into [minY, maxY] and reports how many points moved. It exists because a formula authored on the reference geometry can project below the live deployable band even when the projection itself is correct: at 1280x720 the village view is shorter in reference terms, so an authored y of 580 lands at 645 on a 720px screen — under the troop bar at 624. Tapping there hits the HUD and the unit never deploys, which is the "it didn't use all the troops" failure; clamping keeps the unit in play at a slightly different spot instead. The count is returned so the caller can say so out loud, because the durable fix is re-authoring the point.

A degenerate band (maxY <= minY) is ignored rather than collapsing every point onto one row.

func (*Formula) LookUp

func (f *Formula) LookUp(unitName string) (UnitEntry, bool)

LookUp returns the entry for a unit name, normalizing underscores to spaces (Strategy stores "stone_slammer"; formula stores "stone slammer").

func (*Formula) MirrorForCorner

func (f *Formula) MirrorForCorner(targetEdge string)

MirrorForCorner reflects every P / P1 / P2 / Lines[i].P1 / Lines[i].P2 across the screen axes as needed so a formula authored for ONE corner (BottomRight by convention) applies to any of the 4 corners. Used by the orchestrator with `target_edge: "Rotate"` so the same authored attack lands on a different side each run without the user having to pin all 4 corners in `precision_config.json`.

Reflection rules (mirror around the formula's authored screen center):

BottomRight: identity (formula as-authored)        — no-op
BottomLeft:  reflect X (newX = W - X)              — flip horizontally
TopRight:    reflect Y (newY = H - Y)              — flip vertically
TopLeft:     reflect both (newX = W - X; newY = H - Y)

Accepts BOTH the full canonical names ("BottomLeft", "TopRight", etc. — used by the orchestrator via NextEdgeIndex) AND the abbreviated 2-letter forms ("BL", "TR", etc. — used by cmd/design_attack -verify for its 2x2 grid labels). Freeform values like "left" or "right" also work via the substring fallback so a user-authored strategy with `target_edge: "left"` still gets a reasonable mirror.

Designed to be called BEFORE ApplyScreenScale so the mirror uses the formula's authored 860x732 reference frame. After the mirror, ApplyScreenScale does the live-screen projection. The Grand Warden "always at screen center" hardcode in HeroManager is NOT mirrored — center stays center — so a pin authored at the formula's (430, 366) center mirrors to itself, which is the right behavior either way.

func (*Formula) ProjectUniform

func (f *Formula) ProjectUniform(k float64, srcW, srcH, dstW, dstH int)

ProjectUniform projects every coordinate from a formula authored on the src frame onto a live dst frame using a single display scale k about the two frames' centres: dst_centre + (p - src_centre) * k.

This is the model the game actually renders with, which is why it takes k as an argument instead of deriving a ratio from the frame sizes: the display scale is a property of the device (device.display_scale, measured with cmd/resprobe), not of how many pixels the framebuffer happens to have. The same uniform scale applies on both axes, so a shape keeps its aspect, which is exactly what ApplyScreenScale's per-axis factors break.

Coordinates are NOT clamped: a point that projects off-screen is a deploy point the bot cannot tap, and the caller is expected to report that rather than silently pin it to the frame edge.

No-op when the formula is already in live coordinates (src == dst) or when any dimension or k is non-positive. The src == dst guard matters: formulas written by cmd/design_attack carry the live screen size, and re-scaling them by the device's k would multiply them for a second time.

func (*Formula) PushOutOfZone

func (f *Formula) PushOutOfZone(blocked func(x, y int) bool, cx, cy, step, maxSteps int) int

PushOutOfZone walks every coordinate radially away from (cx, cy) while `blocked` reports it sits in an area that cannot take a tap, and returns how many coordinates moved.

It exists for the red deployment line. The band clamp above answers "is this point under the HUD", which is a frame-edge question; the red line is a shape in the middle of the frame — the game refuses a tap inside it with "You cannot deploy troops on the Red area!" and the unit stays in the bar. A user-pinned formula bypasses the red-zone-aware dynamic deploy line entirely, so a pinned line drawn inside the red boundary deploys nothing, and nothing in the log says why: the taps succeed, the game ignores them.

Direction is radial from the zone's centre because that is the direction of the deployable strip on every edge, and stepping by whole tiles keeps the moved point on the geometry the user was aiming at rather than nudging it a few pixels inside the same rejected area. A point that cannot escape within maxSteps is left alone and counted as unmoved — silently relocating it across the village would be a worse lie than the refused tap.

func (*Formula) Save

func (f *Formula) Save(path string) error

Save writes the formula as pretty JSON.

type LinePoint

type LinePoint struct {
	P1     Point `json:"p1"`
	P2     Point `json:"p2"`
	Count  int   `json:"count"`
	Jitter int   `json:"jitter"`
}

LinePoint is one segment of a multi-line deploy (e.g. rage spell 3+2 split). Count = number of taps distributed along P1->P2 for this segment.

type Point

type Point struct {
	X int `json:"x"`
	Y int `json:"y"`
}

Point is a tap-able 2D screen coordinate.

func (Point) Image

func (p Point) Image() image.Point

Image converts to image.Point for the deploy path.

type ScreenSize

type ScreenSize struct {
	W int `json:"w"`
	H int `json:"h"`
}

Formula is the top-level deploy plan.

`Units` is the per-unit deploy geometry authored for one canonical corner (BottomRight by convention). The orchestrator mirrors Units to the active target edge at runtime (see MirrorForCorner).

`CornerOverrides` is an optional map of per-corner partial overrides. When present, the orchestrator uses the override for that corner INSTEAD OF mirroring Units — because the user has authored explicit coordinates for that side (e.g. with `cmd/design_attack -corner BL`) that better match the base's actual red-line position on that side.

Keys are the canonical corner names produced by NextEdgeIndex: "TopLeft" / "TopRight" / "BottomRight" / "BottomLeft" (case sensitive — the orchestrator's switch uses the raw value). The per-corner value is a per-unit map with the SAME schema as Units; typically a PARTIAL formula (only the units that differ from the mirrored BR default). The orchestrator merges the override with Units per-unit, with the override winning.

`corner_overrides` is omitted from JSON when nil, so old formulas (with only Units) continue to round-trip cleanly. ScreenSize is the reference frame the formula was authored on (typically 860x732). MirrorForCorner reflects around its center and ApplyScreenScale projects onto the live screen size.

type UnitEntry

type UnitEntry struct {
	Type string `json:"type,omitempty"`

	// point
	P      *Point `json:"p,omitempty"`
	Jitter int    `json:"jitter,omitempty"`

	// line
	P1    *Point `json:"p1,omitempty"`
	P2    *Point `json:"p2,omitempty"`
	Count int    `json:"count,omitempty"`

	// lines (rage-style)
	Lines []LinePoint `json:"lines,omitempty"`
}

UnitEntry is the per-unit deploy instruction.

Type discriminator (inferred from populated fields):

"point" - single tap target (heroes, siege)
"line"  - taps evenly distributed from P1 to P2 (balloons, EDrag)
"lines" - rage-style split (each LinePoint is one sub-line)
empty   - no entry; caller falls back to pCfg.Edges

func (UnitEntry) IsLine

func (e UnitEntry) IsLine() bool

IsLine returns true if the entry is "line".

func (UnitEntry) IsLines

func (e UnitEntry) IsLines() bool

IsLines returns true if the entry is "lines".

func (UnitEntry) IsPoint

func (e UnitEntry) IsPoint() bool

IsPoint returns true if the entry is "point".

Jump to

Keyboard shortcuts

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