Files
Backyard-CTF/docs/plans/2026-02-04-feat-asymmetric-grid-ctf-board-plan.md
John Lamb e4ac24f989 feat(grid): Replace real-time CTF with turn-based grid system
Replace continuous free-form movement with discrete 20x50 grid-based
gameplay featuring asymmetric movement mechanics:

- Blue/Red teams with 3 units each
- Zone-based movement: orthogonal (4 dir) in offense, diagonal (8 dir) in defense
- Alternating turns with click-to-select, click-to-move input
- Fog of war: 3-cell Chebyshev vision radius per unit
- Defense speed nerf: skip every 4th move in own zone
- AI opponent that chases flag carriers and advances toward enemy flag
- Collision resolution: defender wins in their zone, lower ID wins in neutral

Implements all 3 phases from the plan:
- Phase 1: Playable grid with hot-seat two-player
- Phase 2: Fog of war + defense speed nerf
- Phase 3: AI opponent

Deleted obsolete files: Flag.cs, Unit.cs, RouteDrawer.cs, SimpleAI.cs, Visibility.cs

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-07 17:05:44 -06:00

14 KiB

title, type, date, brainstorm
title type date brainstorm
feat: Asymmetric Grid CTF Board feat 2026-02-04 docs/brainstorms/2026-02-04-asymmetric-grid-ctf-brainstorm.md

feat: Asymmetric Grid CTF Board

Overview

Replace the current real-time continuous movement CTF game with a turn-based grid system featuring asymmetric movement mechanics. Each team's 3 pieces move differently based on which zone they occupy: orthogonal (Manhattan) in offensive zones, diagonal (Chebyshev) in defensive zones.

Key changes from current implementation:

  • Continuous free-form movement → Discrete grid-based movement
  • Real-time gameplay → Alternating turn-based
  • Uniform movement → Zone-dependent movement rules

Problem Statement

The current implementation is a real-time CTF game where:

  • Units move continuously along drawn paths
  • All units have the same movement capabilities
  • The defense has no inherent advantage

The game design analysis (see docs/CAPTURE THE FLAG.pdf) shows that asymmetric movement creates interesting strategic depth:

  • Defense moving diagonally covers more ground (Chebyshev distance)
  • Offense moving orthogonally is predictable (Manhattan distance)
  • With 75% defense speed nerf, this becomes a balanced mixed-strategy game

Proposed Solution

Build a 20x50 grid-based board with three zones and alternating turn-based gameplay.

Board Layout

┌────────────────────────────────────────────────────────┐
│                    TEAM B DEFENSE                       │  Y: 30-49
│                  (Team A Offense)                       │  (20 rows)
│                 [Diagonal Movement]                     │
│                     🚩 Flag B                           │
├────────────────────────────────────────────────────────┤
│                    NEUTRAL ZONE                         │  Y: 20-29
│               (Both teams on offense)                   │  (10 rows)
│               [Orthogonal Movement]                     │
├────────────────────────────────────────────────────────┤
│                    TEAM A DEFENSE                       │  Y: 0-19
│                  (Team B Offense)                       │  (20 rows)
│                 [Diagonal Movement]                     │
│                     🚩 Flag A                           │
└────────────────────────────────────────────────────────┘
        X: 0 ←────────── 20 cells ──────────→ 19

Movement Rules

Zone (for piece) Movement Type Speed
Own Defense Diagonal (8 directions) 75% (skip every 4th turn)
Enemy Defense (Offense) Orthogonal (4 directions: N/S/E/W) 100%
Neutral Orthogonal (4 directions) 100%

Turn Flow (Alternating)

┌─────────────────┐
│ PLAYER TURN     │ Click unit → click destination → unit moves
└────────┬────────┘
         ▼
┌─────────────────┐
│ RESOLVE         │ Check collision, flag pickup, scoring
└────────┬────────┘
         ▼
┌─────────────────┐
│ AI TURN         │ AI selects and executes move
└────────┬────────┘
         ▼
┌─────────────────┐
│ RESOLVE         │ Check collision, flag pickup, scoring
└────────┬────────┘
         ▼
      Next turn

Note: Start with alternating turns for simplicity. Simultaneous turns can be added later if alternating feels too simple.

Technical Approach

Architecture (3 Classes)

Assets/Scripts/
├── Grid/
│   ├── GridBoard.cs    # Board state, zones, rendering, input, turn logic
│   ├── GridUnit.cs     # Unit data struct
│   └── GridAI.cs       # AI decision-making (Phase 3)
├── Game.cs             # Bootstrap, update for grid
├── Flag.cs             # Adapt for grid coordinates
└── CameraController.cs # Keep as-is

Zone Boundaries (Constants)

public static class ZoneBoundaries
{
    public const int TeamADefenseEnd = 20;  // Y < 20 is Team A defense
    public const int NeutralEnd = 30;        // Y < 30 is neutral (if >= TeamADefenseEnd)
    public const int BoardWidth = 20;
    public const int BoardHeight = 50;
}

Data Structures

public enum Team { Blue, Red }  // Canonical names used everywhere

public enum ZoneOwner { Blue, Neutral, Red }

// GridUnit.cs - minimal data struct
public class GridUnit
{
    public int UnitId;                    // Stable identifier for determinism
    public Team Team;
    public Vector2Int GridPosition;
    public int ConsecutiveDefenseMoves;   // Reset when leaving defense zone
    public bool IsTaggedOut;
    public int RespawnTurnsRemaining;     // 0 when active
    // Note: HasFlag NOT stored here - query Flag.CarriedBy instead
}

// GridBoard.cs - all game logic in one place
public class GridBoard : MonoBehaviour
{
    // Board is a 2D array of unit references (null = empty)
    GridUnit[,] cellOccupants = new GridUnit[BoardWidth, BoardHeight];

    // Canonical position storage - single source of truth
    Dictionary<GridUnit, Vector2Int> unitPositions = new();

    public ZoneOwner GetZoneOwner(int y) => y switch
    {
        < ZoneBoundaries.TeamADefenseEnd => ZoneOwner.Blue,
        < ZoneBoundaries.NeutralEnd => ZoneOwner.Neutral,
        _ => ZoneOwner.Red
    };

    public bool IsDefending(GridUnit unit) =>
        (unit.Team == Team.Blue && GetZoneOwner(unit.GridPosition.y) == ZoneOwner.Blue) ||
        (unit.Team == Team.Red && GetZoneOwner(unit.GridPosition.y) == ZoneOwner.Red);

    // Atomic move that keeps both data structures in sync
    public void MoveUnit(GridUnit unit, Vector2Int to)
    {
        var from = unitPositions[unit];
        cellOccupants[from.x, from.y] = null;
        cellOccupants[to.x, to.y] = unit;
        unitPositions[unit] = to;
        unit.GridPosition = to;
    }
}

Collision Resolution (Deterministic)

Collisions are resolved deterministically by unit ID (lower ID wins ties):

void ResolveCollision(GridUnit unitA, GridUnit unitB, Vector2Int cell)
{
    // Same team = no collision (can share cell temporarily)
    if (unitA.Team == unitB.Team) return;

    var zoneOwner = GetZoneOwner(cell.y);

    // Defender in their zone always wins
    if (unitA.Team == Team.Blue && zoneOwner == ZoneOwner.Blue)
        TagOut(unitB);
    else if (unitB.Team == Team.Blue && zoneOwner == ZoneOwner.Blue)
        TagOut(unitA);
    else if (unitA.Team == Team.Red && zoneOwner == ZoneOwner.Red)
        TagOut(unitB);
    else if (unitB.Team == Team.Red && zoneOwner == ZoneOwner.Red)
        TagOut(unitA);
    else
    {
        // Neutral zone: lower UnitId wins (deterministic)
        if (unitA.UnitId < unitB.UnitId)
            TagOut(unitB);
        else
            TagOut(unitA);
    }
}

Turn Execution Order

Each turn resolves in this exact order:

  1. Speed nerf check - If unit is defending and ConsecutiveDefenseMoves % 4 == 3, skip move
  2. Execute move - Update position
  3. Collision check - Tag losing unit if two enemies on same cell
  4. Flag pickup - Unit on enemy flag cell picks it up
  5. Flag drop - Tagged unit drops flag at current position
  6. Score check - Flag carrier in own base scores
  7. Respawn tick - Decrement RespawnTurnsRemaining, respawn if 0
  8. Update visibility - Toggle enemy sprite visibility (Phase 2)

Implementation Phases

Phase 1: Playable Grid

Goal: Two humans can play hot-seat CTF on a grid.

Tasks:

  • Create GridBoard.cs:
    • Render 20x50 grid using existing CreateSprite() helper
    • Color zones (blue tint for Blue defense, gray for neutral, red tint for Red defense)
    • Handle mouse input (click unit, click destination)
    • Implement GetValidMoves() with zone-aware movement
    • Implement MoveUnit() with atomic position update
    • Implement collision resolution
  • Create GridUnit.cs as minimal data struct
  • Adapt Flag.cs for grid coordinates:
    • Flag pickup on entering cell
    • Flag drop on tag (stays at cell)
    • Flag return when friendly touches it
    • Score when carrier reaches own base
  • Modify Game.cs to instantiate GridBoard instead of current map
  • Alternating turns: bool isPlayerTurn, swap after each move
  • Win at 3 points, use existing gameOverText

Files to create:

  • Assets/Scripts/Grid/GridBoard.cs
  • Assets/Scripts/Grid/GridUnit.cs

Files to modify:

  • Assets/Scripts/Game.cs
  • Assets/Scripts/Flag.cs

Verification:

Run game → See colored grid with 6 pieces and 2 flags
Click unit → Valid moves highlight (4 or 8 based on zone)
Click valid cell → Unit moves, turn swaps
Move to enemy flag → Pick up flag
Return to base with flag → Score point
Move onto enemy in your zone → Enemy respawns
First to 3 → "You Win" / "You Lose"

Phase 2: Asymmetric Mechanics

Goal: Add the mechanics that create strategic depth.

Tasks:

  • Add fog of war:
    • Each unit sees 3 cells (Chebyshev distance)
    • Toggle enemy sprite enabled based on visibility
    • Recalculate after each move
  • Add defense speed nerf:
    • Track ConsecutiveDefenseMoves on GridUnit
    • Skip every 4th move when defending
    • Reset counter when leaving defense zone
    • Visual indicator (dim sprite) when next move will be skipped

Fog visibility (method in GridBoard):

HashSet<Vector2Int> visibleToBlue = new();

void RecalculateVisibility()
{
    visibleToBlue.Clear();
    foreach (var unit in blueUnits)
    {
        if (unit.IsTaggedOut) continue;
        for (int dx = -3; dx <= 3; dx++)
            for (int dy = -3; dy <= 3; dy++)
            {
                var cell = unit.GridPosition + new Vector2Int(dx, dy);
                if (IsInBounds(cell)) visibleToBlue.Add(cell);
            }
    }

    foreach (var enemy in redUnits)
        enemy.Sprite.enabled = visibleToBlue.Contains(enemy.GridPosition);
}

Verification:

Start game → Red units partially hidden
Move Blue unit → Fog reveals new cells
Defender moves 3 times → 4th move skipped (unit dims beforehand)
Defender leaves zone → Counter resets

Phase 3: AI Opponent

Goal: Single-player mode.

Tasks:

  • Create GridAI.cs:
    • AI respects fog (only sees what its units see)
    • Simple strategy: chase visible flag carrier, else advance toward flag
    • AI takes turn after player
  • Delete Assets/Scripts/SimpleAI.cs

AI decision (single difficulty):

Vector2Int? DecideMove(GridUnit unit)
{
    var validMoves = board.GetValidMoves(unit);
    if (validMoves.Count == 0) return null;

    // Priority 1: Chase visible flag carrier
    var carrier = GetVisibleEnemyFlagCarrier();
    if (carrier != null)
        return validMoves.OrderBy(m => Distance(m, carrier.GridPosition)).First();

    // Priority 2: Advance toward enemy flag
    var flagPos = enemyFlag.GridPosition;
    return validMoves.OrderBy(m => Distance(m, flagPos)).First();
}

Files to create:

  • Assets/Scripts/Grid/GridAI.cs

Files to delete:

  • Assets/Scripts/SimpleAI.cs

Verification:

Start game → AI takes turns after player
AI chases if it sees flag carrier
AI advances toward flag otherwise
AI doesn't react to units outside fog
Full game completes without errors

Deferred (Not MVP)

These features are explicitly deferred until the core loop is validated:

  • Simultaneous turns (adds: planned moves storage, submit button, ghost indicators, conflict resolution)
  • Turn timer
  • Sound effects
  • Camera auto-zoom
  • Zone labels
  • Win/lose screen (use Debug.Log or existing gameOverText)
  • Multiple AI difficulties
  • Respawn delay (use instant respawn)
  • Flag auto-return timer (flags stay where dropped)

Edge Cases to Handle

Edge Case Resolution
Three+ units on same cell Process collisions pairwise by UnitId order
Zone crossing during move Speed nerf based on starting position
Flag dropped in neutral Can be "returned" by either team touching it
Both carriers tagged same turn Both flags drop (alternating turns makes this impossible)
Tagged while about to score Tag resolves before score check
Skip turn with flag Carrier stays in place, keeps flag
Respawn location occupied Respawn at nearest empty cell in base

Acceptance Criteria

Functional (Phase 1 - MVP)

  • 20x50 grid renders with three colored zones
  • 6 pieces (3 per team) at starting positions
  • Orthogonal movement in offense zones, diagonal in defense
  • Alternating turns
  • Collision: defender wins in their zone
  • Flags: pickup, drop on tag, return on touch, score on base
  • First to 3 wins

Functional (Phase 2)

  • 3-cell vision radius per unit
  • Enemy units hidden outside fog
  • Defenders skip every 4th move

Functional (Phase 3)

  • AI plays Red team
  • AI respects fog
  • Full game completes

Success Metrics

  1. Playable: Full game loop from start to victory works
  2. Fun check: Does asymmetric movement feel strategically interesting?

Balance testing and engagement metrics deferred until core loop is validated.

References

  • Brainstorm: docs/brainstorms/2026-02-04-asymmetric-grid-ctf-brainstorm.md
  • Game design PDF: docs/CAPTURE THE FLAG.pdf
  • Current movement: Assets/Scripts/Unit.cs:42
  • Bootstrap pattern: Assets/Scripts/Game.cs:15