This is an attempt to document and explain some of the geometric concepts, algebraic models, and implementation procedures I used for data-driven entity positioning, multi-threat perception, ballistic targeting, and squad cohesion in my tank game.

To keep things approachable, I’m focusing here on the Geometric Foundations & Tactical Intuition, the algebra and 2D geometry (distances, angles, triangles, and lines of sight) to explain the why, the tactical logic, and my thought processes behind the mechanics without getting bogged down in dense matrix equations.

I am far from being a mathematician but designing autonomous entities in a 2D environment has been a great motivator to learn some practical math and geometry.

Geometric Foundations & Tactical Intuition

1. Sensory Perception: Vision Cones, Circles, and Obstacles

First of all, I wanted to implement a semi-realistic field-of-view (FOV) system for enemy tanks, but quickly realized that a single vision cone was insufficient for a vehicle with an independently rotating turret. Instead, I split spatial awareness between an abstraction of what the hull sees, where the gun is aimed, and what the crew can hear through the metal plating.

A. The Optical Vision Cone (Geometry of Angles & Distances)

To prevent the computer from cheating, tank vision is non-omniscient. Rather than a global radar map, vision is modeled as a 2D wedge (a sector of a circle):

  1. The turret gun sight and aim direction covers a total field of view, or angle spread, of 120120^\circ (6060^\circ to the left and 6060^\circ to the right of where the cannon barrel points).
  2. An enemy tank cannot spot targets across the entire map as optical targeting cuts off at an arbitrary maximum distance (Rmax=1300 pixelsR_{\text{max}} = 1300\text{ pixels}).
  3. Using Euclidean distance between the turret (xturret,yturret)(x_{\text{turret}}, y_{\text{turret}}) and target (xtarget,ytarget)(x_{\text{target}}, y_{\text{target}}):

D=(xtargetxturret)2+(ytargetyturret)2D = \sqrt{(x_{\text{target}} - x_{\text{turret}})^2 + (y_{\text{target}} - y_{\text{turret}})^2}

If D1300 pxD \le 1300\text{ px} and the target’s relative bearing lies within that ±60\pm 60^\circ arc, the optical sight acquires the target.

Note: Yes yes I know square root distance is expensive, but I use Euclidean distance (rl.Vector2Distance) because I require normalization and direction, projectile flight time prediction, linear scoring and clamping for other systems. In my testing, using the cheaper squared distance (rl.Vector2DistanceSqr) saved ~0.3µs or (3/10,000 of a ms) using Odin and Raylib, and this is an acceptable trade-off.

B. Close-Proximity Blindspots & Engine Hearing

Real tanks have severe blindspots directly behind their rear engine decks. If player tank sneaks up behind the turret outside its 120120^\circ cone, the gunner won’t see them.

However, tank engines are loud, tracks rumble across dirt, and commanders have small cupola vision blocks. To simulate this without complex acoustic physics, I abstracted auditory detection with a simple circular area:

  • Hearing Radius (Rprox=250 pxR_{\text{prox}} = 250\text{ px}) so if an enemy vehicle closes within 250 pixels250\text{ pixels} of the chassis center, the crew immediately detects them regardless of where the turret is facing.

Are You Detected=(Within 60 arc and D1300 px)OR(D250 px)\text{Are You Detected} = (\text{Within } 60^\circ \text{ arc and } D \le 1300\text{ px}) \quad \mathbf{OR} \quad (D \le 250\text{ px})

Together, being detected is modeled as “can it see you?” or “can it hear you?”. But we are not quite done yet, because tank should not be able to see through solid obstacles.

C. Line-of-Sight (LOS) Obstacle Checks

Even if a player is sitting inside the optical wedge and within range, the tank cannot see through solid obstacles such as buildings or trees.

Before confirming detection, the game draws a straight line segment connecting the turret center to the target center. If this line intersects the edges of any obstacle polygon on the map, line of sight is broken.

I use linear interpolation to do this where tt, here as the percentage of the distance along the line, is any value within 0.0 and 1.0:

P(t)=start+t(endstart)wheret[0.0,1.0]P(t) = \text{start} + t \cdot (\text{end} - \text{start}) \quad \text{where} \quad t \in [0.0, 1.0]

How I check whether obstacles block this line segment comes down to two geometric tests:

  • With Tree Trunks (Solid Circles), we find the closest point along the segment to the tree trunk’s center by projecting the tree position onto the line and clamping tt to [0.0,1.0][0.0, 1.0]. If the distance from the tree center to that closest point is less than or equal to the trunk’s radius, the trunk blocks line of sight.
  • With Buildings & Other Tank Hulls (Rotated Rectangles), designed as oriented bounding boxes (OBBs), we rotate the line segment’s start and end points into the obstacle’s local, unrotated coordinate frame (instead of dealing with messy angled edge-intersection math in world space). From there, it’s a classic 2D “slab test” where we calculate where the line enters and exits the box’s width and height bounds. If the line passes through the box while tt is between 0.00.0 and 1.01.0, line of sight is broken.
    • Not to get too too complicated but this essentially tests the OBB against the Separating Axis Theorem (SAT) in 2D.
  • Tank Traps are low-profile iron obstacles, they stop tank treads, but they don’t block visual line of sight or gun fire, so the check simply passes right through them.

2. Multi-Threat Encirclement & Escape Arc Detection

Now that computer can detect threats via dual-FOV and close-proximity hearing, I wanted to implement a system that allows algorithm to detect encirclement or crossfire situations and compute an escape route. This prevents computer tank from freezing in place during an ambush and gives it a chance to break out.

A. The Flaw of Adding Directional Arrows

Initially, I implemented a naive threat model: calculate a “threat pull” by adding together arrows pointing (using directional vectors) toward every visible enemy.

For those with more experience than i have, the issue with this would have been apparent. When three, or really any number of, tanks surround computer tank symmetrically (say, spaced evenly at 120120^\circ angles around it), what happens when you add those three equal opposing forces together?

They cancel each other out to zero:

(+1)+(0.5)+(0.5)=0(+1) + (-0.5) + (-0.5) = 0

The DIRECTIONAL vectors (it was in the name all along!!) cancelled out completely! Computer’s math concluded that zero net threat existed, leaving tank unit paralyzed while my tanks peppered it from all sides.

B. The 360° Protractor: Finding the Biggest Gap

Instead of adding arrows together, I mapped the bearing of every threat onto a standard 360360^\circ protractor centered on the tank:

[0°][30°][60°][90°][120°][150°][180°][210°][240°][270°][300°][330°]

Every nearby enemy within 750 px750\text{ px} gets assigned a clock angle (from 00^\circ to 360360^\circ) relative to our tank.

Once we list the threats in order around the circle, we measure the angular gap (the “pizza slice”) between each neighboring pair of enemies. Because a circle always adds up to 360360^\circ, all gaps combined equal 360360^\circ:

Multi-Threat Encirclement & Escape Gap Bisection

For example, if two enemies are at 135135^\circ and 225225^\circ:

  • Gap between Threat 0 and Threat 1: 225135=90225^\circ - 135^\circ = 90^\circ (where the enemy fire is concentrated).
  • Remaining gap around the back: 36090=270360^\circ - 90^\circ = 270^\circ (wide open space).

The Threat Span is simply the total arc covered by incoming fire:

Threat Span=360(Largest Gap)\text{Threat Span} = 360^\circ - (\text{Largest Gap})

C. Detecting Crossfires & Finding the Exit Heading

If the incoming fire spans more than 110110^\circ around the tank, algorithm flags a Crossfire Alert.

To escape, the tank should want to drive straight through the middle of the widest open gap. I divide the angle in half (bisecting the angle) to find the escape heading:

Escape Direction=Start of Gap+Size of Gap2\text{Escape Direction} = \text{Start of Gap} + \frac{\text{Size of Gap}}{2}

In our example with Threat 0 (135135^\circ) and Threat 1 (225225^\circ), the open gap starts at Threat 1 (225225^\circ) and spans 270270^\circ around the back. Bisecting this 270270^\circ arc:

Escape Direction=225+2702=225+135=3600 (Due East)\text{Escape Direction} = 225^\circ + \frac{270^\circ}{2} = 225^\circ + 135^\circ = 360^\circ \equiv 0^\circ \text{ (Due East)}

Notice how this creates equal clearance on both sides: 00^\circ (Due East) is exactly 135135^\circ away from Threat 0 (135135^\circ) and 135135^\circ away from Threat 1 (225225^\circ), directing the tank straight out of the crossfire.

D. Checking Ahead for Walls

Before committing full throttle down that escape heading, algorithm tests whether that path actually leads anywhere. It casts probe points 200 pixels200\text{ pixels} ahead along the escape heading and slightly to the left and right (±25\pm 25^\circ).

  • If the primary gap is blocked by a solid wall, algorithm tests the second-largest gap.
  • It steps through smaller gaps in descending order until it finds an open exit corridor.
  • If every single exit is walled off, the tank activates the Hedgehog Defense (see below).

3. Multi-Threat Cover Search & Scoring

Once an open escape corridor is found, the enemy tank doesn’t just drive blindly in a straight line, it wants to find a spot that puts solid cover between itself and the attackers while traveling the shortest distance possible.

A. Sampling Radar Rings

The computer samples candidate hiding spots at set distances along the escape path, like expanding ripples on water:

  • Distances tested: 120 px120\text{ px}, 200 px200\text{ px}, 280 px280\text{ px}, and 360 px360\text{ px}.
  • At each distance, it tests points directly ahead, slightly to the flanks, and wide to the sides.

To calculate where those candidate spots land in world space, some trigonometry is required.

We take our tank’s position, pick one of the four radii RR (120120, 200200, 280280, or 360 px360\text{ px}), and fan out across seven relative angle offsets:

  • Straight down the escape heading (00^\circ)
  • Gentle flank angles (±22\approx \pm 22^\circ or ±0.38 rad\pm 0.38\text{ rad})
  • Wide flank angles (±43\approx \pm 43^\circ or ±0.75 rad\pm 0.75\text{ rad})
  • Perpendicular beam angles (±90\pm 90^\circ or ±1.57 rad\pm 1.57\text{ rad})

Candidate Hiding Spot Radar Rings

Then we compute the (X,Y)(X, Y) coordinate for each candidate spot:

Spot X=Tank X+R×cos(Escape Heading+Angle Offset)\text{Spot } X = \text{Tank } X + R \times \cos(\text{Escape Heading} + \text{Angle Offset})

Spot Y=Tank Y+R×sin(Escape Heading+Angle Offset)\text{Spot } Y = \text{Tank } Y + R \times \sin(\text{Escape Heading} + \text{Angle Offset})

Testing 4 distance rings×7 angles4\text{ distance rings} \times 7\text{ angles} gives us 2828 candidate hiding spots evaluated in a fraction of a millisecond. If a spot lands inside an obstacle wall, we toss it out; if it’s open, we run it through the cover scoring formula.

B. The Cover Score Formula

For each candidate spot, algorithm runs a simple point system:

  1. +1000 Points for every enemy whose line of sight is broken by a wall from that spot.
  2. -0.5 Points per Pixel of driving distance from the tank’s current position (to penalize driving across the map if nearby cover exists).

Score=(1000×Enemies Blocked)(0.5×Driving Distance)\text{Score} = (1000 \times \text{Enemies Blocked}) - (0.5 \times \text{Driving Distance})

The tank picks the spot with the highest score. If two spots block all enemies, the closer one wins.

C. Dead-End Fallback: The Hedgehog Defense (Back-to-the-Wall)

What happens if the tank is cornered in a cul-de-sac and no position can block enemy line of sight (Enemies Blocked=0\text{Enemies Blocked} = 0)?

Rather than doing donuts or spinning in confusion, algorithm tells the tank NPCs to engage in the Hedgehog Defense:

  1. Algorithm locates the closest obstacle directly behind its hull.
  2. Tank NPC backs its vulnerable engine deck flush against the wall (chosen_gear = .Reverse), physically shielding its rear.
  3. Then tank NPC angles its thicker frontal hull armor at roughly 2424^\circ (the angle of the glacis plate) toward the nearest attacker to maximize deflection chance while returning fire.

4. Ballistic Prediction & Weakpoint Targeting

To make enemy tanks feel dangerous without letting them instantly snipe the player across the map, the firing solution uses a kinematic intercept model.

A. Physics 101: Speed, Time, and Distance

This section was particularly fun to implement because I remember sleeping through this exact chapter in high school physics class, yet here we are! lol At any rate, in game, shells travel at a constant muzzle speed (Vshell=500 pixels/secondV_{\text{shell}} = 500\text{ pixels/second} for the test Panzer IV).

I use basic physics calc:

Distance=Speed×Time    Time=DistanceSpeed\text{Distance} = \text{Speed} \times \text{Time} \implies \text{Time} = \frac{\text{Distance}}{\text{Speed}}

  1. The gunner measures the straight-line Euclidean distance directly from the turret pivot (xturret,yturret)(x_{\text{turret}}, y_{\text{turret}}) to the target’s center (xtarget,ytarget)(x_{\text{target}}, y_{\text{target}}):

    D=(xtargetxturret)2+(ytargetyturret)2D = \sqrt{(x_{\text{target}} - x_{\text{turret}})^2 + (y_{\text{target}} - y_{\text{turret}})^2}

(This is another reason I stick with standard rl.Vector2Distance here. I needed the actual pixel distance to divide by bullet speed and find the flight duration!)

Dividing distance by 500 px/s500\text{ px/s} gives the flight time: for a target 1000 px1000\text{ px} away, the shell takes exactly 2.0 seconds2.0\text{ seconds} to arrive.

  1. Then gunner attempts to predict the intercept point. If the player is driving straight at 150 px/s150\text{ px/s}, where will they be in 2.0 seconds2.0\text{ seconds}?

    Lead Distance=150 px/s×2.0 s=300 pixels ahead\text{Lead Distance} = 150\text{ px/s} \times 2.0\text{ s} = 300\text{ pixels ahead}

Algorithm then aims at that future spot rather than where the player is currently sitting.

B. Difficulty Tuning (Ballistic Lead Scalar)

To support different difficulty levels, we scale the calculated lead distance:

  • Recruit (0%0\% lead): Aims directly at the player’s current location with zero lead. (Easy to dodge by just driving sideways!)
  • Veteran (65%65\% lead): Leads partially, enough to punish overwhelmed commanders but still dodgeable with lateral movement.
  • Ace (100%100\% lead): Calculates the full intercept position, some micro management required to dodge.

C. Weakpoint Targeting (Flank Penetration)

Because I previously implemented a soft z-layer (initially purely for z-layering of visual elements), the computer can target the rear engine compartment of the player’s tank even if the front segment is pointed toward the enemy. I don’t think this is too unfair being the computer tank lacks the ingenuity of a human player.

When attacking from an offset angle, especially when executing a flanking maneuver, the computer shifts its aimpoint from the target’s center of mass toward the rear engine compartment (which carries a 2.25×2.25\times damage multiplier) 😜:

Aim Point=Intercept Spot(Direction Player Is Facing×22% of Tank Length)\text{Aim Point} = \text{Intercept Spot} - (\text{Direction Player Is Facing} \times 22\% \text{ of Tank Length})

5. Sloped Armor & Hull Angling Mechanics

On top of their bias to aim for the player’s weakpoints, computer-driven tanks also angle their hulls to maximize effective armor thickness and deflection probability.

A. Right Triangles & Effective Armor

Why is sloped armor so effective? It comes down to basic right-triangle trigonometry.

Imagine a flat armor plate with thickness T=50 mmT = 50\text{ mm}. If an incoming shell strikes perpendicularly (00^\circ), it punches straight through 50 mm50\text{ mm} of steel.

Sloped Armor Trigonometry & Effective Thickness

Now tilt that plate at an angle β\beta. The shell no longer travels straight through; it must punch along the hypotenuse of the right triangle:

In any right triangle:

cosβ=AdjacentHypotenuse    Hypotenuse=Adjacentcosβ\cos\beta = \frac{\text{Adjacent}}{\text{Hypotenuse}} \implies \text{Hypotenuse} = \frac{\text{Adjacent}}{\cos\beta}

Effective Thickness=Nominal Thicknesscos(Impact Angle)\text{Effective Thickness} = \frac{\text{Nominal Thickness}}{\cos(\text{Impact Angle})}

  • At 00^\circ obliquity: cos(0)=1.0    50 mm\cos(0^\circ) = 1.0 \implies 50\text{ mm} effective.
  • At 4545^\circ obliquity: cos(45)0.707    50/0.70770.7 mm\cos(45^\circ) \approx 0.707 \implies 50 / 0.707 \approx 70.7\text{ mm} (+41%+41\% protection!).
  • At 6060^\circ obliquity: cos(60)=0.50    50/0.50=100 mm\cos(60^\circ) = 0.50 \implies 50 / 0.50 = 100\text{ mm} (double the armor!).

When the impact angle exceeds 6060^\circ, shells glance off the hull, creating either relief or dread depending on which side your own but regardless, dealing virtually zero damage.

B. The 24° Hull Angling Trick

To maximize deflections on its thick frontal glacis plate, algorithm angles its hull 2424^\circ off-center relative to incoming fire. This sweet spot slopes the front armor enough to trigger ricochets while preventing the thin side tracks from being exposed to direct fire.

6. Squad Formations & Battlefield Traffic

After studying Boids or Bird-oid flocking algorithms, I implemented a 3-tank formation system that allows a squad leader to maintain cohesion while maneuvering through the battlefield (I later changed this to allow for sections of even just one tank).

A. Relative Formation Offsets

Squad mates position themselves relative to the commander’s lead tank using simple coordinate offsets (forward/backward and left/right):

  1. Wedge Formation (Triangle):
    • Leader at the apex.
    • Wingman 1: 80 px80\text{ px} behind, 90 px90\text{ px} to the left.
    • Wingman 2: 80 px80\text{ px} behind, 90 px90\text{ px} to the right.
  2. Line Abreast (Assault Row):
    • Tanks drive shoulder-to-shoulder, spaced 100 px100\text{ px} apart to the left and right.
  3. Column Formation (Roads & Narrow Passes):
    • Single-file march: second tank follows ~95 px95\text{ px} behind, third tank follows ~190 px190\text{ px} behind.

During development, this was all relative to the test tank’s dimensions so when movement was necessary, members would not collide with each other.

B. Leashes & Tactical Exceptions

  • If a wingman lags more than 240 pixels240\text{ pixels} away from its slot, the commander throttles back to a crawling speed of 35 px/s35\text{ px/s}, mimicing a leash, until the squad reforms.
  • Tanks actively executing a flank or evading encirclement are given full tactical autonomy, tank ignore the leash entirely and do not slow the commander down.

C. Traffic Rules & Chokepoint Queuing

To prevent tanks from piling into each other or jamming roads:

  1. Tank destinations enforce a strict minimum clearance of 80 pixels80\text{ pixels} (1.25×\approx 1.25\times tank length).
  2. Rather than erratic swerving that throws tanks into walls, tanks follow longitudinal speed controls. If closing within 121\text{--}2 tank lengths of a teammate ahead, the trailing tank gently throttles down to matching crawl speed.
  3. When two tanks converge on a narrow opening in a bocage hedgerow, the tank that is closer gets the right-of-way. The trailing tank waits outside the throat until the opening clears, forming a smooth single-file passage.

7. Hierarchical Two-Section Platoon Command (The Hammer & Anvil)

As fun as 3-tank formations were, things got chaotic once I scaled platoon battles up to 8 tanks. Trying to cram 8 tanks into a single rigid wedge or column created massive traffic jams, and having all 8 enemy tanks focus-fire on a single player tank felt both silly and absurd.

To solve this, I split the platoon into two coordinated tactical elements: Section Alpha (Base-of-Fire / Anvil) and Section Bravo (Maneuver Element / Hammer).

A. Procedural Section Slots (1 to 4 Tanks Each)

Instead of hardcoding a fixed squad size, each section procedurally lays out slots for however many tanks it currently has (11 to 44 tanks):

  • Wedge / Diamond formation has Slot 0 at the apex, Wingmen 1 & 2 stepped 80 px80\text{ px} back and 90 px90\text{ px} to the flanks, and Slot 3 tucking 160 px160\text{ px} behind as a diamond rear guard.
  • Line Abreast formation has tanks spread out shoulder-to-shoulder with 100 px100\text{ px} lateral spacing for assault sweeps.
  • Column formation has tanks rolling in a straight line with 90 px90\text{ px} longitudinal spacing between hulls for road marches and narrow defiles.

B. The Hammer & Anvil: Orbital Crescent Flanking

When unengaged, Section Bravo acts as flank guard trailing Section Alpha in echelon. But once Section Alpha engages the enemy and gets pinned down, Section Bravo switches to an aggressive Flanking Hook.

Instead of all Bravo tanks rushing toward the same single spot behind the player (which just causes bumper-car pileups), Section Bravo fans out along an orbital crescent arc:

  1. Algorithm computes an anchor circle around the player platoon’s center at a standoff distance of 650850 pixels650\text{--}850\text{ pixels}.
  2. A 2D cross product automatically detects which side Section Bravo is already closer to, ensuring they swing around that flank rather than driving across Alpha’s line of fire.
  3. Bravo wingmen fan out along the arc separated by roughly 2222^\circ (0.38 radians\approx 0.38\text{ radians}). Along an 800 px800\text{ px} circle, this creates about 285 pixels285\text{ pixels} of breathing room between tanks (3.5\ge 3.5 tank lengths).

The result is a smooth, wide crossfire crescent that envelops the player without tanks bumping fenders or jamming roads. Although at the map edges, the orbital arc is truncated and the tanks pin themselves against the wall, they still engage player in a very elegant arcing pattern.

C. Combat Pinning & Holding Cover

In real combat, tanks trading fire from behind a stone wall don’t suddenly expose their side armor to drive across an open field just because the commander drove forward.

I added a Combat Pinning rule: if a tank has direct line-of-sight to an enemy, is within effective gun range (<650 px< 650\text{ px}), and its section is engaged, it enters a pinned stance (hold_position = true). It holds its hull-down ground and trades fire rather than abandoning good cover to chase a shifting formation slot.

D. Anti-Overkill (Target Saturation Penalty)

If 8 enemy tanks all acquire the lead player tank, you get vaporized in half a second while the rest of your squad sits completely unpressured.

To create balanced, tactical engagements, target selection includes an anti-saturation penalty:

  • Every time a tank locks onto a target, it reports it to the squad brain.
  • If 33 or more enemy guns are already aimed at the same player tank, subsequent tanks apply a heavy penalty to that target in their scoring formula.
  • Those tanks will deliberately choose secondary targets (wingmen or flanking player tanks) if visible.

This naturally distributes incoming fire across the player’s platoon, forcing you to manage multiple duels across the battlefield instead of surviving a single focused firing squad.