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>
404 lines
14 KiB
Markdown
404 lines
14 KiB
Markdown
---
|
|
title: "feat: Asymmetric Grid CTF Board"
|
|
type: feat
|
|
date: 2026-02-04
|
|
brainstorm: 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)
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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):
|
|
|
|
```csharp
|
|
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:**
|
|
- [x] 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
|
|
- [x] Create `GridUnit.cs` as minimal data struct
|
|
- [x] 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
|
|
- [x] Modify `Game.cs` to instantiate `GridBoard` instead of current map
|
|
- [x] Alternating turns: `bool isPlayerTurn`, swap after each move
|
|
- [x] 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:**
|
|
- [x] Add fog of war:
|
|
- Each unit sees 3 cells (Chebyshev distance)
|
|
- Toggle enemy sprite `enabled` based on visibility
|
|
- Recalculate after each move
|
|
- [x] 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):**
|
|
```csharp
|
|
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:**
|
|
- [x] 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
|
|
- [x] Delete `Assets/Scripts/SimpleAI.cs`
|
|
|
|
**AI decision (single difficulty):**
|
|
```csharp
|
|
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)
|
|
- [x] 20x50 grid renders with three colored zones
|
|
- [x] 6 pieces (3 per team) at starting positions
|
|
- [x] Orthogonal movement in offense zones, diagonal in defense
|
|
- [x] Alternating turns
|
|
- [x] Collision: defender wins in their zone
|
|
- [x] Flags: pickup, drop on tag, return on touch, score on base
|
|
- [x] First to 3 wins
|
|
|
|
### Functional (Phase 2)
|
|
- [x] 3-cell vision radius per unit
|
|
- [x] Enemy units hidden outside fog
|
|
- [x] Defenders skip every 4th move
|
|
|
|
### Functional (Phase 3)
|
|
- [x] AI plays Red team
|
|
- [x] AI respects fog
|
|
- [x] 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`
|