about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--forth/foreforthfourth/README.md108
-rw-r--r--forth/foreforthfourth/forth.js177
-rw-r--r--html/XCOM/game.js3598
-rw-r--r--html/XCOM/index.html22
-rw-r--r--html/XCOM/style.css104
5 files changed, 3937 insertions, 72 deletions
diff --git a/forth/foreforthfourth/README.md b/forth/foreforthfourth/README.md
index 3e8579b..29c3b5e 100644
--- a/forth/foreforthfourth/README.md
+++ b/forth/foreforthfourth/README.md
@@ -25,9 +25,10 @@ This Forth interpreter features **4 separate stacks** that users can juggle betw
 
 ### Multi-Stack Operations
 - **Stack Focus System**: `focus.red`, `focus.teal`, `focus.blue`, `focus.yellow` (or `focus.1`, `focus.2`, `focus.3`, `focus.4`)
-- **Push Operations**: `push.red`, `push.teal`, `push.blue`, `push.yellow` (or `push.1`, `push.2`, `push.3`, `push.4`)
+- **Move Operations**: `move.red`, `move.teal`, `move.blue`, `move.yellow` (or `move.1`, `move.2`, `move.3`, `move.4`)
 - **Pop Operations**: `pop.red`, `pop.teal`, `pop.blue`, `pop.yellow` (or `pop.1`, `pop.2`, `pop.3`, `pop.4`)
-- **Move Operations**: `move` (interactive stack-to-stack movement), `push.red`, `push.teal`, `push.blue`, `push.yellow` (or `push.1`, `push.2`, `push.3`, `push.4`)
+- **Copy Operations**: `copy.red`, `copy.teal`, `copy.blue`, `copy.yellow` (or `copy.1`, `copy.2`, `copy.3`, `copy.4`)
+- **Move Operations**: `move` (interactive stack-to-stack movement), `move.red`, `move.teal`, `move.blue`, `move.yellow` (or `move.1`, `move.2`, `move.3`, `move.4`)
 - **Clear Operations**: `clear.all` (clear all stacks), `clear.focused` (clear focused stack)
 - **Cross-Stack Operations**: `dup.stacks`, `over.stacks`, `swap.stacks`, `nip.stacks`, `tuck.stacks`, `rot.stacks`, `2dup.stacks`, `2over.stacks`, `2swap.stacks`
 
@@ -98,8 +99,8 @@ dup over          # Duplicate top, copy second over top
 ### Multi-Stack Juggling
 ```forth
 5 3 2             # Push to red stack
-push.teal         # Move top of red to teal stack
-push.blue         # Move top of red to blue stack
+move.teal         # Move top of red to teal stack
+move.blue         # Move top of red to blue stack
 ```
 
 ### Stack Focus System
@@ -130,11 +131,17 @@ focus.2            # Same as focus.teal
 focus.3            # Same as focus.blue
 focus.4            # Same as focus.yellow
 
-# Push commands
-push.1             # Same as push.red
-push.2             # Same as push.teal
-push.3             # Same as push.blue
-push.4             # Same as push.yellow
+# Move commands
+move.1             # Same as move.red
+move.2             # Same as move.teal
+move.3             # Same as move.blue
+move.4             # Same as move.yellow
+
+# Copy commands
+copy.1             # Same as copy.red
+copy.2             # Same as copy.teal
+copy.3             # Same as copy.blue
+copy.4             # Same as copy.yellow
 
 # Pop commands
 pop.1              # Same as pop.red
@@ -258,36 +265,47 @@ move    # Start move operation
 3. Enter the **destination stack number** (1-4)
 4. The item is **removed** from source and **added** to destination
 
-#### **Push Commands with Focus System**
-Use `push.` commands to move items from the focused stack to a specific target stack:
+#### **Move Commands with Focus System**
+Use `move.` commands to move items from the focused stack to a specific target stack:
 
 ```forth
 focus.red         # Focus on Red stack (1)
 42                # Add item to Red stack
-push.3            # Move top item to Blue stack (3)
+move.3            # Move top item to Blue stack (3)
 # Result: 42 moved from Red to Blue stack
 
 focus.teal        # Focus on Teal stack (2)
 100               # Add item to Teal stack
-push.yellow       # Move top item to Yellow stack (4)
+move.yellow       # Move top item to Yellow stack (4)
 # Result: 100 moved from Teal to Yellow stack
 ```
 
 #### **Number and Color Aliases**
-All push commands support both number and color naming:
+All move and copy commands support both number and color naming:
 
 ```forth
-# Number aliases
-push.1            # Move to Red stack (1)
-push.2            # Move to Teal stack (2)
-push.3            # Move to Blue stack (3)
-push.4            # Move to Yellow stack (4)
+# Move commands (remove from source)
+move.1            # Move to Red stack (1)
+move.2            # Move to Teal stack (2)
+move.3            # Move to Blue stack (3)
+move.4            # Move to Yellow stack (4)
+
+# Copy commands (keep in source)
+copy.1            # Copy to Red stack (1)
+copy.2            # Copy to Teal stack (2)
+copy.3            # Copy to Blue stack (3)
+copy.4            # Copy to Yellow stack (4)
 
 # Color aliases
-push.red          # Move to Red stack (1)
-push.teal         # Move to Teal stack (2)
-push.blue         # Move to Blue stack (3)
-push.yellow       # Move to Yellow stack (4)
+move.red          # Move to Red stack (1)
+move.teal         # Move to Teal stack (2)
+move.blue         # Move to Blue stack (3)
+move.yellow       # Move to Yellow stack (4)
+
+copy.red          # Copy to Red stack (1)
+copy.teal         # Copy to Teal stack (2)
+copy.blue         # Copy to Blue stack (3)
+copy.yellow       # Copy to Yellow stack (4)
 ```
 
 #### **Comparison: Move vs Copy Operations**
@@ -295,19 +313,24 @@ push.yellow       # Move to Yellow stack (4)
 | Operation | Effect | Duplication | Use Case |
 |-----------|--------|-------------|----------|
 | `move` | **Moves** item from source to destination | ❌ No | Relocate items between stacks |
-| `push.` | **Moves** item from focused stack to target | ❌ No | Move from focused stack to specific stack |
+| `move.{stack}` | **Moves** item from focused stack to target | ❌ No | Move from focused stack to specific stack |
+| `copy.{stack}` | **Copies** item from focused stack to target | ✅ Yes | Keep item on source, copy to target |
 | `dup.stacks` | **Copies** item from focused stack to target | ✅ Yes | Keep item on source, copy to target |
 | `over.stacks` | **Copies** second item from focused stack to target | ✅ Yes | Copy second item without affecting top |
 
-#### **Quick Reference: All Move Operations**
+#### **Quick Reference: All Move and Copy Operations**
 
 | Command | From | To | Effect |
 |---------|------|----|---------|
 | `move` + source + dest | Any stack | Any stack | Move top item between specified stacks |
-| `push.red` / `push.1` | Focused stack | Red stack (1) | Move top item to Red stack |
-| `push.teal` / `push.2` | Focused stack | Teal stack (2) | Move top item to Teal stack |
-| `push.blue` / `push.3` | Focused stack | Blue stack (3) | Move top item to Blue stack |
-| `push.yellow` / `push.4` | Focused stack | Yellow stack (4) | Move top item to Yellow stack |
+| `move.red` / `move.1` | Focused stack | Red stack (1) | Move top item to Red stack |
+| `move.teal` / `move.2` | Focused stack | Teal stack (2) | Move top item to Teal stack |
+| `move.blue` / `move.3` | Focused stack | Blue stack (3) | Move top item to Blue stack |
+| `move.yellow` / `move.4` | Focused stack | Yellow stack (4) | Move top item to Yellow stack |
+| `copy.red` / `copy.1` | Focused stack | Red stack (1) | Copy top item to Red stack |
+| `copy.teal` / `copy.2` | Focused stack | Teal stack (2) | Copy top item to Teal stack |
+| `copy.blue` / `copy.3` | Focused stack | Blue stack (3) | Copy top item to Blue stack |
+| `copy.yellow` / `copy.4` | Focused stack | Yellow stack (4) | Copy top item to Yellow stack |
 
 #### **Complete Move Example**
 ```forth
@@ -321,11 +344,11 @@ focus.blue
 
 # Move items between stacks
 focus.red
-push.2            # Move 42 from Red to Teal
+move.2            # Move 42 from Red to Teal
 # Red stack: [], Teal stack: [100, 42]
 
 focus.teal
-push.3            # Move 100 from Teal to Blue
+move.3            # Move 100 from Teal to Blue
 # Teal stack: [42], Blue stack: [200, 100]
 
 # Use interactive move for complex operations
@@ -376,7 +399,7 @@ words             # List all available words
 
 ## Current Status
 
-### ✅ **Fully Implemented Features**
+### **Fully Implemented Features**
 - **Control Flow**: `IF ... THEN`, `IF ... ELSE ... THEN`, `BEGIN ... UNTIL` constructs
 - **String Operations**: String literals (`."` and `s"`), manipulation (`strlen`, `strcat`, `char+`, `type`, `count`)
 - **Stack Focus System**: Operate on any of the 4 stacks using focus commands
@@ -384,25 +407,14 @@ words             # List all available words
 - **Help System**: Comprehensive help (`help`) and word documentation (`doc`)
 - **Multi-Stack Operations**: Full support for all 4 stacks with focus system
 - **Enhanced Clear Operations**: `clear.all` and `clear.focused` commands
-- **Move Operations**: Interactive `move` command and `push.` commands for moving items between stacks
-- **Number Aliases**: All focus, push, and pop commands support both color names and numbers (1-4)
+- **Move Operations**: Interactive `move` command and `move.{stack}` commands for moving items between stacks
+- **Copy Operations**: `copy.{stack}` commands for copying items between stacks without removal
+- **Number Aliases**: All focus, move, copy, and pop commands support both color names and numbers (1-4)
 - **Math Utilities**: `abs`, `negate`, `min`, `max` operations
 
-### 🚀 **Advanced Capabilities**
+### **Advanced Capabilities**
 - **Universal Stack Operations**: All built-in words work on any focused stack
 - **Dual Naming System**: Both color names and numbers work for all commands
 - **Professional Error Handling**: Context-aware error messages with solutions
 - **Visual Focus Indicators**: UI shows which stack is currently focused
-- **Complete Test Coverage**: 100% test coverage of all features
-
-### Potential Extensions
-- **Graphics**: Simple drawing operations
-- **Networking**: HTTP requests and responses
-- **Persistence**: Save state between sessions
-- **Modules**: Import/export word definitions
-
-## Known Issues
-
-- The `move` operation requires two separate command inputs
-- Some edge cases in error handling may need refinement
-- Performance optimization for large programs
+- **Complete Test Coverage**: 100% test coverage of all features
\ No newline at end of file
diff --git a/forth/foreforthfourth/forth.js b/forth/foreforthfourth/forth.js
index 3a654d1..af133ea 100644
--- a/forth/foreforthfourth/forth.js
+++ b/forth/foreforthfourth/forth.js
@@ -933,11 +933,11 @@ const builtinWords = {
     },
 
     // Multi-stack operations
-    'push.red': {
+    'move.red': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -945,15 +945,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 0, value)
             });
         },
-        doc: 'Move top item from focused stack to red stack',
+        doc: 'Move top item from focused stack to red stack (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.1': {
+    'move.1': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -961,15 +961,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 0, value)
             });
         },
-        doc: 'Move top item from focused stack to red stack (Stack 1)',
+        doc: 'Move top item from focused stack to red stack (Stack 1) (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.teal': {
+    'move.teal': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -977,15 +977,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 1, value)
             });
         },
-        doc: 'Move top item from focused stack to teal stack',
+        doc: 'Move top item from focused stack to teal stack (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.2': {
+    'move.2': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -993,15 +993,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 1, value)
             });
         },
-        doc: 'Move top item from focused stack to teal stack (Stack 2)',
+        doc: 'Move top item from focused stack to teal stack (Stack 2) (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.blue': {
+    'move.blue': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -1009,15 +1009,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 2, value)
             });
         },
-        doc: 'Move top item from focused stack to blue stack',
+        doc: 'Move top item from focused stack to blue stack (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.3': {
+    'move.3': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -1025,15 +1025,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 2, value)
             });
         },
-        doc: 'Move top item from focused stack to blue stack (Stack 3)',
+        doc: 'Move top item from focused stack to blue stack (Stack 3) (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.yellow': {
+    'move.yellow': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -1041,15 +1041,15 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 3, value)
             });
         },
-        doc: 'Move top item from focused stack to yellow stack',
+        doc: 'Move top item from focused stack to yellow stack (removes from focused stack)',
         stack: '( x -- )'
     },
 
-    'push.4': {
+    'move.4': {
         fn: (state) => {
             if (state.stacks[state.focusedStack].length === 0) {
                 return updateState(state, { 
-                    output: [...state.output, `Error: Stack underflow on push.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
+                    output: [...state.output, `Error: Stack underflow on move.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] 
                 });
             }
             const { stacks, value } = popFromStack(state.stacks, state.focusedStack);
@@ -1057,10 +1057,139 @@ const builtinWords = {
                 stacks: pushToStack(stacks, 3, value)
             });
         },
-        doc: 'Move top item from focused stack to yellow stack (Stack 4)',
+        doc: 'Move top item from focused stack to yellow stack (Stack 4) (removes from focused stack)',
         stack: '( x -- )'
     },
 
+    // Copy operations (keep item in source stack)
+    'copy.red': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 0, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to red stack (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.1': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 0, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to red stack (Stack 1) (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.teal': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 1, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to teal stack (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.2': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 1, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to teal stack (Stack 2) (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.blue': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 2, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to blue stack (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.3': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 2, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to blue stack (Stack 3) (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.yellow': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 3, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to yellow stack (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
+    'copy.4': {
+        fn: (state) => {
+            if (state.stacks[state.focusedStack].length === 0) {
+                return updateState(state, { 
+                    output: [...state.output, `Error: Stack underflow on copy.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] 
+                });
+            }
+            const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1];
+            return updateState(state, {
+                stacks: pushToStack(state.stacks, 3, top)
+            });
+        },
+        doc: 'Copy top item from focused stack to yellow stack (Stack 4) (keeps item on focused stack)',
+        stack: '( x -- x )'
+    },
+
     'pop.red': {
         fn: (state) => popAndPrint(state, 0),
         doc: 'Pop and print top item from red stack',
diff --git a/html/XCOM/game.js b/html/XCOM/game.js
new file mode 100644
index 0000000..6c4e256
--- /dev/null
+++ b/html/XCOM/game.js
@@ -0,0 +1,3598 @@
+// XCOM-like Game - Game Logic and Rendering
+// Built using The Elm Architecture pattern
+
+// ------------------------------------------------
+// TYPE DEFINITIONS
+// ------------------------------------------------
+
+/**
+ * @typedef {'floor' | 'wall' | 'obstacle'} TileType
+ * @typedef {{ type: TileType, x: number, y: number, providesCover: boolean, health: number, damageFlash: number, color: string }} Tile
+ * @typedef {'player' | 'enemy'} UnitOwner
+ * @typedef {{ 
+ *   id: number, 
+ *   owner: UnitOwner, 
+ *   x: number, 
+ *   y: number, 
+ *   maxMovement: number,
+ *   movementRange: number,
+ *   shootRange: number,
+ *   damage: number,
+ *   maxHp: number,
+ *   currentHp: number,
+ *   inCover: boolean,
+ *   hasMoved: boolean,
+ *   hasAttacked: boolean,
+ *   isAnimating: boolean,
+ *   animationType: 'none' | 'moving' | 'shooting' | 'dying',
+ *   animationProgress: number,
+ *   targetX: number,
+ *   targetY: number,
+ *   path: {x: number, y: number}[],
+ *   projectileX: number,
+ *   projectileY: number,
+ *   projectileTargetX: number,
+ *   projectileTargetY: number,
+ *   deathParticles: DeathParticle[],
+ *   isDead: boolean,
+ *   pendingDamage: number,
+ *   justCompletedShot: boolean,
+ *   isVisible: boolean,
+ *   lastSeen: number,
+ *   lastKnownX: number,
+ *   lastKnownY: number,
+ *   aiBehavior: 'aggressive' | 'patrol' | 'stationary',
+ *   turnOrder: number,
+ *   patrolCenterX: number,
+ *   patrolCenterY: number,
+ *   patrolRadius: number,
+ *   actionFeedbackTimer: number
+ * }} Unit
+ * @typedef {{ 
+ *   x: number, 
+ *   y: number, 
+ *   velocityX: number, 
+ *   velocityY: number, 
+ *   size: number, 
+ *   life: number, 
+ *   maxLife: number,
+ *   color: string
+ * }} DeathParticle
+ * @typedef {{ type: 'AwaitingSelection' } | { type: 'UnitSelected', unitId: number } | { type: 'AttackMode', unitId: number }} UIState
+ * @typedef {{ grid: Tile[][], units: Unit[], selectedUnit: Unit | null, uiState: UIState, currentTurnIndex: number }} Model
+ */
+
+// ------------------------------------------------
+// RENDER CONSTANTS
+// ------------------------------------------------
+
+const TILE_SIZE = 40;
+const UNIT_RADIUS = 15;
+const OBSTACLE_MAX_HEALTH = 20; 
+const MAX_VISIBILITY_RANGE = 6; // Maximum distance units can see
+const ENEMY_TURN_SPEED = 0.3; // Enemy turns are 3x faster than player turns
+const ENEMY_ACTION_FEEDBACK_DURATION = 800; // How long to show enemy action feedback
+const AI_BEHAVIORS = ['aggressive', 'patrol', 'stationary'];
+const COLORS = {
+    gridLine: '#444',
+    floor: '#2a2a2a',
+    wall: '#555',
+    obstacle: '#2C3E50', // Dark blue-grey for obstacles
+    player: '#00f',
+    enemy: '#f00',
+    unitBorder: '#fff',
+    selectedBorder: '#0f0',
+    moveHighlight: 'rgba(0, 150, 255, 0.4)',
+    coverHighlight: 'rgba(255, 215, 0, 0.3)'
+};
+
+// ------------------------------------------------
+// PROCEDURAL GENERATION CONSTANTS
+// ------------------------------------------------
+
+const GENERATION_TYPES = {
+    SPARSE_WAREHOUSE: 'sparse_warehouse',
+    HALLWAY_ROOMS: 'hallway_rooms',
+    DRUNKARDS_WALK: 'drunkards_walk',
+    CELLULAR_AUTOMATA: 'cellular_automata'
+};
+
+const WAREHOUSE_CONFIG = {
+    obstacleChance: 0.15,
+    minObstacles: 3,
+    maxObstacles: 8
+};
+
+const HALLWAY_CONFIG = {
+    minRoomSize: 3,
+    maxRoomSize: 6,
+    minRooms: 2,
+    maxRooms: 5,
+    corridorWidth: 2
+};
+
+const DRUNKARD_CONFIG = {
+    steps: 0.4, // Percentage of grid to fill
+    maxSteps: 1000,
+    minPathLength: 0.3 // Minimum percentage of floor tiles
+};
+
+const CELLULAR_CONFIG = {
+    iterations: 4,
+    birthThreshold: 4,
+    survivalThreshold: 3,
+    minFloorPercentage: 0.4
+};
+
+// ------------------------------------------------
+// UTILITY FUNCTIONS
+// ------------------------------------------------
+
+/**
+ * Calculates the optimal grid size to fit the viewport
+ * @returns {{ width: number, height: number }} Grid dimensions
+ */
+function calculateGridSize() {
+    const viewportWidth = window.innerWidth;
+    const viewportHeight = window.innerHeight;
+    
+    // Account for button space and borders
+    const availableWidth = viewportWidth - 40;
+    const availableHeight = viewportHeight - 40;
+    
+    const width = Math.floor(availableWidth / TILE_SIZE);
+    const height = Math.floor(availableHeight / TILE_SIZE);
+    
+    // Ensure minimum grid size
+    return {
+        width: Math.max(width, 8),
+        height: Math.max(height, 6)
+    };
+}
+
+/**
+ * Checks if a path between two points is blocked by obstacles
+ * @param {number} startX 
+ * @param {number} startY 
+ * @param {number} endX 
+ * @param {number} endY 
+ * @param {Tile[][]} grid
+ * @returns {boolean} True if path is blocked
+ */
+function isPathBlocked(startX, startY, endX, endY, grid) {
+    console.log('isPathBlocked check from', startX, startY, 'to', endX, endY);
+    
+    const dx = Math.sign(endX - startX);
+    const dy = Math.sign(endY - startY);
+    
+    let currentX = startX;
+    let currentY = startY;
+    
+    while (currentX !== endX || currentY !== endY) {
+        if (currentX !== endX) {
+            currentX += dx;
+            if (grid[currentY][currentX].type === 'obstacle') {
+                console.log('Path blocked by obstacle at', currentX, currentY);
+                return true;
+            }
+        }
+        if (currentY !== endY) {
+            currentY += dy;
+            if (grid[currentY][currentX].type === 'obstacle') {
+                console.log('Path blocked by obstacle at', currentX, currentY);
+                return true;
+            }
+        }
+    }
+    
+    console.log('Path is clear');
+    return false;
+}
+
+/**
+ * Checks if a unit is in cover based on adjacent obstacles
+ * @param {Unit} unit 
+ * @param {Tile[][]} grid
+ * @returns {boolean} True if unit is in cover
+ */
+function checkCover(unit, grid) {
+    const { x, y } = unit;
+    const directions = [
+        { dx: -1, dy: 0 }, { dx: 1, dy: 0 },
+        { dx: 0, dy: -1 }, { dx: 0, dy: 1 }
+    ];
+    
+    return directions.some(({ dx, dy }) => {
+        const checkX = x + dx;
+        const checkY = y + dy;
+        return checkX >= 0 && checkX < grid[0].length &&
+               checkY >= 0 && checkY < grid.length &&
+               grid[checkY][checkX].type === 'obstacle';
+    });
+}
+
+// ------------------------------------------------
+// PATHFINDING AND ANIMATION
+// ------------------------------------------------
+
+/**
+ * A* pathfinding algorithm to find optimal path between two points
+ * @param {number} startX 
+ * @param {number} startY 
+ * @param {number} endX 
+ * @param {number} endY 
+ * @param {Tile[][]} grid
+ * @returns {{x: number, y: number}[]} Path array
+ */
+function findPath(startX, startY, endX, endY, grid) {
+    const width = grid[0].length;
+    const height = grid.length;
+    
+    // Validate input coordinates
+    if (startX < 0 || startX >= width || startY < 0 || startY >= height ||
+        endX < 0 || endX >= width || endY < 0 || endY >= height) {
+        console.error('findPath: Invalid coordinates:', { startX, startY, endX, endY, width, height });
+        return [];
+    }
+    
+    // If start and end are the same, no path needed
+    if (startX === endX && startY === endY) {
+        return [];
+    }
+    
+    // Simple pathfinding for now - can be enhanced with A* later
+    const path = [];
+    let currentX = startX;
+    let currentY = startY;
+    
+    // Move horizontally first, then vertically
+    while (currentX !== endX) {
+        const nextX = currentX + Math.sign(endX - currentX);
+        if (grid[currentY][nextX].type !== 'obstacle') {
+            currentX = nextX;
+            path.push({ x: currentX, y: currentY });
+        } else {
+            // Try to go around obstacle
+            if (currentY + 1 < height && grid[currentY + 1][currentX].type !== 'obstacle') {
+                currentY++;
+                path.push({ x: currentX, y: currentY });
+            } else if (currentY - 1 >= 0 && grid[currentY - 1][currentX].type !== 'obstacle') {
+                currentY--;
+                path.push({ x: currentX, y: currentY });
+            } else {
+                // Can't find path
+                console.warn('findPath: Cannot find path around obstacle');
+                return [];
+            }
+        }
+    }
+    
+    while (currentY !== endY) {
+        const nextY = currentY + Math.sign(endY - currentY);
+        if (grid[nextY][currentX].type !== 'obstacle') {
+            currentY = nextY;
+            path.push({ x: currentX, y: currentY });
+        } else {
+            // Try to go around obstacle
+            if (currentX + 1 < width && grid[currentY][currentX + 1].type !== 'obstacle') {
+                currentX++;
+                path.push({ x: currentX, y: currentY });
+            } else if (currentX - 1 >= 0 && grid[currentX - 1][currentY].type !== 'obstacle') {
+                currentX--;
+                path.push({ x: currentX, y: currentY });
+            } else {
+                // Can't find path
+                console.warn('findPath: Cannot find path around obstacle');
+                return [];
+            }
+        }
+    }
+    
+    // Validate path before returning
+    const validPath = path.filter(point => 
+        point && typeof point.x === 'number' && typeof point.y === 'number' &&
+        point.x >= 0 && point.x < width && point.y >= 0 && point.y < height
+    );
+    
+    if (validPath.length !== path.length) {
+        console.error('findPath: Invalid path points found:', path);
+    }
+    
+    return validPath;
+}
+
+/**
+ * Starts movement animation for a unit
+ * @param {Unit} unit 
+ * @param {number} targetX 
+ * @param {number} targetY 
+ * @param {Tile[][]} grid
+ * @returns {Unit} Updated unit with animation state
+ */
+function startMovementAnimation(unit, targetX, targetY, grid) {
+    console.log('startMovementAnimation called with:', { unit: unit.id, from: { x: unit.x, y: unit.y }, to: { x: targetX, y: targetY } });
+    
+    const path = findPath(unit.x, unit.y, targetX, targetY, grid);
+    console.log('Path found:', path);
+    
+    if (path.length === 0) {
+        console.warn('No path found for unit', unit.id);
+        return unit; // No path found
+    }
+    
+    const animatedUnit = {
+        ...unit,
+        isAnimating: true,
+        animationType: 'moving',
+        animationProgress: 0,
+        targetX,
+        targetY,
+        path
+    };
+    
+    console.log('Animated unit created:', animatedUnit);
+    return animatedUnit;
+}
+
+/**
+ * Updates animation progress for all units
+ * @param {Unit[]} units 
+ * @param {number} deltaTime 
+ * @returns {Unit[]} Updated units
+ */
+function updateAnimations(units, deltaTime) {
+    const ANIMATION_SPEED = 0.003; // Adjust for animation speed
+    
+    return units.map(unit => {
+        if (!unit.isAnimating) return unit;
+        
+        const newProgress = unit.animationProgress + deltaTime * ANIMATION_SPEED;
+        
+        if (newProgress >= 1) {
+            // Animation complete
+            if (unit.animationType === 'moving') {
+                return {
+                    ...unit,
+                    x: unit.targetX,
+                    y: unit.targetY,
+                    hasMoved: true,
+                    isAnimating: false,
+                    animationType: 'none',
+                    animationProgress: 0,
+                    targetX: -1,
+                    targetY: -1,
+                    path: []
+                };
+            } else if (unit.animationType === 'shooting') {
+                return {
+                    ...unit,
+                    hasAttacked: true,
+                    isAnimating: false,
+                    animationType: 'none',
+                    animationProgress: 0,
+                    projectileX: -1,
+                    projectileY: -1,
+                    // Don't reset projectileTargetX/Y yet - keep them for damage processing
+                    justCompletedShot: true // Flag to indicate shot just completed
+                };
+            } else if (unit.animationType === 'dying') {
+                // Death animation is complete, remove the unit
+                return null;
+            }
+        }
+        
+        // Update death particles if dying
+        if (unit.animationType === 'dying') {
+            const updatedParticles = updateDeathParticles(unit.deathParticles);
+            if (updatedParticles.length === 0) {
+                // All particles are gone, complete the death animation
+                return null;
+            }
+            return {
+                ...unit,
+                deathParticles: updatedParticles,
+                animationProgress: newProgress
+            };
+        }
+        
+        return {
+            ...unit,
+            animationProgress: newProgress
+        };
+    }).filter(unit => unit !== null); // Filter out units that completed their animation
+}
+
+/**
+ * Gets the current position of a unit during animation
+ * @param {Unit} unit 
+ * @returns {{x: number, y: number}} Current position
+ */
+function getUnitPosition(unit) {
+    // Safety checks
+    if (!unit) {
+        console.error('getUnitPosition: unit is undefined');
+        return { x: 0, y: 0 };
+    }
+    
+    if (typeof unit.x !== 'number' || typeof unit.y !== 'number') {
+        console.error('getUnitPosition: unit coordinates are invalid:', unit);
+        return { x: 0, y: 0 };
+    }
+    
+    // If unit is not animating or not moving, return current position
+    if (!unit.isAnimating || unit.animationType !== 'moving') {
+        return { x: unit.x, y: unit.y };
+    }
+    
+    // Safety check for path data
+    if (!unit.path || !Array.isArray(unit.path) || unit.path.length === 0) {
+        console.warn('getUnitPosition: unit has invalid path data, returning current position:', unit.id);
+        return { x: unit.x, y: unit.y };
+    }
+    
+    // Safety check for animation progress
+    if (typeof unit.animationProgress !== 'number' || unit.animationProgress < 0 || unit.animationProgress > 1) {
+        console.warn('getUnitPosition: unit has invalid animation progress, returning current position:', unit.id);
+        return { x: unit.x, y: unit.y };
+    }
+    
+    // Calculate position along path
+    const totalDistance = unit.path.length;
+    const currentStep = Math.floor(unit.animationProgress * totalDistance);
+    
+    if (currentStep >= unit.path.length) {
+        if (typeof unit.targetX === 'number' && typeof unit.targetY === 'number') {
+            return { x: unit.targetX, y: unit.targetY };
+        } else {
+            return { x: unit.x, y: unit.y };
+        }
+    }
+    
+    const pathPoint = unit.path[currentStep];
+    if (pathPoint && typeof pathPoint.x === 'number' && typeof pathPoint.y === 'number') {
+        return pathPoint;
+    } else {
+        console.warn('getUnitPosition: invalid path point at step', currentStep, 'for unit', unit.id, 'path:', unit.path);
+        return { x: unit.x, y: unit.y };
+    }
+}
+
+/**
+ * Gets the current projectile position during shooting animation
+ * @param {Unit} unit 
+ * @returns {{x: number, y: number} | null} Current projectile position or null if not shooting
+ */
+function getProjectilePosition(unit) {
+    if (!unit.isAnimating || unit.animationType !== 'shooting') {
+        return null;
+    }
+    
+    if (typeof unit.projectileX !== 'number' || typeof unit.projectileY !== 'number' ||
+        typeof unit.projectileTargetX !== 'number' || typeof unit.projectileTargetY !== 'number') {
+        return null;
+    }
+    
+    // Calculate projectile position along the line from start to target
+    const startX = unit.projectileX * TILE_SIZE + TILE_SIZE / 2;
+    const startY = unit.projectileY * TILE_SIZE + TILE_SIZE / 2;
+    const targetX = unit.projectileTargetX * TILE_SIZE + TILE_SIZE / 2;
+    const targetY = unit.projectileTargetY * TILE_SIZE + TILE_SIZE / 2;
+    
+    const currentX = startX + (targetX - startX) * unit.animationProgress;
+    const currentY = startY + (targetY - startY) * unit.animationProgress;
+    
+    return { x: currentX, y: currentY };
+}
+
+/**
+ * Starts shooting animation for a unit
+ * @param {Unit} unit 
+ * @param {number} targetX 
+ * @param {number} targetY 
+ * @returns {Unit} Updated unit with shooting animation
+ */
+function startShootingAnimation(unit, targetX, targetY) {
+    console.log('Starting shooting animation for unit:', unit.id, 'at target:', targetX, targetY);
+    return {
+        ...unit,
+        isAnimating: true,
+        animationType: 'shooting',
+        animationProgress: 0,
+        projectileX: unit.x,
+        projectileY: unit.y,
+        projectileTargetX: targetX,
+        projectileTargetY: targetY
+    };
+}
+
+/**
+ * Creates death particles for a unit
+ * @param {Unit} unit 
+ * @returns {Unit} Updated unit with death particles
+ */
+function createDeathParticles(unit) {
+    const DEATH_PARTICLE_COUNT = 30;
+    const DEATH_PARTICLE_SPEED = 8;
+    const DEATH_PARTICLE_SIZE = 3;
+    const DEATH_PARTICLE_LIFETIME = 60;
+    
+    const centerX = unit.x * TILE_SIZE + TILE_SIZE / 2;
+    const centerY = unit.y * TILE_SIZE + TILE_SIZE / 2;
+    
+    const deathParticles = [];
+    for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) {
+        const angle = (Math.PI * 2 * i) / DEATH_PARTICLE_COUNT;
+        const speed = DEATH_PARTICLE_SPEED * (0.5 + Math.random());
+        
+        // Choose color based on unit type
+        let color;
+        if (unit.owner === 'player') {
+            color = ['#4CAF50', '#45a049', '#2E7D32'][Math.floor(Math.random() * 3)]; // Green variants
+        } else {
+            color = ['#F44336', '#D32F2F', '#B71C1C'][Math.floor(Math.random() * 3)]; // Red variants
+        }
+        
+        deathParticles.push({
+            x: centerX + (Math.random() - 0.5) * 10,
+            y: centerY + (Math.random() - 0.5) * 10,
+            velocityX: Math.cos(angle) * speed,
+            velocityY: Math.sin(angle) * speed,
+            size: DEATH_PARTICLE_SIZE + Math.random() * 2,
+            life: DEATH_PARTICLE_LIFETIME,
+            maxLife: DEATH_PARTICLE_LIFETIME,
+            color: color
+        });
+    }
+    
+    return {
+        ...unit,
+        isAnimating: true,
+        animationType: 'dying',
+        animationProgress: 0,
+        deathParticles,
+        isDead: true
+    };
+}
+
+/**
+ * Updates death particle animations
+ * @param {DeathParticle[]} particles 
+ * @returns {DeathParticle[]} Updated particles
+ */
+function updateDeathParticles(particles) {
+    return particles.map(particle => {
+        // Apply gravity
+        particle.velocityY += 0.3;
+        
+        // Update position
+        particle.x += particle.velocityX;
+        particle.y += particle.velocityY;
+        
+        // Reduce life
+        particle.life--;
+        
+        // Add some randomness to movement
+        particle.velocityX *= 0.98;
+        particle.velocityY *= 0.98;
+        
+        return particle;
+    }).filter(particle => particle.life > 0);
+}
+
+// ------------------------------------------------
+// PROCEDURAL GENERATION ALGORITHMS
+// ------------------------------------------------
+
+/**
+ * Generates a sparse warehouse-like environment
+ * @param {number} width 
+ * @param {number} height 
+ * @returns {Tile[][]} Generated grid
+ */
+function generateSparseWarehouse(width, height) {
+    const grid = Array.from({ length: height }, (_, y) =>
+        Array.from({ length: width }, (_, x) => ({
+            type: 'floor',
+            x,
+            y,
+            providesCover: false,
+            health: 0,
+            damageFlash: 0,
+            color: generateObstacleColor()
+        }))
+    );
+    
+    // Add random obstacles
+    const numObstacles = Math.floor(
+        Math.random() * (WAREHOUSE_CONFIG.maxObstacles - WAREHOUSE_CONFIG.minObstacles + 1) + 
+        WAREHOUSE_CONFIG.minObstacles
+    );
+    
+    let obstaclesPlaced = 0;
+    const maxAttempts = width * height * 2;
+    let attempts = 0;
+    
+    while (obstaclesPlaced < numObstacles && attempts < maxAttempts) {
+        const x = Math.floor(Math.random() * width);
+        const y = Math.floor(Math.random() * height);
+        
+        // Don't place on edges or if already occupied
+        if (x > 0 && x < width - 1 && y > 0 && y < height - 1 && 
+            grid[y][x].type === 'floor') {
+            
+            // Check if obstacle provides meaningful cover (not isolated)
+            const hasAdjacentFloor = [
+                { dx: -1, dy: 0 }, { dx: 1, dy: 0 },
+                { dx: 0, dy: -1 }, { dx: 0, dy: 1 }
+            ].some(({ dx, dy }) => {
+                const checkX = x + dx;
+                const checkY = y + dy;
+                return checkX >= 0 && checkX < width && 
+                       checkY >= 0 && checkY < height && 
+                       grid[checkY][checkX].type === 'floor';
+            });
+            
+            if (hasAdjacentFloor) {
+                grid[y][x] = createObstacle(x, y);
+                obstaclesPlaced++;
+            }
+        }
+        attempts++;
+    }
+    
+    return grid;
+}
+
+/**
+ * Generates a structured hallway and room layout
+ * @param {number} width 
+ * @param {number} height 
+ * @returns {Tile[][]} Generated grid
+ */
+function generateHallwayRooms(width, height) {
+    const grid = Array.from({ length: height }, (_, y) =>
+        Array.from({ length: width }, (_, x) => ({
+            type: 'floor',
+            x,
+            y,
+            providesCover: false,
+            health: 0,
+            damageFlash: 0,
+            color: generateObstacleColor()
+        }))
+    );
+    
+    // Generate rooms
+    const numRooms = Math.floor(
+        Math.random() * (HALLWAY_CONFIG.maxRooms - HALLWAY_CONFIG.minRooms + 1) + 
+        HALLWAY_CONFIG.minRooms
+    );
+    
+    const rooms = [];
+    
+    for (let i = 0; i < numRooms; i++) {
+        const roomWidth = Math.floor(
+            Math.random() * (HALLWAY_CONFIG.maxRoomSize - HALLWAY_CONFIG.minRoomSize + 1) + 
+            HALLWAY_CONFIG.minRoomSize
+        );
+        const roomHeight = Math.floor(
+            Math.random() * (HALLWAY_CONFIG.maxRoomSize - HALLWAY_CONFIG.minRoomSize + 1) + 
+            HALLWAY_CONFIG.minRoomSize
+        );
+        
+        // Try to place room
+        let roomPlaced = false;
+        let attempts = 0;
+        const maxAttempts = 50;
+        
+        while (!roomPlaced && attempts < maxAttempts) {
+            const startX = Math.floor(Math.random() * (width - roomWidth - 2)) + 1;
+            const startY = Math.floor(Math.random() * (height - roomHeight - 2)) + 1;
+            
+            // Check if room can fit
+            let canFit = true;
+            for (let y = startY - 1; y <= startY + roomHeight; y++) {
+                for (let x = startX - 1; x <= startX + roomWidth; x++) {
+                    if (y < 0 || y >= height || x < 0 || x >= width) {
+                        canFit = false;
+                        break;
+                    }
+                    if (grid[y][x].type === 'obstacle') {
+                        canFit = false;
+                        break;
+                    }
+                }
+                if (!canFit) break;
+            }
+            
+            if (canFit) {
+                // Place room walls
+                for (let y = startY; y < startY + roomHeight; y++) {
+                    for (let x = startX; x < startX + roomWidth; x++) {
+                        grid[y][x] = createObstacle(x, y);
+                    }
+                }
+                
+                // Add some interior obstacles for cover
+                const interiorObstacles = Math.floor(Math.random() * 3) + 1;
+                for (let j = 0; j < interiorObstacles; j++) {
+                    const obstacleX = startX + 1 + Math.floor(Math.random() * (roomWidth - 2));
+                    const obstacleY = startY + 1 + Math.floor(Math.random() * (roomHeight - 2));
+                    
+                    if (grid[obstacleY][obstacleX].type === 'obstacle') {
+                        grid[obstacleY][obstacleX] = {
+                            type: 'floor',
+                            x: obstacleX,
+                            y: obstacleY,
+                            providesCover: false,
+                            health: 0,
+                            damageFlash: 0,
+                            color: generateObstacleColor()
+                        };
+                    }
+                }
+                
+                rooms.push({ startX, startY, width: roomWidth, height: roomHeight });
+                roomPlaced = true;
+            }
+            attempts++;
+        }
+    }
+    
+    // Connect rooms with corridors
+    if (rooms.length > 1) {
+        for (let i = 0; i < rooms.length - 1; i++) {
+            const room1 = rooms[i];
+            const room2 = rooms[i + 1];
+            
+            // Create corridor between room centers
+            const center1X = Math.floor(room1.startX + room1.width / 2);
+            const center1Y = Math.floor(room1.startY + room1.height / 2);
+            const center2X = Math.floor(room2.startX + room2.width / 2);
+            const center2Y = Math.floor(room2.startY + room2.height / 2);
+            
+            // Horizontal corridor
+            const corridorStartX = Math.min(center1X, center2X);
+            const corridorEndX = Math.max(center1X, center2X);
+            for (let x = corridorStartX; x <= corridorEndX; x++) {
+                if (x >= 0 && x < width) {
+                    for (let y = center1Y - 1; y <= center1Y + 1; y++) {
+                        if (y >= 0 && y < height && grid[y][x].type === 'floor') {
+                            grid[y][x] = {
+                                type: 'obstacle',
+                                x,
+                                y,
+                                providesCover: true,
+                                health: OBSTACLE_MAX_HEALTH, // 6 shots to destroy
+                                damageFlash: 0,
+                                color: generateObstacleColor()
+                            };
+                        }
+                    }
+                }
+            }
+            
+            // Vertical corridor
+            const corridorStartY = Math.min(center1Y, center2Y);
+            const corridorEndY = Math.max(center1Y, center2Y);
+            for (let y = corridorStartY; y <= corridorEndY; y++) {
+                if (y >= 0 && y < height) {
+                    for (let x = center2X - 1; x <= center2X + 1; x++) {
+                        if (x >= 0 && x < width && grid[y][x].type === 'floor') {
+                            grid[y][x] = {
+                                type: 'obstacle',
+                                x,
+                                y,
+                                providesCover: true,
+                                health: OBSTACLE_MAX_HEALTH, // 6 shots to destroy
+                                damageFlash: 0,
+                                color: generateObstacleColor()
+                            };
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    return grid;
+}
+
+/**
+ * Generates a level using the Drunkard's Walk algorithm
+ * @param {number} width 
+ * @param {number} height 
+ * @returns {Tile[][]} Generated grid
+ */
+function generateDrunkardsWalk(width, height) {
+    const grid = Array.from({ length: height }, (_, y) =>
+        Array.from({ length: width }, (_, x) => ({
+            type: 'obstacle',
+            x,
+            y,
+            providesCover: true,
+            health: 0,
+            damageFlash: 0,
+            color: generateObstacleColor()
+        }))
+    );
+    
+    // Start from center
+    let currentX = Math.floor(width / 2);
+    let currentY = Math.floor(height / 2);
+    
+    const targetSteps = Math.floor(width * height * DRUNKARD_CONFIG.steps);
+    let steps = 0;
+    let maxAttempts = DRUNKARD_CONFIG.maxSteps;
+    let attempts = 0;
+    
+    // Ensure starting position is floor
+    grid[currentY][currentX] = {
+        type: 'floor',
+        x: currentX,
+        y: currentY,
+        providesCover: false,
+        health: 0,
+        damageFlash: 0,
+        color: generateObstacleColor()
+    };
+    
+    while (steps < targetSteps && attempts < maxAttempts) {
+        // Random direction: 0=up, 1=right, 2=down, 3=left
+        const direction = Math.floor(Math.random() * 4);
+        let newX = currentX;
+        let newY = currentY;
+        
+        switch (direction) {
+            case 0: newY = Math.max(0, currentY - 1); break; // Up
+            case 1: newX = Math.min(width - 1, currentX + 1); break; // Right
+            case 2: newY = Math.min(height - 1, currentY + 1); break; // Down
+            case 3: newX = Math.max(0, currentX - 1); break; // Left
+        }
+        
+        // Carve path
+        if (grid[newY][newX].type === 'obstacle') {
+            grid[newY][newX] = {
+                type: 'floor',
+                x: newX,
+                y: newY,
+                providesCover: false,
+                health: 0,
+                damageFlash: 0,
+                color: generateObstacleColor()
+            };
+            steps++;
+        }
+        
+        currentX = newX;
+        currentY = newY;
+        attempts++;
+    }
+    
+    // Add some random floor tiles to ensure connectivity
+    const additionalFloorTiles = Math.floor(width * height * 0.1);
+    for (let i = 0; i < additionalFloorTiles; i++) {
+        const x = Math.floor(Math.random() * width);
+        const y = Math.floor(Math.random() * height);
+        
+        if (grid[y][x].type === 'obstacle') {
+            // Check if it's adjacent to existing floor
+            const hasAdjacentFloor = [
+                { dx: -1, dy: 0 }, { dx: 1, dy: 0 },
+                { dx: 0, dy: -1 }, { dx: 0, dy: 1 }
+            ].some(({ dx, dy }) => {
+                const checkX = x + dx;
+                const checkY = y + dy;
+                return checkX >= 0 && checkX < width && 
+                       checkY >= 0 && checkY < height && 
+                       grid[checkY][checkX].type === 'floor';
+            });
+            
+            if (hasAdjacentFloor) {
+                grid[y][x] = {
+                    type: 'floor',
+                    x,
+                    y,
+                    providesCover: false,
+                    health: 0,
+                    damageFlash: 0,
+                    color: generateObstacleColor()
+                };
+            }
+        }
+    }
+    
+    return grid;
+}
+
+/**
+ * Generates a level using Cellular Automata
+ * @param {number} width 
+ * @param {number} height 
+ * @returns {Tile[][]} Generated grid
+ */
+function generateCellularAutomata(width, height) {
+    // Initialize with random noise
+    let grid = Array.from({ length: height }, (_, y) =>
+        Array.from({ length: width }, (_, x) => 
+            Math.random() < 0.45 ? createObstacle(x, y) : {
+                type: 'floor',
+                x,
+                y,
+                providesCover: false,
+                health: 0,
+                damageFlash: 0,
+                color: generateObstacleColor()
+            }
+        )
+    );
+    
+    // Ensure edges are obstacles
+    for (let y = 0; y < height; y++) {
+        grid[y][0] = createObstacle(0, y);
+        grid[y][width - 1] = createObstacle(width - 1, y);
+    }
+    for (let x = 0; x < width; x++) {
+        grid[0][x] = createObstacle(x, 0);
+        grid[height - 1][x] = createObstacle(x, height - 1);
+    }
+    
+    // Run cellular automata iterations
+    for (let iteration = 0; iteration < CELLULAR_CONFIG.iterations; iteration++) {
+        const newGrid = Array.from({ length: height }, (_, y) =>
+            Array.from({ length: width }, (_, x) => ({ ...grid[y][x] }))
+        );
+        
+        for (let y = 1; y < height - 1; y++) {
+            for (let x = 1; x < width - 1; x++) {
+                // Count adjacent obstacles
+                let adjacentObstacles = 0;
+                for (let dy = -1; dy <= 1; dy++) {
+                    for (let dx = -1; dx <= 1; dx++) {
+                        if (dx === 0 && dy === 0) continue;
+                        if (grid[y + dy][x + dx].type === 'obstacle') {
+                            adjacentObstacles++;
+                        }
+                    }
+                }
+                
+                // Apply rules
+                if (grid[y][x].type === 'obstacle') {
+                    // Survival rule
+                    if (adjacentObstacles < CELLULAR_CONFIG.survivalThreshold) {
+                        newGrid[y][x].type = 'floor';
+                        newGrid[y][x].providesCover = false;
+                    }
+                } else {
+                    // Birth rule
+                    if (adjacentObstacles >= CELLULAR_CONFIG.birthThreshold) {
+                        newGrid[y][x] = createObstacle(x, y);
+                    }
+                }
+            }
+        }
+        
+        grid = newGrid;
+    }
+    
+    // Ensure minimum floor percentage
+    const floorCount = grid.flat().filter(tile => tile.type === 'floor').length;
+    const minFloorTiles = Math.floor(width * height * CELLULAR_CONFIG.minFloorPercentage);
+    
+    if (floorCount < minFloorTiles) {
+        // Convert some obstacles to floor to meet minimum
+        const obstaclesToConvert = minFloorTiles - floorCount;
+        const obstacles = grid.flat().filter(tile => tile.type === 'obstacle' && 
+            tile.x > 0 && tile.x < width - 1 && tile.y > 0 && tile.y < height - 1);
+        
+        for (let i = 0; i < Math.min(obstaclesToConvert, obstacles.length); i++) {
+            const obstacle = obstacles[i];
+            grid[obstacle.y][obstacle.x].type = 'floor';
+            grid[obstacle.y][obstacle.x].providesCover = false;
+        }
+    }
+    
+    return grid;
+}
+
+/**
+ * Checks if a grid is fully navigable using flood fill
+ * @param {Tile[][]} grid 
+ * @returns {boolean} True if grid is navigable
+ */
+function isGridNavigable(grid) {
+    const width = grid[0].length;
+    const height = grid.length;
+    
+    // Find a starting floor tile
+    let startX = -1, startY = -1;
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            if (grid[y][x].type === 'floor') {
+                startX = x;
+                startY = y;
+                break;
+            }
+        }
+        if (startX !== -1) break;
+    }
+    
+    if (startX === -1) return false; // No floor tiles
+    
+    // Flood fill to count accessible floor tiles
+    const visited = Array.from({ length: height }, () => new Array(width).fill(false));
+    const queue = [{ x: startX, y: startY }];
+    let accessibleCount = 0;
+    
+    while (queue.length > 0) {
+        const { x, y } = queue.shift();
+        
+        if (visited[y][x] || grid[y][x].type !== 'floor') continue;
+        
+        visited[y][x] = true;
+        accessibleCount++;
+        
+        // Add adjacent tiles to queue
+        const directions = [
+            { dx: -1, dy: 0 }, { dx: 1, dy: 0 },
+            { dx: 0, dy: -1 }, { dx: 0, dy: 1 }
+        ];
+        
+        for (const { dx, dy } of directions) {
+            const newX = x + dx;
+            const newY = y + dy;
+            
+            if (newX >= 0 && newX < width && newY >= 0 && newY < height &&
+                !visited[newY][newX] && grid[newY][newX].type === 'floor') {
+                queue.push({ x: newX, y: newY });
+            }
+        }
+    }
+    
+    // Count total floor tiles
+    const totalFloorTiles = grid.flat().filter(tile => tile.type === 'floor').length;
+    
+    // Grid is navigable if at least 90% of floor tiles are accessible
+    return accessibleCount >= totalFloorTiles * 0.9;
+}
+
+/**
+ * Ensures a grid is navigable by adding connecting paths if needed
+ * @param {Tile[][]} grid 
+ * @returns {Tile[][]} Navigable grid
+ */
+function ensureNavigability(grid) {
+    if (isGridNavigable(grid)) {
+        return grid;
+    }
+    
+    const width = grid[0].length;
+    const height = grid.length;
+    
+    // Find isolated floor regions and connect them
+    const visited = Array.from({ length: height }, () => new Array(width).fill(false));
+    const regions = [];
+    
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            if (grid[y][x].type === 'floor' && !visited[y][x]) {
+                // Found new region, flood fill it
+                const region = [];
+                const queue = [{ x, y }];
+                
+                while (queue.length > 0) {
+                    const { x: cx, y: cy } = queue.shift();
+                    
+                    if (visited[cy][cx] || grid[cy][cx].type !== 'floor') continue;
+                    
+                    visited[cy][cx] = true;
+                    region.push({ x: cx, y: cy });
+                    
+                    const directions = [
+                        { dx: -1, dy: 0 }, { dx: 1, dy: 0 },
+                        { dx: 0, dy: -1 }, { dx: 0, dy: 1 }
+                    ];
+                    
+                    for (const { dx, dy } of directions) {
+                        const newX = cx + dx;
+                        const newY = cy + dy;
+                        
+                        if (newX >= 0 && newX < width && newY >= 0 && newY < height &&
+                            !visited[newY][newX] && grid[newY][newX].type === 'floor') {
+                            queue.push({ x: newX, y: newY });
+                        }
+                    }
+                }
+                
+                if (region.length > 0) {
+                    regions.push(region);
+                }
+            }
+        }
+    }
+    
+    // Connect regions by creating paths between them
+    for (let i = 0; i < regions.length - 1; i++) {
+        const region1 = regions[i];
+        const region2 = regions[i + 1];
+        
+        // Find closest points between regions
+        let minDistance = Infinity;
+        let point1 = null, point2 = null;
+        
+        for (const tile1 of region1) {
+            for (const tile2 of region2) {
+                const distance = Math.abs(tile1.x - tile2.x) + Math.abs(tile1.y - tile2.y);
+                if (distance < minDistance) {
+                    minDistance = distance;
+                    point1 = tile1;
+                    point2 = tile2;
+                }
+            }
+        }
+        
+        if (point1 && point2) {
+            // Create path between points
+            let currentX = point1.x;
+            let currentY = point1.y;
+            
+            while (currentX !== point2.x || currentY !== point2.y) {
+                if (currentX !== point2.x) {
+                    currentX += Math.sign(point2.x - currentX);
+                    if (grid[currentY][currentX].type === 'obstacle') {
+                        grid[currentY][currentX] = {
+                            type: 'floor',
+                            x: currentX,
+                            y: currentY,
+                            providesCover: false,
+                            health: 0,
+                            damageFlash: 0,
+                            color: generateObstacleColor()
+                        };
+                    }
+                }
+                if (currentY !== point2.y) {
+                    currentY += Math.sign(point2.y - currentY);
+                    if (grid[currentY][currentX].type === 'obstacle') {
+                        grid[currentY][currentX] = {
+                            type: 'floor',
+                            x: currentX,
+                            y: currentY,
+                            providesCover: false,
+                            health: 0,
+                            damageFlash: 0,
+                            color: generateObstacleColor()
+                        };
+                    }
+                }
+            }
+        }
+    }
+    
+    return grid;
+}
+
+/**
+ * Generates the game world using a randomly selected algorithm
+ * @param {number} width 
+ * @param {number} height 
+ * @returns {Tile[][]} Generated grid
+ */
+function generateWorld(width, height) {
+    const generationType = Math.random() < 0.25 ? 
+        GENERATION_TYPES.SPARSE_WAREHOUSE : 
+        Math.random() < 0.33 ? GENERATION_TYPES.HALLWAY_ROOMS :
+        Math.random() < 0.5 ? GENERATION_TYPES.DRUNKARDS_WALK :
+        GENERATION_TYPES.CELLULAR_AUTOMATA;
+    
+    console.log(`Generating ${generationType} layout...`);
+    
+    let grid;
+    switch (generationType) {
+        case GENERATION_TYPES.SPARSE_WAREHOUSE:
+            grid = generateSparseWarehouse(width, height);
+            break;
+        case GENERATION_TYPES.HALLWAY_ROOMS:
+            grid = generateHallwayRooms(width, height);
+            break;
+        case GENERATION_TYPES.DRUNKARDS_WALK:
+            grid = generateDrunkardsWalk(width, height);
+            break;
+        case GENERATION_TYPES.CELLULAR_AUTOMATA:
+            grid = generateCellularAutomata(width, height);
+            break;
+        default:
+            grid = generateSparseWarehouse(width, height);
+    }
+    
+    // Ensure navigability
+    grid = ensureNavigability(grid);
+    
+    return grid;
+}
+
+// ------------------------------------------------
+// SQUAD GENERATION
+// ------------------------------------------------
+
+/**
+ * Generates a random stat within a given range
+ * @param {number} min 
+ * @param {number} max 
+ * @returns {number} Random stat value
+ */
+function generateStat(min, max) {
+    return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/**
+ * Generates a player squad of 3 units
+ * @param {Tile[][]} grid 
+ * @returns {Unit[]} Array of player units
+ */
+function generatePlayerSquad(grid) {
+    const units = [];
+    const width = grid[0].length;
+    const height = grid.length;
+    
+    // Find a good starting position for the squad
+    let squadCenterX, squadCenterY;
+    let attempts = 0;
+    const maxAttempts = 100;
+    
+    do {
+        squadCenterX = Math.floor(Math.random() * (width - 4)) + 2;
+        squadCenterY = Math.floor(Math.random() * (height - 4)) + 2;
+        attempts++;
+    } while (
+        attempts < maxAttempts && 
+        (grid[squadCenterY][squadCenterX].type === 'obstacle' ||
+         grid[squadCenterY][squadCenterX + 1].type === 'obstacle' ||
+         grid[squadCenterY + 1][squadCenterX].type === 'obstacle')
+    );
+    
+    // Place units in a triangle formation around the center
+    const positions = [
+        { x: squadCenterX, y: squadCenterY },
+        { x: squadCenterX + 1, y: squadCenterY },
+        { x: squadCenterX, y: squadCenterY + 1 }
+    ];
+    
+    // Ensure all positions are valid
+    const validPositions = positions.filter(pos => 
+        pos.x >= 0 && pos.x < width && 
+        pos.y >= 0 && pos.y < height && 
+        grid[pos.y][pos.x].type !== 'obstacle'
+    );
+    
+    // If we can't place all units in formation, place them randomly but close
+    if (validPositions.length < 3) {
+        for (let i = 0; i < 3; i++) {
+            let x, y;
+            do {
+                x = squadCenterX + Math.floor(Math.random() * 3) - 1;
+                y = squadCenterY + Math.floor(Math.random() * 3) - 1;
+                x = Math.max(0, Math.min(width - 1, x));
+                y = Math.max(0, Math.min(height - 1, y));
+            } while (grid[y][x].type === 'obstacle' || 
+                     units.some(unit => unit.x === x && unit.y === y));
+            
+            const unit = createPlayerUnit(i + 1, x, y);
+            units.push(unit);
+        }
+    } else {
+        // Place units in formation
+        for (let i = 0; i < 3; i++) {
+            const pos = validPositions[i];
+            const unit = createPlayerUnit(i + 1, pos.x, pos.y);
+            units.push(unit);
+        }
+    }
+    
+    return units;
+}
+
+/**
+ * Creates a player unit with the given stats
+ * @param {number} id 
+ * @param {number} x 
+ * @param {number} y 
+ * @returns {Unit} Player unit
+ */
+function createPlayerUnit(id, x, y) {
+    const unit = {
+        id,
+        owner: 'player',
+        x,
+        y,
+        maxMovement: generateStat(2, 6), // Reduced from 2-10 to 2-6
+        movementRange: 0, // Will be set to maxMovement
+        shootRange: generateStat(3, 8), // Reduced from 4-15 to 3-8
+        damage: generateStat(5, 20),
+        maxHp: generateStat(30, 60),
+        currentHp: 0, // Will be set to maxHp
+        inCover: false,
+        hasMoved: false,
+        hasAttacked: false,
+        isAnimating: false,
+        animationType: 'none',
+        animationProgress: 0,
+        targetX: -1,
+        targetY: -1,
+        path: [],
+        projectileX: -1,
+        projectileY: -1,
+        projectileTargetX: -1,
+        projectileTargetY: -1,
+        deathParticles: [],
+        isDead: false,
+        pendingDamage: 0,
+        justCompletedShot: false,
+        isVisible: true,
+        lastSeen: 0,
+        lastKnownX: x,
+        lastKnownY: y,
+        aiBehavior: 'aggressive',
+        turnOrder: 0,
+        patrolCenterX: 0,
+        patrolCenterY: 0,
+        patrolRadius: 0,
+        actionFeedbackTimer: 0
+    };
+    
+    // Set current values to max values
+    unit.movementRange = unit.maxMovement;
+    unit.currentHp = unit.maxHp;
+    
+    // Validate unit creation
+    console.log('Player unit created:', unit);
+    
+    return unit;
+}
+
+/**
+ * Generates an enemy squad of 2-7 units
+ * @param {Tile[][]} grid 
+ * @returns {Unit[]} Array of enemy units
+ */
+function generateEnemySquad(grid) {
+    const units = [];
+    const width = grid[0].length;
+    const height = grid.length;
+    const squadSize = generateStat(2, 7);
+    
+    for (let i = 0; i < squadSize; i++) {
+        let x, y;
+        do {
+            x = Math.floor(Math.random() * width);
+            y = Math.floor(Math.random() * height);
+        } while (grid[y][x].type === 'obstacle' || 
+                 units.some(unit => unit.x === x && unit.y === y));
+        
+        const unit = createEnemyUnit(100 + i + 1, x, y);
+        units.push(unit);
+    }
+    
+    return units;
+}
+
+/**
+ * Creates an enemy unit with the given stats
+ * @param {number} id 
+ * @param {number} x 
+ * @param {number} y 
+ * @returns {Unit} Enemy unit
+ */
+function createEnemyUnit(id, x, y) {
+    const aiBehavior = AI_BEHAVIORS[Math.floor(Math.random() * AI_BEHAVIORS.length)];
+    
+    const unit = {
+        id,
+        owner: 'enemy',
+        x,
+        y,
+        maxMovement: generateStat(2, 6), // Reduced from 2-10 to 2-6
+        movementRange: 0, // Will be set to maxMovement
+        shootRange: generateStat(3, 8), // Reduced from 4-15 to 3-8
+        damage: generateStat(5, 10),
+        maxHp: generateStat(10, 20),
+        currentHp: 0, // Will be set to maxHp
+        inCover: false,
+        hasMoved: false,
+        hasAttacked: false,
+        isAnimating: false,
+        animationType: 'none',
+        animationProgress: 0,
+        targetX: -1,
+        targetY: -1,
+        path: [],
+        projectileX: -1,
+        projectileY: -1,
+        projectileTargetX: -1,
+        projectileTargetY: -1,
+        deathParticles: [],
+        isDead: false,
+        pendingDamage: 0,
+        justCompletedShot: false,
+        isVisible: false, // Enemy units start hidden
+        lastSeen: 0,
+        lastKnownX: x,
+        lastKnownY: y,
+        aiBehavior,
+        turnOrder: 0, // Will be set by generateTurnOrder
+        patrolCenterX: x, // Starting position becomes patrol center
+        patrolCenterY: y,
+        patrolRadius: generateStat(3, 8), // Random patrol radius
+        actionFeedbackTimer: 0
+    };
+    
+    // Set current values to max values
+    unit.movementRange = unit.maxMovement;
+    unit.currentHp = unit.maxHp;
+    
+    // Validate unit creation
+    console.log('Enemy unit created:', unit);
+    
+    return unit;
+}
+
+/**
+ * Checks for completed shooting animations and applies damage to targets
+ * @param {Unit[]} units 
+ * @param {Tile[][]} grid
+ * @returns {{units: Unit[], grid: Tile[][], updated: boolean}} Updated units and grid with damage applied
+ */
+function processCompletedShots(units, grid) {
+    let unitsUpdated = false;
+    let gridUpdated = false;
+    let newGrid = grid;
+    
+    const updatedUnits = units.map(unit => {
+        // Check if this unit just completed a shooting animation
+        if (unit.justCompletedShot) {
+            console.log('Processing completed shot for unit:', unit.id);
+            console.log('Projectile target coordinates:', unit.projectileTargetX, unit.projectileTargetY);
+            console.log('All units and their coordinates:');
+            units.forEach(u => console.log(`Unit ${u.id} (${u.owner}): x=${u.x}, y=${u.y}`));
+            
+            // Check if we hit an obstacle
+            if (grid[unit.projectileTargetY] && grid[unit.projectileTargetY][unit.projectileTargetX] && 
+                grid[unit.projectileTargetY][unit.projectileTargetX].type === 'obstacle') {
+                
+                console.log('Shot hit obstacle, applying damage');
+                if (!gridUpdated) {
+                    newGrid = grid.map(row => [...row]);
+                    gridUpdated = true;
+                }
+                
+                // Get the current obstacle
+                const currentObstacle = newGrid[unit.projectileTargetY][unit.projectileTargetX];
+                
+                // Apply damage
+                currentObstacle.health -= 1;
+                currentObstacle.damageFlash = 1.0; // Set flash effect
+                
+                if (currentObstacle.health <= 0) {
+                    // Obstacle destroyed
+                    newGrid[unit.projectileTargetY][unit.projectileTargetX] = {
+                        type: 'floor',
+                        x: unit.projectileTargetX,
+                        y: unit.projectileTargetY,
+                        providesCover: false,
+                        health: 0,
+                        damageFlash: 0,
+                        color: generateObstacleColor()
+                    };
+                    console.log('Obstacle destroyed by shot!');
+                }
+                
+                unitsUpdated = true;
+            } else {
+                // Find the target unit
+                const targetUnit = units.find(u => 
+                    u.x === unit.projectileTargetX && u.y === unit.projectileTargetY
+                );
+                
+                console.log('Target unit found:', targetUnit);
+                console.log('Target pending damage:', targetUnit?.pendingDamage);
+                
+                if (targetUnit && targetUnit.pendingDamage > 0) {
+                    // Apply damage to target
+                    const newHp = Math.max(0, targetUnit.currentHp - targetUnit.pendingDamage);
+                    console.log('Applying damage:', targetUnit.pendingDamage, 'New HP:', newHp);
+                    
+                    if (newHp <= 0 && !targetUnit.isDead) {
+                        // Target died - start death animation
+                        console.log('Target died, starting death animation');
+                        const deadUnit = createDeathParticles({
+                            ...targetUnit,
+                            currentHp: newHp,
+                            pendingDamage: 0
+                        });
+                        
+                        // Update the target unit in the array
+                        const targetIndex = units.findIndex(u => u.id === targetUnit.id);
+                        if (targetIndex !== -1) {
+                            units[targetIndex] = deadUnit;
+                        }
+                        unitsUpdated = true;
+                    } else {
+                        // Target survived - just apply damage
+                        console.log('Target survived with new HP:', newHp);
+                        const updatedTarget = { ...targetUnit, currentHp: newHp, pendingDamage: 0 };
+                        const targetIndex = units.findIndex(u => u.id === targetUnit.id);
+                        units[targetIndex] = updatedTarget;
+                        unitsUpdated = true;
+                    }
+                } else {
+                    console.log('No target found or no pending damage');
+                }
+            }
+            
+            // Clear the flag and return updated shooting unit
+            return {
+                ...unit,
+                justCompletedShot: false,
+                projectileTargetX: -1,
+                projectileTargetY: -1
+            };
+        }
+        return unit;
+    });
+    
+    return {
+        units: unitsUpdated ? cleanupDeadUnits(units) : updatedUnits,
+        grid: newGrid,
+        updated: unitsUpdated || gridUpdated
+    };
+}
+
+// ------------------------------------------------
+// 1. STATE INITIALIZATION (MODEL)
+// ------------------------------------------------
+
+/**
+ * Creates the initial state of the game.
+ * @returns {Model} The initial model.
+ */
+function init() {
+    const { width, height } = calculateGridSize();
+    
+    // Generate world using procedural generation
+    const grid = generateWorld(width, height);
+
+    // Generate squads
+    const playerUnits = generatePlayerSquad(grid);
+    const enemyUnits = generateEnemySquad(grid);
+    let units = [...playerUnits, ...enemyUnits];
+    
+    // Generate turn order for all units
+    const unitsWithTurnOrder = generateTurnOrder(units);
+    
+    // Update cover status for all units
+    unitsWithTurnOrder.forEach(unit => {
+        unit.inCover = checkCover(unit, grid);
+    });
+    
+    // Set initial visibility - enemies start hidden unless they're in line of sight of player units
+    const unitsWithVisibility = updateUnitVisibility(unitsWithTurnOrder, grid);
+    
+    // Debug: Show all obstacle health values
+    console.log('=== GAME INITIALIZATION ===');
+    let obstacleCount = 0;
+    for (let y = 0; y < grid.length; y++) {
+        for (let x = 0; x < grid[y].length; x++) {
+            if (grid[y][x].type === 'obstacle') {
+                obstacleCount++;
+            }
+        }
+    }
+    console.log(`Total obstacles created: ${obstacleCount}`);
+    console.log('Initial currentTurnIndex:', 0);
+    console.log('Initial turn order:', unitsWithVisibility.map(u => ({ id: u.id, owner: u.owner, turnOrder: u.turnOrder })));
+    console.log('First unit in turn order:', unitsWithVisibility.find(u => u.turnOrder === 0));
+    console.log('=== END INITIALIZATION ===');
+    
+    return {
+        grid: grid,
+        units: unitsWithVisibility,
+        selectedUnit: null,
+        uiState: { type: 'AwaitingSelection' },
+        currentTurnIndex: 0
+    };
+}
+
+// ------------------------------------------------
+// 2. LOGIC (UPDATE)
+// ------------------------------------------------
+
+/**
+ * Handles all state changes in the application.
+ * @param {object} msg - The action message.
+ * @param {Model} model - The current state.
+ * @returns {Model} The new state.
+ */
+function update(msg, model) {
+    // Check if it's a player unit's turn
+    const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+    const isPlayerTurn = currentUnit && currentUnit.owner === 'player';
+    
+    if (!isPlayerTurn) {
+        return model;
+    }
+
+    switch (msg.type) {
+        case 'TILE_CLICKED': {
+            const { x, y } = msg.payload;
+            const unitAtTile = model.units.find(u => u.x === x && u.y === y);
+            
+            console.log('=== TILE CLICK ===');
+            console.log('Clicked at:', x, y);
+            console.log('Unit at tile:', unitAtTile ? `${unitAtTile.owner} ${unitAtTile.id}` : 'none');
+            
+            // Check if it's a player unit's turn
+            const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+            console.log('Current unit in turn order:', currentUnit ? `${currentUnit.owner} ${currentUnit.id}` : 'none');
+            console.log('Is player turn:', currentUnit && currentUnit.owner === 'player');
+            
+            if (!currentUnit || currentUnit.owner !== 'player') {
+                console.log('Not a player unit\'s turn, returning');
+                return model; // Not a player unit's turn
+            }
+
+            if (model.uiState.type === 'UnitSelected') {
+                const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+                
+                // Only allow actions if the selected unit is the current unit in turn order
+                if (selectedUnit.id !== currentUnit.id) {
+                    return model; // Can't act with a different unit
+                }
+                
+                if (!selectedUnit.hasMoved) {
+                    // Movement phase
+                    const distance = Math.abs(selectedUnit.x - x) + Math.abs(selectedUnit.y - y);
+                    const isMoveValid = distance > 0 && 
+                                      distance <= selectedUnit.movementRange && 
+                                      !unitAtTile &&
+                                      model.grid[y][x].type !== 'obstacle' &&
+                                      !isPathBlocked(selectedUnit.x, selectedUnit.y, x, y, model.grid) &&
+                                      !selectedUnit.hasMoved;
+                    
+                    if (isMoveValid) {
+                        const newUnits = model.units.map(unit =>
+                            unit.id === selectedUnit.id
+                                ? startMovementAnimation(unit, x, y, model.grid)
+                                : unit
+                        );
+                        
+                        // Update cover status for moved unit
+                        newUnits.forEach(unit => {
+                            unit.inCover = checkCover(unit, model.grid);
+                        });
+                        
+                        // Update unit visibility after movement
+                        const unitsWithVisibility = updateUnitVisibility(newUnits, model.grid);
+                        
+                        return { ...model, units: unitsWithVisibility, uiState: { type: 'AwaitingSelection' } };
+                    }
+                } else if (!selectedUnit.hasAttacked) {
+                    // Attack phase - can attack any unit (including friendly fire)
+                    if (unitAtTile) {
+                        // Can't attack enemy units that are not visible
+                        if (unitAtTile.owner === 'enemy' && !unitAtTile.isVisible) {
+                            return model; // Invalid attack - enemy not visible
+                        }
+                        
+                        // Can't attack units that are at their last known position (ghosts)
+                        if (unitAtTile.owner === 'enemy' && 
+                            unitAtTile.lastSeen && 
+                            (unitAtTile.x !== unitAtTile.lastKnownX || unitAtTile.y !== unitAtTile.lastKnownY)) {
+                            return model; // Invalid attack - attacking ghost position
+                        }
+                        
+                        const distance = Math.abs(selectedUnit.x - unitAtTile.x) + Math.abs(selectedUnit.y - unitAtTile.y);
+                        const isAttackValid = distance <= selectedUnit.shootRange && !selectedUnit.hasAttacked;
+                        
+                        if (isAttackValid) {
+                            // Check line of sight
+                            const los = checkLineOfSight(
+                                selectedUnit.x, selectedUnit.y,
+                                unitAtTile.x, unitAtTile.y,
+                                model.grid, model.units
+                            );
+                            
+                            if (los.blocked) {
+                                // Line of sight is blocked - handle stray shot
+                                let damage = selectedUnit.damage;
+                                if (los.obstacleX !== null && los.obstacleY !== null) {
+                                    // Hit an obstacle
+                                    console.log('Stray shot hit obstacle at:', los.obstacleX, los.obstacleY);
+                                    const newGrid = model.grid.map(row => [...row]);
+                                    
+                                    // Get the current obstacle
+                                    const currentObstacle = newGrid[los.obstacleY][los.obstacleX];
+                                    
+                                    // Apply damage
+                                    currentObstacle.health -= 1;
+                                    currentObstacle.damageFlash = 1.0; // Set flash effect
+                                    
+                                    if (currentObstacle.health <= 0) {
+                                        // Obstacle destroyed
+                                        newGrid[los.obstacleY][los.obstacleX] = {
+                                            type: 'floor',
+                                            x: los.obstacleX,
+                                            y: los.obstacleY,
+                                            providesCover: false,
+                                            health: 0,
+                                            damageFlash: 0,
+                                            color: generateObstacleColor()
+                                        };
+                                        console.log('Obstacle destroyed by stray shot!');
+                                    }
+                                    
+                                    // Start shooting animation towards the obstacle
+                                    const newUnits = model.units.map(unit =>
+                                        unit.id === selectedUnit.id
+                                            ? startShootingAnimation(unit, los.obstacleX, los.obstacleY)
+                                            : unit
+                                    );
+                                    
+                                    return { 
+                                        ...model, 
+                                        grid: newGrid, 
+                                        units: newUnits, 
+                                        uiState: { type: 'AwaitingSelection' } 
+                                    };
+                                } else if (los.blocker) {
+                                    // Hit a blocking unit
+                                    console.log('Stray shot hit blocking unit:', los.blocker.id);
+                                    const newUnits = model.units.map(unit =>
+                                        unit.id === los.blocker.id
+                                            ? { ...unit, pendingDamage: damage }
+                                            : unit.id === selectedUnit.id
+                                            ? startShootingAnimation(unit, los.blocker.x, los.blocker.y)
+                                            : unit
+                                    );
+                                    
+                                    return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } };
+                                }
+                            } else {
+                                // Clear line of sight - proceed with normal attack
+                                // Calculate damage (cover reduces damage by 50%)
+                                let damage = selectedUnit.damage;
+                                if (unitAtTile.inCover) {
+                                    damage = Math.floor(damage * 0.5);
+                                }
+                                
+                                // Start shooting animation and mark target for damage
+                                const newUnits = model.units.map(unit =>
+                                    unit.id === unitAtTile.id
+                                        ? { ...unit, pendingDamage: damage } // Mark target for damage when projectile hits
+                                        : unit.id === selectedUnit.id
+                                        ? startShootingAnimation(unit, unitAtTile.x, unitAtTile.y)
+                                        : unit
+                                );
+                                
+                                return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } };
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Check if we can target an obstacle directly
+            if (model.uiState.type === 'UnitSelected' && model.grid[y][x].type === 'obstacle') {
+                const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+                
+                if (selectedUnit && !selectedUnit.hasAttacked) {
+                    const distance = Math.abs(selectedUnit.x - x) + Math.abs(selectedUnit.y - y);
+                    const isAttackValid = distance <= selectedUnit.shootRange && !selectedUnit.hasAttacked;
+                    
+                    if (isAttackValid) {
+                        console.log('Direct obstacle attack at:', x, y);
+                        
+                        // Start shooting animation towards the obstacle
+                        const newUnits = model.units.map(unit =>
+                            unit.id === selectedUnit.id
+                                ? startShootingAnimation(unit, x, y)
+                                : unit
+                        );
+                        
+                        return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } };
+                    }
+                }
+            }
+
+            if (unitAtTile && unitAtTile.owner === 'player' && 
+                (!unitAtTile.hasMoved || !unitAtTile.hasAttacked)) {
+                
+                console.log('Attempting to select player unit:', unitAtTile.id);
+                console.log('Unit state:', {
+                    hasMoved: unitAtTile.hasMoved,
+                    hasAttacked: unitAtTile.hasAttacked
+                });
+                
+                // Only allow selecting the current unit in turn order
+                if (unitAtTile.id !== currentUnit.id) {
+                    console.log('Cannot select unit - not current unit in turn order');
+                    return model; // Can't select a different unit
+                }
+                
+                console.log('Successfully selecting unit:', unitAtTile.id);
+                return { ...model, uiState: { type: 'UnitSelected', unitId: unitAtTile.id } };
+            }
+
+            // Can't select enemy units that are not visible
+            if (unitAtTile && unitAtTile.owner === 'enemy' && !unitAtTile.isVisible) {
+                return model; // Invalid selection - enemy not visible
+            }
+
+            return { ...model, uiState: { type: 'AwaitingSelection' } };
+        }
+        case 'SKIP_MOVEMENT': {
+            if (model.uiState.type === 'UnitSelected') {
+                const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+                const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+                
+                // Only allow skipping if it's the current unit's turn
+                if (!currentUnit || selectedUnit.id !== currentUnit.id) {
+                    return model;
+                }
+                
+                const newUnits = model.units.map(unit =>
+                    unit.id === selectedUnit.id
+                        ? { ...unit, hasMoved: true }
+                        : unit
+                );
+                return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } };
+            }
+            return model;
+        }
+        case 'SKIP_ATTACK': {
+            if (model.uiState.type === 'UnitSelected') {
+                const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+                const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+                
+                // Only allow skipping if it's the current unit's turn
+                if (!currentUnit || selectedUnit.id !== currentUnit.id) {
+                    return model;
+                }
+                
+                const newUnits = model.units.map(unit =>
+                    unit.id === selectedUnit.id
+                        ? { ...unit, hasAttacked: true }
+                        : unit
+                );
+                return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } };
+            }
+            return model;
+        }
+        case 'END_TURN_CLICKED': {
+            // Advance to next turn in turn order
+            return advanceTurn(model);
+        }
+        default:
+            return model;
+    }
+}
+
+// ------------------------------------------------
+// 3. RENDERER (VIEW)
+// ------------------------------------------------
+
+/**
+ * Draws the grid tiles and obstacles.
+ * @param {Model} model
+ * @param {CanvasRenderingContext2D} ctx
+ */
+function drawGrid(model, ctx) {
+    const gridWidth = model.grid[0].length;
+    const gridHeight = model.grid.length;
+
+    // Draw tiles
+    for (let y = 0; y < gridHeight; y++) {
+        for (let x = 0; x < gridWidth; x++) {
+            const tile = model.grid[y][x];
+            const tileX = x * TILE_SIZE;
+            const tileY = y * TILE_SIZE;
+            
+            // Fill tile
+            ctx.fillStyle = tile.type === 'obstacle' ? tile.color : COLORS.floor;
+            ctx.fillRect(tileX, tileY, TILE_SIZE, TILE_SIZE);
+            
+            // Draw obstacle health indicator
+            if (tile.type === 'obstacle' && tile.health < OBSTACLE_MAX_HEALTH) {
+                // Create a more gradual color fade based on health using grey/black/blue palette
+                const healthRatio = tile.health / OBSTACLE_MAX_HEALTH;
+                let damageColor;
+                
+                if (healthRatio > 0.83) {
+                    // 5 HP - slight blue tint
+                    damageColor = `rgba(52, 73, 94, ${0.2 + (1 - healthRatio) * 0.3})`;
+                } else if (healthRatio > 0.66) {
+                    // 4 HP - more blue-grey
+                    damageColor = `rgba(44, 62, 80, ${0.3 + (1 - healthRatio) * 0.4})`;
+                } else if (healthRatio > 0.5) {
+                    // 3 HP - darker blue-grey
+                    damageColor = `rgba(36, 51, 66, ${0.4 + (1 - healthRatio) * 0.4})`;
+                } else if (healthRatio > 0.33) {
+                    // 2 HP - very dark blue-grey
+                    damageColor = `rgba(28, 40, 52, ${0.5 + (1 - healthRatio) * 0.4})`;
+                } else {
+                    // 1 HP - almost black with blue tint
+                    damageColor = `rgba(20, 29, 38, ${0.6 + (1 - healthRatio) * 0.4})`;
+                }
+                
+                ctx.fillStyle = damageColor;
+                ctx.fillRect(tileX + 2, tileY + 2, TILE_SIZE - 4, TILE_SIZE - 4);
+            }
+            
+            // Draw damage flash effect if obstacle was recently hit
+            if (tile.type === 'obstacle' && tile.damageFlash && tile.damageFlash > 0) {
+                ctx.fillStyle = `rgba(52, 152, 219, ${tile.damageFlash * 0.6})`; // Blue-white flash
+                ctx.fillRect(tileX, tileY, TILE_SIZE, TILE_SIZE);
+            }
+            
+            // Draw grid lines
+            ctx.strokeStyle = COLORS.gridLine;
+            ctx.lineWidth = 1;
+            ctx.strokeRect(tileX, tileY, TILE_SIZE, TILE_SIZE);
+        }
+    }
+}
+
+/**
+ * Draws highlights for valid moves and attacks.
+ * @param {Model} model
+ * @param {CanvasRenderingContext2D} ctx
+ */
+function drawHighlights(model, ctx) {
+    if (model.uiState.type === 'AwaitingSelection') return;
+
+    const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+    const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+    
+    if (!selectedUnit || !currentUnit) return;
+    
+    // Only show highlights if the selected unit is the current unit in turn order
+    if (selectedUnit.id !== currentUnit.id) return;
+
+    if (model.uiState.type === 'UnitSelected' && !selectedUnit.hasMoved) {
+        // Show movement range
+        ctx.fillStyle = COLORS.moveHighlight;
+        for (const row of model.grid) {
+            for (const tile of row) {
+                if (tile.type === 'obstacle') continue;
+                
+                const distance = Math.abs(selectedUnit.x - tile.x) + Math.abs(selectedUnit.y - tile.y);
+                if (distance > 0 && distance <= selectedUnit.movementRange) {
+                    // Check if path is not blocked
+                    if (!isPathBlocked(selectedUnit.x, selectedUnit.y, tile.x, tile.y, model.grid)) {
+                        ctx.fillRect(tile.x * TILE_SIZE, tile.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
+                    }
+                }
+            }
+        }
+    }
+
+    if (model.uiState.type === 'UnitSelected' && selectedUnit.hasMoved && !selectedUnit.hasAttacked) {
+        // Show attack range
+        ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
+        for (const row of model.grid) {
+            for (const tile of row) {
+                const distance = Math.abs(selectedUnit.x - tile.x) + Math.abs(selectedUnit.y - tile.y);
+                if (distance > 0 && distance <= selectedUnit.shootRange) {
+                    ctx.fillRect(tile.x * TILE_SIZE, tile.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
+                }
+            }
+        }
+        
+        // Highlight potential targets
+        ctx.fillStyle = 'rgba(255, 0, 0, 0.6)';
+        model.units.forEach(unit => {
+            if (unit.id !== selectedUnit.id && 
+                (unit.owner === 'player' || (unit.owner === 'enemy' && unit.isVisible && 
+                 unit.x === unit.lastKnownX && unit.y === unit.lastKnownY))) {
+                const distance = Math.abs(selectedUnit.x - unit.x) + Math.abs(selectedUnit.y - unit.y);
+                if (distance <= selectedUnit.shootRange) {
+                    ctx.beginPath();
+                    ctx.arc(unit.x * TILE_SIZE + TILE_SIZE / 2, unit.y * TILE_SIZE + TILE_SIZE / 2, UNIT_RADIUS + 5, 0, Math.PI * 2);
+                    ctx.fill();
+                }
+            }
+        });
+        
+        // Highlight targetable obstacles
+        ctx.fillStyle = 'rgba(52, 152, 219, 0.4)'; // Blue highlight for targetable obstacles
+        for (const row of model.grid) {
+            for (const tile of row) {
+                if (tile.type === 'obstacle') {
+                    const distance = Math.abs(selectedUnit.x - tile.x) + Math.abs(selectedUnit.y - tile.y);
+                    if (distance > 0 && distance <= selectedUnit.shootRange) {
+                        ctx.fillRect(tile.x * TILE_SIZE + 2, tile.y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4);
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Draws the units on the grid.
+ * @param {Model} model
+ * @param {CanvasRenderingContext2D} ctx
+ */
+function drawUnits(model, ctx) {
+    model.units.forEach(unit => {
+        if (unit.isDead) return;
+        
+        // Sanitize unit state to prevent crashes
+        const sanitizedUnit = sanitizeUnitState(unit);
+        
+        let drawPosition;
+        if (sanitizedUnit.owner === 'enemy' && !sanitizedUnit.isVisible && sanitizedUnit.lastSeen) {
+            // Draw at last known position for invisible enemy units
+            drawPosition = { x: sanitizedUnit.lastKnownX, y: sanitizedUnit.lastKnownY };
+        } else {
+            // Draw at current position for visible units
+            try {
+                drawPosition = getUnitPosition(sanitizedUnit);
+            } catch (error) {
+                console.warn('Error getting position for unit', sanitizedUnit.id, ':', error);
+                drawPosition = { x: sanitizedUnit.x, y: sanitizedUnit.y };
+            }
+        }
+        
+        if (!drawPosition || typeof drawPosition.x !== 'number' || typeof drawPosition.y !== 'number') {
+            console.warn('Invalid position for unit:', sanitizedUnit.id, drawPosition, 'skipping render');
+            return; // Skip this unit
+        }
+        
+        const { x, y } = drawPosition;
+        const centerX = x * TILE_SIZE + TILE_SIZE / 2;
+        const centerY = y * TILE_SIZE + TILE_SIZE / 2;
+
+        // For invisible enemy units, draw as a ghost
+        if (sanitizedUnit.owner === 'enemy' && !sanitizedUnit.isVisible) {
+            ctx.globalAlpha = 0.3; // Make them semi-transparent
+            ctx.fillStyle = '#666'; // Grey color for ghosts
+        } else {
+            ctx.globalAlpha = 1.0; // Full opacity for visible units
+            ctx.fillStyle = sanitizedUnit.owner === 'player' ? COLORS.player : COLORS.enemy;
+        }
+
+    // Draw cover indicator
+    if (sanitizedUnit.inCover) {
+        ctx.fillStyle = COLORS.coverHighlight;
+        ctx.beginPath();
+        ctx.arc(centerX, centerY, UNIT_RADIUS + 5, 0, Math.PI * 2);
+        ctx.fill();
+    }
+
+    // Draw shooting animation effect
+    if (sanitizedUnit.isAnimating && sanitizedUnit.animationType === 'shooting') {
+        const pulseSize = UNIT_RADIUS + 10 + Math.sin(sanitizedUnit.animationProgress * Math.PI * 4) * 5;
+        ctx.fillStyle = 'rgba(255, 255, 0, 0.3)';
+        ctx.beginPath();
+        ctx.arc(centerX, centerY, pulseSize, 0, Math.PI * 2);
+        ctx.fill();
+        
+        // Draw projectile
+        const projectilePos = getProjectilePosition(sanitizedUnit);
+        if (projectilePos) {
+            // Draw projectile trail
+            const trailLength = 15;
+            const angle = Math.atan2(projectilePos.y - centerY, projectilePos.x - centerX);
+            const trailStartX = projectilePos.x - Math.cos(angle) * trailLength;
+            const trailStartY = projectilePos.y - Math.sin(angle) * trailLength;
+            
+            // Gradient for trail effect
+            const gradient = ctx.createLinearGradient(trailStartX, trailStartY, projectilePos.x, projectilePos.y);
+            gradient.addColorStop(0, 'rgba(255, 255, 0, 0)');
+            gradient.addColorStop(0.5, 'rgba(255, 255, 0, 0.8)');
+            gradient.addColorStop(1, 'rgba(255, 255, 0, 1)');
+            
+            ctx.strokeStyle = gradient;
+            ctx.lineWidth = 3;
+            ctx.lineCap = 'round';
+            ctx.beginPath();
+            ctx.moveTo(trailStartX, trailStartY);
+            ctx.lineTo(projectilePos.x, projectilePos.y);
+            ctx.stroke();
+            
+            // Draw projectile bolt
+            ctx.fillStyle = '#FFFF00';
+            ctx.beginPath();
+            ctx.arc(projectilePos.x, projectilePos.y, 4, 0, Math.PI * 2);
+            ctx.fill();
+            
+            // Add glow effect
+            ctx.shadowColor = '#FFFF00';
+            ctx.shadowBlur = 8;
+            ctx.beginPath();
+            ctx.arc(projectilePos.x, projectilePos.y, 2, 0, Math.PI * 2);
+            ctx.fill();
+            ctx.shadowBlur = 0;
+            
+            // Draw impact effect when projectile reaches target
+            if (sanitizedUnit.animationProgress > 0.8) {
+                const targetCenterX = sanitizedUnit.projectileTargetX * TILE_SIZE + TILE_SIZE / 2;
+                const targetCenterY = sanitizedUnit.projectileTargetY * TILE_SIZE + TILE_SIZE / 2;
+                
+                // Impact explosion
+                const explosionSize = (sanitizedUnit.animationProgress - 0.8) * 20;
+                const alpha = 1 - (sanitizedUnit.animationProgress - 0.8) * 5;
+                
+                ctx.fillStyle = `rgba(255, 100, 0, ${alpha})`;
+                ctx.beginPath();
+                ctx.arc(targetCenterX, targetCenterY, explosionSize, 0, Math.PI * 2);
+                ctx.fill();
+                
+                // Inner explosion
+                ctx.fillStyle = `rgba(255, 255, 0, ${alpha * 0.8})`;
+                ctx.beginPath();
+                ctx.arc(targetCenterX, targetCenterY, explosionSize * 0.6, 0, Math.PI * 2);
+                ctx.fill();
+            }
+        }
+    }
+
+    // Draw death particles
+    if (sanitizedUnit.isAnimating && sanitizedUnit.animationType === 'dying' && sanitizedUnit.deathParticles) {
+        sanitizedUnit.deathParticles.forEach(particle => {
+            const alpha = particle.life / particle.maxLife;
+            const size = particle.size * alpha;
+            
+            ctx.fillStyle = particle.color;
+            ctx.globalAlpha = alpha;
+            ctx.beginPath();
+            ctx.arc(particle.x, particle.y, size, 0, Math.PI * 2);
+            ctx.fill();
+            
+            // Add glow effect
+            ctx.shadowColor = particle.color;
+            ctx.shadowBlur = size * 2;
+            ctx.beginPath();
+            ctx.arc(particle.x, particle.y, size * 0.5, 0, Math.PI * 2);
+            ctx.fill();
+            ctx.shadowBlur = 0;
+        });
+        ctx.globalAlpha = 1; // Reset global alpha
+    }
+
+    // Draw targeting reticle for units in attack range
+    if (model.uiState.type === 'UnitSelected' && 
+        model.uiState.unitId !== sanitizedUnit.id &&
+        sanitizedUnit.owner !== 'player') {
+        const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+        if (selectedUnit && selectedUnit.hasMoved && !selectedUnit.hasAttacked) {
+            const distance = Math.abs(selectedUnit.x - sanitizedUnit.x) + Math.abs(selectedUnit.y - sanitizedUnit.y);
+            if (distance <= selectedUnit.shootRange) {
+                // Draw targeting reticle
+                ctx.strokeStyle = '#FF0000';
+                ctx.lineWidth = 2;
+                ctx.setLineDash([5, 5]);
+                
+                const reticleSize = UNIT_RADIUS + 8;
+                ctx.beginPath();
+                ctx.arc(centerX, centerY, reticleSize, 0, Math.PI * 2);
+                ctx.stroke();
+                
+                // Draw crosshairs
+                ctx.beginPath();
+                ctx.moveTo(centerX - reticleSize - 5, centerY);
+                ctx.lineTo(centerX - reticleSize + 5, centerY);
+                ctx.moveTo(centerX + reticleSize - 5, centerY);
+                ctx.lineTo(centerX + reticleSize + 5, centerY);
+                ctx.moveTo(centerX, centerY - reticleSize - 5);
+                ctx.lineTo(centerX, centerY - reticleSize + 5);
+                ctx.moveTo(centerX, centerY + reticleSize - 5);
+                ctx.lineTo(centerX, centerY + reticleSize + 5);
+                ctx.stroke();
+                
+                ctx.setLineDash([]);
+            }
+        }
+    }
+
+    // Draw unit
+    ctx.fillStyle = sanitizedUnit.owner === 'player' ? COLORS.player : COLORS.enemy;
+    ctx.strokeStyle = (model.uiState.type === 'UnitSelected' && model.uiState.unitId === sanitizedUnit.id)
+        ? COLORS.selectedBorder
+        : COLORS.unitBorder;
+    ctx.lineWidth = 2;
+
+    // Add pending damage indicator
+    if (sanitizedUnit.pendingDamage > 0) {
+        ctx.strokeStyle = '#FF0000';
+        ctx.lineWidth = 4;
+    }
+
+    ctx.beginPath();
+    ctx.arc(centerX, centerY, UNIT_RADIUS, 0, Math.PI * 2);
+    ctx.fill();
+    ctx.stroke();
+
+    // Draw HP bar
+    const hpBarWidth = UNIT_RADIUS * 2;
+    const hpBarHeight = 4;
+    const hpBarX = centerX - hpBarWidth / 2;
+    const hpBarY = centerY - UNIT_RADIUS - 10;
+    
+    // Background
+    ctx.fillStyle = '#333';
+    ctx.fillRect(hpBarX, hpBarY, hpBarWidth, hpBarHeight);
+    
+    // HP bar
+    const hpPercentage = sanitizedUnit.currentHp / sanitizedUnit.maxHp;
+    ctx.fillStyle = hpPercentage > 0.5 ? '#4CAF50' : hpPercentage > 0.25 ? '#FF9800' : '#F44336';
+    ctx.fillRect(hpBarX, hpBarY, hpBarWidth * hpPercentage, hpBarHeight);
+    
+    // HP text
+    ctx.fillStyle = '#fff';
+    ctx.font = '10px Arial';
+    ctx.textAlign = 'center';
+    ctx.fillText(`${sanitizedUnit.currentHp}/${sanitizedUnit.maxHp}`, centerX, hpBarY - 2);
+    
+    // Draw action indicators
+    if (sanitizedUnit.owner === 'player') {
+        // Movement indicator
+        if (!sanitizedUnit.hasMoved) {
+            ctx.fillStyle = '#4CAF50';
+            ctx.beginPath();
+            ctx.arc(centerX - 8, centerY + UNIT_RADIUS + 5, 3, 0, Math.PI * 2);
+            ctx.fill();
+        }
+        
+        // Attack indicator
+        if (!sanitizedUnit.hasAttacked) {
+            ctx.fillStyle = sanitizedUnit.hasMoved ? '#FF0000' : '#FF9800'; // Red if ready to attack, orange if waiting
+            ctx.beginPath();
+            ctx.arc(centerX + 8, centerY + UNIT_RADIUS + 5, 3, 0, Math.PI * 2);
+            ctx.fill();
+        }
+        
+        // Phase indicator text
+        if (model.uiState.type === 'UnitSelected' && model.uiState.unitId === sanitizedUnit.id) {
+            ctx.fillStyle = '#fff';
+            ctx.font = '12px Arial';
+            ctx.textAlign = 'center';
+            if (!sanitizedUnit.hasMoved) {
+                ctx.fillText('MOVE', centerX, centerY - UNIT_RADIUS - 15);
+            } else if (!sanitizedUnit.hasAttacked) {
+                ctx.fillText('ATTACK', centerX, centerY - UNIT_RADIUS - 15);
+            }
+        }
+    }
+    
+    // Draw turn indicator for current unit
+    const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+    if (currentUnit && currentUnit.id === sanitizedUnit.id) {
+        // Draw turn indicator ring
+        ctx.strokeStyle = sanitizedUnit.owner === 'player' ? '#00FF00' : '#FF0000';
+        ctx.lineWidth = 3;
+        ctx.beginPath();
+        ctx.arc(centerX, centerY, UNIT_RADIUS + 8, 0, Math.PI * 2);
+        ctx.stroke();
+        
+        // Draw turn indicator text
+        ctx.fillStyle = sanitizedUnit.owner === 'player' ? '#00FF00' : '#FF0000';
+        ctx.font = '14px Arial';
+        ctx.textAlign = 'center';
+        ctx.fillText(sanitizedUnit.owner === 'player' ? 'YOUR TURN' : 'ENEMY TURN', centerX, centerY + UNIT_RADIUS + 25);
+        
+        // For player units, add a subtle glow to show they're selectable
+        if (sanitizedUnit.owner === 'player' && !sanitizedUnit.hasMoved && !sanitizedUnit.hasAttacked) {
+            ctx.shadowColor = '#00FF00';
+            ctx.shadowBlur = 15;
+            ctx.beginPath();
+            ctx.arc(centerX, centerY, UNIT_RADIUS + 2, 0, Math.PI * 2);
+            ctx.stroke();
+            ctx.shadowBlur = 0;
+        }
+        
+        // For enemy units, show action feedback
+        if (sanitizedUnit.owner === 'enemy' && sanitizedUnit.actionFeedbackTimer > 0) {
+            // Draw action feedback ring
+            const feedbackAlpha = sanitizedUnit.actionFeedbackTimer / ENEMY_ACTION_FEEDBACK_DURATION;
+            ctx.strokeStyle = `rgba(255, 255, 0, ${feedbackAlpha})`;
+            ctx.lineWidth = 4;
+            ctx.beginPath();
+            ctx.arc(centerX, centerY, UNIT_RADIUS + 15, 0, Math.PI * 2);
+            ctx.stroke();
+            
+            // Draw action feedback text
+            ctx.fillStyle = `rgba(255, 255, 0, ${feedbackAlpha})`;
+            ctx.font = '12px Arial';
+            ctx.textAlign = 'center';
+            
+            if (sanitizedUnit.isAnimating) {
+                if (sanitizedUnit.animationType === 'moving') {
+                    ctx.fillText('MOVING...', centerX, centerY + UNIT_RADIUS + 40);
+                } else if (sanitizedUnit.animationType === 'shooting') {
+                    ctx.fillText('ATTACKING...', centerX, centerY + UNIT_RADIUS + 40);
+                }
+            } else {
+                ctx.fillText('THINKING...', centerX, centerY + UNIT_RADIUS + 40);
+            }
+        }
+        
+        // Show thinking indicator for current enemy unit even without action feedback
+        if (sanitizedUnit.owner === 'enemy' && currentUnit && currentUnit.id === sanitizedUnit.id && 
+            !sanitizedUnit.actionFeedbackTimer && !sanitizedUnit.isAnimating) {
+            // Draw subtle thinking indicator
+            ctx.strokeStyle = 'rgba(255, 255, 0, 0.3)';
+            ctx.lineWidth = 2;
+            ctx.beginPath();
+            ctx.arc(centerX, centerY, UNIT_RADIUS + 12, 0, Math.PI * 2);
+            ctx.stroke();
+            
+            // Draw thinking text
+            ctx.fillStyle = 'rgba(255, 255, 0, 0.5)';
+            ctx.font = '10px Arial';
+            ctx.textAlign = 'center';
+            ctx.fillText('THINKING...', centerX, centerY + UNIT_RADIUS + 35);
+        }
+    }
+    
+    // Draw movement path preview
+    if (sanitizedUnit.isAnimating && sanitizedUnit.animationType === 'moving' && sanitizedUnit.path.length > 0) {
+        ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)';
+        ctx.lineWidth = 2;
+        ctx.setLineDash([5, 5]);
+        
+        ctx.beginPath();
+        ctx.moveTo(sanitizedUnit.x * TILE_SIZE + TILE_SIZE / 2, sanitizedUnit.y * TILE_SIZE + TILE_SIZE / 2);
+        
+        for (const pathPoint of sanitizedUnit.path) {
+            ctx.lineTo(pathPoint.x * TILE_SIZE + TILE_SIZE / 2, pathPoint.y * TILE_SIZE + TILE_SIZE / 2);
+        }
+        
+                 ctx.stroke();
+         ctx.setLineDash([]);
+     }
+ });
+     
+     // Reset global alpha to ensure proper rendering
+     ctx.globalAlpha = 1.0;
+ }
+
+/**
+ * Renders the entire game state to the canvas.
+ * @param {Model} model - The current state to render.
+ * @param {HTMLCanvasElement} canvas
+ * @param {CanvasRenderingContext2D} ctx
+ */
+function view(model, canvas, ctx) {
+    // Pre-condition: canvas and context are valid.
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    
+    drawGrid(model, ctx);
+    drawHighlights(model, ctx);
+    drawUnits(model, ctx);
+    drawFogOfWar(model, ctx); // Add fog of war effect
+    drawStatusMessage(model, ctx); // Add status message
+    drawVisibilityRanges(model, ctx); // Add visibility ranges
+    // Post-condition: canvas displays the current model state.
+}
+
+/**
+ * Draws status messages at the top of the screen
+ * @param {Model} model 
+ * @param {HTMLCanvasElement} ctx 
+ */
+function drawStatusMessage(model, ctx) {
+    const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+    if (!currentUnit) return;
+    
+    // Draw status message at top of screen
+    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+    ctx.fillRect(0, 0, ctx.canvas.width, 40);
+    
+    ctx.fillStyle = '#FFFFFF';
+    ctx.font = '16px Arial';
+    ctx.textAlign = 'center';
+    
+    if (currentUnit.owner === 'enemy') {
+        if (currentUnit.isAnimating) {
+            if (currentUnit.animationType === 'moving') {
+                ctx.fillText(`Enemy ${currentUnit.id} is moving...`, ctx.canvas.width / 2, 25);
+            } else if (currentUnit.animationType === 'shooting') {
+                ctx.fillText(`Enemy ${currentUnit.id} is attacking...`, ctx.canvas.width / 2, 25);
+            }
+        } else {
+            ctx.fillText(`Enemy ${currentUnit.id}'s turn`, ctx.canvas.width / 2, 25);
+        }
+    } else {
+        ctx.fillText(`Player ${currentUnit.id}'s turn - Move and Attack`, ctx.canvas.width / 2, 25);
+    }
+}
+
+/**
+ * Draws visibility range indicators around player units
+ * @param {Model} model 
+ * @param {CanvasRenderingContext2D} ctx 
+ */
+function drawVisibilityRanges(model, ctx) {
+    model.units.forEach(unit => {
+        if (unit.owner === 'player' && !unit.isDead) {
+            const centerX = unit.x * TILE_SIZE + TILE_SIZE / 2;
+            const centerY = unit.y * TILE_SIZE + TILE_SIZE / 2;
+            
+            // Draw visibility range circle
+            ctx.strokeStyle = 'rgba(0, 255, 255, 0.3)'; // Cyan with transparency
+            ctx.lineWidth = 1;
+            ctx.setLineDash([3, 3]); // Dashed line
+            
+            ctx.beginPath();
+            ctx.arc(centerX, centerY, MAX_VISIBILITY_RANGE * TILE_SIZE, 0, Math.PI * 2);
+            ctx.stroke();
+            
+            ctx.setLineDash([]); // Reset line dash
+        }
+    });
+}
+
+// ------------------------------------------------
+// 4. MAIN APPLICATION LOOP
+// ------------------------------------------------
+
+/**
+ * The main driver for the application.
+ */
+function App() {
+    const canvas = document.getElementById('gameCanvas');
+    const endTurnBtn = document.getElementById('endTurnBtn');
+    const skipMovementBtn = document.getElementById('skipMovementBtn');
+    const skipAttackBtn = document.getElementById('skipAttackBtn');
+    
+    if (!canvas || !endTurnBtn || !skipMovementBtn || !skipAttackBtn) return;
+    
+    const ctx = canvas.getContext('2d');
+    let model = init();
+    let lastTime = 0;
+    let animationId;
+    
+    // Set canvas dimensions based on model
+    canvas.width = model.grid[0].length * TILE_SIZE;
+    canvas.height = model.grid.length * TILE_SIZE;
+
+    /**
+     * Updates button states based on current game state
+     */
+    function updateButtonStates() {
+        const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+        const isPlayerTurn = currentUnit && currentUnit.owner === 'player';
+        
+        // Only enable end turn button if it's a player turn and they've completed their actions
+        if (isPlayerTurn) {
+            const hasUnfinishedActions = !currentUnit.hasMoved || !currentUnit.hasAttacked;
+            const hasAnimatingUnits = model.units.some(u => u.isAnimating);
+            endTurnBtn.disabled = hasUnfinishedActions || hasAnimatingUnits;
+        } else {
+            endTurnBtn.disabled = true; // Disable during enemy turns
+        }
+        
+        // Only enable skip buttons if it's the current unit's turn and they're selected
+        if (model.uiState.type === 'UnitSelected' && isPlayerTurn) {
+            const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
+            if (selectedUnit && selectedUnit.id === currentUnit.id) {
+                skipMovementBtn.disabled = selectedUnit.hasMoved || selectedUnit.isAnimating;
+                skipAttackBtn.disabled = selectedUnit.hasAttacked || selectedUnit.isAnimating;
+            } else {
+                skipMovementBtn.disabled = true;
+                skipAttackBtn.disabled = true;
+            }
+        } else {
+            skipMovementBtn.disabled = true;
+            skipAttackBtn.disabled = true;
+        }
+    }
+
+    /**
+     * Game loop for animations
+     */
+    function gameLoop(currentTime) {
+        const deltaTime = currentTime - lastTime;
+        lastTime = currentTime;
+        
+        // Update animations
+        model.units = updateAnimations(model.units, deltaTime);
+        const shotResult = processCompletedShots(model.units, model.grid); // Process completed shots
+        model.units = shotResult.units;
+        if (shotResult.updated) {
+            model.grid = shotResult.grid;
+        }
+        
+        // Update obstacle flash effects
+        model.grid = updateObstacleFlash(model.grid);
+        
+        // Update unit visibility based on line of sight - ONLY when player units move or when needed
+        // model.units = updateUnitVisibility(model.units, model.grid);
+        
+        // Check if current unit has completed their turn and auto-advance
+        const currentTurnUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+        console.log('=== TURN CHECK ===');
+        console.log('currentTurnIndex:', model.currentTurnIndex);
+        console.log('currentTurnUnit:', currentTurnUnit ? `${currentTurnUnit.owner} ${currentTurnUnit.id}` : 'none');
+        console.log('currentTurnUnit state:', currentTurnUnit ? {
+            hasMoved: currentTurnUnit.hasMoved,
+            hasAttacked: currentTurnUnit.hasAttacked,
+            isAnimating: currentTurnUnit.isAnimating
+        } : 'none');
+        
+        if (currentTurnUnit && currentTurnUnit.owner === 'player' && 
+            currentTurnUnit.hasMoved && currentTurnUnit.hasAttacked) {
+            // Player unit completed their turn, auto-advance
+            console.log('Player unit completed turn, advancing...');
+            model = advanceTurn(model);
+            
+            // Check if next unit is enemy and process their turn
+            const nextUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+            if (nextUnit && nextUnit.owner === 'enemy') {
+                console.log('Next unit is enemy, processing their turn...');
+                const result = executeEnemyTurn(model, nextUnit);
+                model = advanceTurn(result);
+                
+                // Continue processing enemy turns until we reach a player unit
+                while (true) {
+                    const nextEnemyUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+                    if (!nextEnemyUnit || nextEnemyUnit.owner === 'player') {
+                        break; // Player turn or no more units
+                    }
+                    console.log('Processing next enemy turn for unit', nextEnemyUnit.id);
+                    const enemyResult = executeEnemyTurn(model, nextEnemyUnit);
+                    model = advanceTurn(enemyResult);
+                }
+            }
+        }
+        
+        console.log('After turn advancement check - currentTurnIndex:', model.currentTurnIndex, 'currentUnit:', currentTurnUnit ? `${currentTurnUnit.owner} ${currentTurnUnit.id}` : 'none');
+        console.log('=== END TURN CHECK ===');
+        
+        // Update action feedback timers
+        model.units = updateActionFeedbackTimers(model.units);
+        
+        // Render
+        view(model, canvas, ctx);
+        updateButtonStates();
+        
+        // Check if we should continue the game loop
+        const hasAnimations = model.units.some(u => u.isAnimating);
+        const loopCurrentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+        
+        console.log('Game loop continuation check:', {
+            hasAnimations,
+            currentTurnIndex: model.currentTurnIndex,
+            currentUnit: loopCurrentUnit ? `${loopCurrentUnit.owner} ${loopCurrentUnit.id}` : 'none',
+            unitState: loopCurrentUnit ? {
+                hasMoved: loopCurrentUnit.hasMoved,
+                hasAttacked: loopCurrentUnit.hasAttacked,
+                isAnimating: loopCurrentUnit.isAnimating
+            } : 'none'
+        });
+        
+        if (hasAnimations) {
+            console.log('Continuing game loop for animations...');
+            requestAnimationFrame(gameLoop);
+        } else {
+            // If no animations, render once and stop
+            console.log('Stopping game loop, rendering once...');
+            view(model, canvas, ctx);
+            updateButtonStates();
+        }
+    }
+
+    /**
+     * The dispatch function, linking events to the update logic.
+     * @param {object} msg 
+     */
+    function dispatch(msg) {
+        console.log('=== DISPATCH ===');
+        console.log('Message type:', msg.type);
+        
+        model = update(msg, model);
+        
+        console.log('After update - currentTurnIndex:', model.currentTurnIndex);
+        console.log('Current unit:', model.units.find(u => u.turnOrder === model.currentTurnIndex));
+        console.log('Has animations:', model.units.some(u => u.isAnimating));
+        
+        // Start animation loop if needed
+        if (model.units.some(u => u.isAnimating)) {
+            console.log('Starting game loop for animations');
+            if (animationId) {
+                cancelAnimationFrame(animationId);
+            }
+            lastTime = performance.now();
+            animationId = requestAnimationFrame(gameLoop);
+        } else {
+            console.log('No animations, just rendering once');
+            // Just render once if no animations
+            view(model, canvas, ctx);
+            updateButtonStates();
+        }
+        
+        // Check for game over
+        const playerUnits = model.units.filter(u => u.owner === 'player' && !u.isDead);
+        const enemyUnits = model.units.filter(u => u.owner === 'enemy' && !u.isDead);
+        
+        if (playerUnits.length === 0) {
+            alert('Game Over! Enemy wins!');
+            return;
+        }
+        if (enemyUnits.length === 0) {
+            alert('Victory! Player wins!');
+            return;
+        }
+
+
+    }
+    
+    // Setup Event Listeners
+    canvas.addEventListener('click', (e) => {
+        const rect = canvas.getBoundingClientRect();
+        const x = e.clientX - rect.left;
+        const y = e.clientY - rect.top;
+
+        const tileX = Math.floor(x / TILE_SIZE);
+        const tileY = Math.floor(y / TILE_SIZE);
+
+        dispatch({ type: 'TILE_CLICKED', payload: { x: tileX, y: tileY } });
+    });
+
+    endTurnBtn.addEventListener('click', () => {
+        dispatch({ type: 'END_TURN_CLICKED' });
+    });
+
+    skipMovementBtn.addEventListener('click', () => {
+        dispatch({ type: 'SKIP_MOVEMENT' });
+    });
+
+    skipAttackBtn.addEventListener('click', () => {
+        dispatch({ type: 'SKIP_ATTACK' });
+    });
+
+    // Handle window resize
+    window.addEventListener('resize', () => {
+        // Recalculate grid size and reinitialize if needed
+        const newSize = calculateGridSize();
+        if (newSize.width !== model.grid[0].length || newSize.height !== model.grid.length) {
+            model = init();
+            canvas.width = model.grid[0].length * TILE_SIZE;
+            canvas.height = model.grid.length * TILE_SIZE;
+            view(model, canvas, ctx);
+            updateButtonStates();
+        }
+    });
+
+    // Initial render
+    dispatch({ type: 'INITIAL_RENDER' }); // Dispatch a dummy event to start
+}
+
+// Start the game when DOM is loaded
+document.addEventListener('DOMContentLoaded', App);
+
+/**
+ * Checks if there's a clear line of sight between two points
+ * @param {number} startX 
+ * @param {number} startY 
+ * @param {number} endX 
+ * @param {number} endY 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} units 
+ * @returns {{blocked: boolean, blocker: Unit | null, obstacleX: number | null, obstacleY: number | null}} Line of sight result
+ */
+function checkLineOfSight(startX, startY, endX, endY, grid, units) {
+    const dx = endX - startX;
+    const dy = endY - startY;
+    const distance = Math.sqrt(dx * dx + dy * dy);
+    
+    // Add maximum visibility range - units can't see beyond this distance
+    const MAX_VISIBILITY_RANGE = 6; // Reduced from unlimited to 6 tiles
+    
+    if (distance === 0) return { blocked: false, blocker: null, obstacleX: null, obstacleY: null };
+    
+    // If target is beyond visibility range, it's blocked
+    if (distance > MAX_VISIBILITY_RANGE) {
+        return { 
+            blocked: true, 
+            blocker: null, 
+            obstacleX: null, 
+            obstacleY: null,
+            reason: 'beyond_visibility_range'
+        };
+    }
+    
+    // Use Bresenham's line algorithm to check each tile along the path
+    const steps = Math.max(Math.abs(dx), Math.abs(dy));
+    const xStep = dx / steps;
+    const yStep = dy / steps;
+    
+    for (let i = 1; i <= steps; i++) {
+        const checkX = Math.round(startX + xStep * i);
+        const checkY = Math.round(startY + yStep * i);
+        
+        // Check if we've reached the target
+        if (checkX === endX && checkY === endY) {
+            break;
+        }
+        
+        // Check for obstacles
+        if (grid[checkY] && grid[checkY][checkX] && grid[checkY][checkX].type === 'obstacle') {
+            return { 
+                blocked: true, 
+                blocker: null, 
+                obstacleX: checkX, 
+                obstacleY: checkY,
+                reason: 'obstacle'
+            };
+        }
+        
+        // Check for units blocking the path
+        const blockingUnit = units.find(unit => 
+            unit.x === checkX && unit.y === checkY && !unit.isDead
+        );
+        
+        if (blockingUnit) {
+            return { 
+                blocked: true, 
+                blocker: blockingUnit, 
+                obstacleX: null, 
+                obstacleY: null,
+                reason: 'unit'
+            };
+        }
+    }
+    
+    return { blocked: false, blocker: null, obstacleX: null, obstacleY: null, reason: 'clear' };
+}
+
+/**
+ * Updates obstacle damage flash effects
+ * @param {Tile[][]} grid 
+ * @returns {Tile[][]} Updated grid with reduced flash values
+ */
+function updateObstacleFlash(grid) {
+    const FLASH_DECAY_RATE = 0.05; // How fast the flash fades
+    
+    return grid.map(row => 
+        row.map(tile => {
+            if (tile.type === 'obstacle' && tile.damageFlash > 0) {
+                return {
+                    ...tile,
+                    damageFlash: Math.max(0, tile.damageFlash - FLASH_DECAY_RATE)
+                };
+            }
+            return tile;
+        })
+    );
+}
+
+/**
+ * Generates a varied obstacle color within the grey/black/blue palette
+ * @returns {string} Hex color for the obstacle
+ */
+function generateObstacleColor() {
+    const colors = [
+        '#2C3E50', // Dark blue-grey (original)
+        '#34495E', // Slightly lighter blue-grey
+        '#2E4053', // Medium blue-grey
+        '#283747', // Darker blue-grey
+        '#1B2631', // Very dark blue-grey
+        '#1F2937'  // Dark grey with blue tint
+    ];
+    return colors[Math.floor(Math.random() * colors.length)];
+}
+
+/**
+ * Creates a standardized obstacle with consistent properties
+ * @param {number} x 
+ * @param {number} y 
+ * @returns {Tile} Standardized obstacle tile
+ */
+function createObstacle(x, y) {
+    return {
+        type: 'obstacle',
+        x,
+        y,
+        providesCover: true,
+        health: OBSTACLE_MAX_HEALTH,
+        damageFlash: 0,
+        color: generateObstacleColor()
+    };
+}
+
+/**
+ * Updates unit visibility based on line of sight from player units
+ * @param {Unit[]} units 
+ * @param {Tile[][]} grid 
+ * @returns {Unit[]} Updated units with visibility updated
+ */
+function updateUnitVisibility(units, grid) {
+    const playerUnits = units.filter(unit => unit.owner === 'player' && !unit.isDead);
+    const enemyUnits = units.filter(unit => unit.owner === 'enemy' && !unit.isDead);
+    const currentTime = Date.now();
+    
+    // Update units with visibility and memory tracking
+    const updatedUnits = units.map(unit => {
+        if (unit.owner === 'enemy') {
+            // Check if this enemy is currently visible to any player unit
+            let isCurrentlyVisible = false;
+            let lastSeenTime = unit.lastSeen || 0;
+            
+            // Check line of sight from each player unit
+            for (const playerUnit of playerUnits) {
+                const los = checkLineOfSight(
+                    playerUnit.x, playerUnit.y,
+                    unit.x, unit.y,
+                    grid, units
+                );
+                
+                if (!los.blocked) {
+                    isCurrentlyVisible = true;
+                    lastSeenTime = currentTime;
+                    break;
+                }
+            }
+            
+            return { 
+                ...unit, 
+                isVisible: isCurrentlyVisible,
+                lastSeen: lastSeenTime,
+                lastKnownX: isCurrentlyVisible ? unit.x : (unit.lastKnownX || unit.x),
+                lastKnownY: isCurrentlyVisible ? unit.y : (unit.lastKnownY || unit.y)
+            };
+        }
+        return unit;
+    });
+    
+    return updatedUnits;
+}
+
+/**
+ * Draws fog of war effects for areas where enemy units might be hidden
+ * @param {Model} model
+ * @param {CanvasRenderingContext2D} ctx
+ */
+function drawFogOfWar(model, ctx) {
+    // Create a subtle fog effect for areas outside player line of sight
+    ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
+    
+    // Draw fog over the entire grid
+    ctx.fillRect(0, 0, model.grid[0].length * TILE_SIZE, model.grid.length * TILE_SIZE);
+    
+    // Clear fog around player units (line of sight areas)
+    const playerUnits = model.units.filter(unit => unit.owner === 'player' && !unit.isDead);
+    
+    playerUnits.forEach(playerUnit => {
+        const centerX = playerUnit.x * TILE_SIZE + TILE_SIZE / 2;
+        const centerY = playerUnit.y * TILE_SIZE + TILE_SIZE / 2;
+        const sightRadius = Math.max(playerUnit.shootRange * TILE_SIZE, 100); // Minimum sight radius
+        
+        // Create radial gradient to clear fog around player units
+        const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, sightRadius);
+        gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
+        gradient.addColorStop(0.7, 'rgba(0, 0, 0, 0)');
+        gradient.addColorStop(1, 'rgba(0, 0, 0, 0.1)');
+        
+        ctx.fillStyle = gradient;
+        ctx.fillRect(0, 0, model.grid[0].length * TILE_SIZE, model.grid.length * TILE_SIZE);
+    });
+}
+
+/**
+ * Generates a random turn order for all units
+ * @param {Unit[]} units 
+ * @returns {Unit[]} Units with turn order assigned
+ */
+function generateTurnOrder(units) {
+    // Separate player and enemy units
+    const playerUnits = units.filter(u => u.owner === 'player');
+    const enemyUnits = units.filter(u => u.owner === 'enemy');
+    
+    // Shuffle each group separately
+    const shuffledPlayers = [...playerUnits].sort(() => Math.random() - 0.5);
+    const shuffledEnemies = [...enemyUnits].sort(() => Math.random() - 0.5);
+    
+    // Combine: players first, then enemies
+    const orderedUnits = [...shuffledPlayers, ...shuffledEnemies];
+    
+    // Assign turn order
+    return orderedUnits.map((unit, index) => ({
+        ...unit,
+        turnOrder: index
+    }));
+}
+
+/**
+ * Finds the best cover position for a unit that allows them to attack while being protected
+ * @param {Unit} unit 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @returns {{x: number, y: number} | null} Best cover position or null if none found
+ */
+function findBestCoverPosition(unit, grid, allUnits) {
+    const width = grid[0].length;
+    const height = grid.length;
+    const maxSearchDistance = 8; // Don't search too far
+    
+    let bestPosition = null;
+    let bestScore = -1;
+    
+    // Search in expanding radius around unit
+    for (let radius = 1; radius <= maxSearchDistance; radius++) {
+        for (let dx = -radius; dx <= radius; dx++) {
+            for (let dy = -radius; dy <= radius; dy++) {
+                if (Math.abs(dx) + Math.abs(dy) !== radius) continue; // Only check perimeter
+                
+                const checkX = unit.x + dx;
+                const checkY = unit.y + dy;
+                
+                // Check bounds
+                if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) continue;
+                
+                // Check if position is walkable
+                if (grid[checkY][checkX].type === 'obstacle') continue;
+                
+                // Check if position is occupied
+                if (allUnits.some(u => u.x === checkX && u.y === checkY && !u.isDead)) continue;
+                
+                // Check if position provides cover
+                const hasCover = checkCover({ x: checkX, y: checkY }, grid);
+                
+                // Check if position allows attacking any visible enemies
+                const canAttackFromHere = allUnits.some(target => 
+                    target.owner === 'player' && 
+                    !target.isDead &&
+                    Math.abs(checkX - target.x) + Math.abs(checkY - target.y) <= unit.shootRange
+                );
+                
+                // Calculate score: prioritize cover + attack capability
+                let score = 0;
+                if (hasCover) score += 10;
+                if (canAttackFromHere) score += 5;
+                score -= Math.abs(dx) + Math.abs(dy); // Prefer closer positions
+                
+                if (score > bestScore) {
+                    bestScore = score;
+                    bestPosition = { x: checkX, y: checkY };
+                }
+            }
+        }
+    }
+    
+    return bestPosition;
+}
+
+/**
+ * Makes AI decisions for an enemy unit
+ * @param {Unit} unit 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @returns {{action: 'move' | 'attack' | 'skip', targetX?: number, targetY?: number, targetUnit?: Unit} | null} AI decision
+ */
+function makeAIDecision(unit, grid, allUnits) {
+    console.log('makeAIDecision called for unit', unit.id, 'with behavior:', unit.aiBehavior);
+    console.log('Unit state:', { hasMoved: unit.hasMoved, hasAttacked: unit.hasAttacked, isDead: unit.isDead });
+    
+    if (unit.isDead || unit.hasMoved && unit.hasAttacked) {
+        console.log('Unit', unit.id, 'cannot act - dead or completed actions');
+        return null; // Unit can't act
+    }
+    
+    // Find visible player units
+    const visiblePlayers = allUnits.filter(target => 
+        target.owner === 'player' && 
+        !target.isDead && 
+        target.isVisible
+    );
+    
+    console.log('Visible players for unit', unit.id, ':', visiblePlayers.length);
+    
+    // If no visible players, behavior depends on AI type
+    if (visiblePlayers.length === 0) {
+        console.log('No visible players for unit', unit.id, 'using behavior:', unit.aiBehavior);
+        switch (unit.aiBehavior) {
+            case 'aggressive':
+                return { type: 'skip' }; // Wait for targets
+            case 'patrol':
+                return generatePatrolAction(unit, grid, allUnits);
+            case 'stationary':
+                return { type: 'skip' }; // Stay put
+        }
+    }
+    
+    // If we have visible players, behavior depends on AI type
+    console.log('Visible players found for unit', unit.id, 'using behavior:', unit.aiBehavior);
+    switch (unit.aiBehavior) {
+        case 'aggressive':
+            return generateAggressiveAction(unit, visiblePlayers, grid, allUnits);
+        case 'patrol':
+            return generatePatrolAction(unit, grid, allUnits, visiblePlayers);
+        case 'stationary':
+            return generateStationaryAction(unit, visiblePlayers, grid, allUnits);
+    }
+    
+    return { type: 'skip' };
+}
+
+/**
+ * Generates aggressive AI action - move toward and attack visible players
+ * @param {Unit} unit 
+ * @param {Unit[]} visiblePlayers 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @returns {{action: string, targetX?: number, targetY?: number, targetUnit?: Unit}}
+ */
+function generateAggressiveAction(unit, visiblePlayers, grid, allUnits) {
+    console.log('generateAggressiveAction for unit', unit.id, 'with', visiblePlayers.length, 'visible players');
+    
+    // Find closest visible player
+    const closestPlayer = visiblePlayers.reduce((closest, player) => {
+        const distance = Math.abs(unit.x - player.x) + Math.abs(unit.y - player.y);
+        const closestDistance = Math.abs(unit.x - closest.x) + Math.abs(unit.y - closest.y);
+        return distance < closestDistance ? player : closest;
+    });
+    
+    const distance = Math.abs(unit.x - closestPlayer.x) + Math.abs(unit.y - closestPlayer.y);
+    console.log('Distance to closest player for unit', unit.id, ':', distance, 'shoot range:', unit.shootRange);
+    
+    // If we can attack and haven't attacked yet, do it!
+    if (!unit.hasAttacked && distance <= unit.shootRange) {
+        console.log('Unit', unit.id, 'can attack player at', closestPlayer.x, closestPlayer.y);
+        return { type: 'attack', targetX: closestPlayer.x, targetY: closestPlayer.y };
+    }
+    
+    // If we can't attack but can move and haven't moved yet, move closer
+    if (!unit.hasMoved && distance > unit.shootRange) {
+        console.log('Unit', unit.id, 'needs to move closer to attack');
+        const moveTarget = findMoveTowardTarget(unit, closestPlayer, grid, allUnits);
+        if (moveTarget) {
+            console.log('Move target found for unit', unit.id, ':', moveTarget);
+            return { type: 'move', x: moveTarget.x, y: moveTarget.y };
+        } else {
+            console.log('No move target found for unit', unit.id);
+        }
+    }
+    
+    // If we've done what we can, skip the turn
+    console.log('Unit', unit.id, 'skipping turn - no more actions possible');
+    return { type: 'skip' };
+}
+
+/**
+ * Generates patrol AI action - defend territory, engage if players enter
+ * @param {Unit} unit 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @param {Unit[]} visiblePlayers 
+ * @returns {{action: string, targetX?: number, targetY?: number, targetUnit?: Unit}}
+ */
+function generatePatrolAction(unit, grid, allUnits, visiblePlayers = []) {
+    // If players are visible, engage them
+    if (visiblePlayers.length > 0) {
+        return generateAggressiveAction(unit, visiblePlayers, grid, allUnits);
+    }
+    
+    // Otherwise, patrol within territory
+    if (!unit.hasMoved) {
+        const patrolTarget = findPatrolPosition(unit, grid, allUnits);
+        if (patrolTarget) {
+            return { type: 'move', x: patrolTarget.x, y: patrolTarget.y };
+        }
+    }
+    
+    return { type: 'skip' };
+}
+
+/**
+ * Generates stationary AI action - attack from cover, flee when attacked
+ * @param {Unit} unit 
+ * @param {Unit[]} visiblePlayers 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @returns {{action: string, targetX?: number, targetY?: number, targetUnit?: Unit}}
+ */
+function generateStationaryAction(unit, visiblePlayers, grid, allUnits) {
+    // If we can attack, do it
+    if (!unit.hasAttacked) {
+        const attackablePlayer = visiblePlayers.find(player => {
+            const distance = Math.abs(unit.x - player.x) + Math.abs(unit.y - player.y);
+            return distance <= unit.shootRange;
+        });
+        
+        if (attackablePlayer) {
+            return { type: 'attack', targetX: attackablePlayer.x, targetY: attackablePlayer.y };
+        }
+    }
+    
+    // If we're not in cover and can move, try to find cover
+    if (!unit.hasMoved && !unit.inCover) {
+        const coverPosition = findBestCoverPosition(unit, grid, allUnits);
+        if (coverPosition) {
+            return { type: 'move', x: coverPosition.x, y: coverPosition.y };
+        }
+    }
+    
+    return { type: 'skip' };
+}
+
+/**
+ * Finds a movement target toward a specific target unit
+ * @param {Unit} unit 
+ * @param {Unit} target 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @returns {{x: number, y: number} | null} Movement target or null if none found
+ */
+function findMoveTowardTarget(unit, target, grid, allUnits) {
+    console.log('findMoveTowardTarget for unit', unit.id, 'toward target at', target.x, target.y);
+    console.log('Unit position:', unit.x, unit.y, 'movement range:', unit.movementRange);
+    
+    const width = grid[0].length;
+    const height = grid.length;
+    const maxSearchDistance = Math.min(unit.movementRange, 8);
+    
+    console.log('Searching within distance:', maxSearchDistance);
+    
+    let bestPosition = null;
+    let bestScore = -1;
+    
+    // Search for positions that get us closer to target
+    for (let dx = -maxSearchDistance; dx <= maxSearchDistance; dx++) {
+        for (let dy = -maxSearchDistance; dy <= maxSearchDistance; dy++) {
+            if (Math.abs(dx) + Math.abs(dy) > unit.movementRange) continue;
+            
+            const checkX = unit.x + dx;
+            const checkY = unit.y + dy;
+            
+            // Check bounds
+            if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) continue;
+            
+            // Check if position is walkable
+            if (grid[checkY][checkX].type === 'obstacle') continue;
+            
+            // Check if position is occupied
+            if (allUnits.some(u => u.x === checkX && u.y === checkY && !u.isDead)) continue;
+            
+            // Check if path is not blocked
+            if (isPathBlocked(unit.x, unit.y, checkX, checkY, grid)) continue;
+            
+            // Calculate score: prefer positions closer to target
+            const currentDistance = Math.abs(unit.x - target.x) + Math.abs(unit.y - target.y);
+            const newDistance = Math.abs(checkX - target.x) + Math.abs(checkY - target.y);
+            const distanceImprovement = currentDistance - newDistance;
+            
+            let score = distanceImprovement * 10; // Prioritize getting closer
+            if (grid[checkY][checkX].providesCover) score += 5; // Bonus for cover
+            
+            if (score > bestScore) {
+                bestScore = score;
+                bestPosition = { x: checkX, y: checkY };
+            }
+        }
+    }
+    
+    console.log('Best position found for unit', unit.id, ':', bestPosition, 'with score:', bestScore);
+    return bestPosition;
+}
+
+/**
+ * Finds a patrol position within the unit's territory
+ * @param {Unit} unit 
+ * @param {Tile[][]} grid 
+ * @param {Unit[]} allUnits 
+ * @returns {{x: number, y: number} | null} Patrol position or null if none found
+ */
+function findPatrolPosition(unit, grid, allUnits) {
+    const width = grid[0].length;
+    const height = grid.length;
+    const maxSearchDistance = Math.min(unit.movementRange, unit.patrolRadius);
+    
+    let bestPosition = null;
+    let bestScore = -1;
+    
+    // Search for positions within patrol radius
+    for (let dx = -maxSearchDistance; dx <= maxSearchDistance; dx++) {
+        for (let dy = -maxSearchDistance; dy <= maxSearchDistance; dy++) {
+            if (Math.abs(dx) + Math.abs(dy) > unit.movementRange) continue;
+            
+            const checkX = unit.x + dx;
+            const checkY = unit.y + dy;
+            
+            // Check bounds
+            if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) continue;
+            
+            // Check if position is within patrol radius
+            const distanceFromCenter = Math.abs(checkX - unit.patrolCenterX) + Math.abs(checkY - unit.patrolCenterY);
+            if (distanceFromCenter > unit.patrolRadius) continue;
+            
+            // Check if position is walkable
+            if (grid[checkY][checkX].type === 'obstacle') continue;
+            
+            // Check if position is occupied
+            if (allUnits.some(u => u.x === checkX && u.y === checkY && !u.isDead)) continue;
+            
+            // Check if path is not blocked
+            if (isPathBlocked(unit.x, unit.y, checkX, checkY, grid)) continue;
+            
+            // Calculate score: prefer positions with good cover and visibility
+            let score = 0;
+            if (grid[checkY][checkX].providesCover) score += 8;
+            
+            // Bonus for positions that allow seeing outside patrol area
+            const canSeeOutside = checkX === 0 || checkX === width - 1 || checkY === 0 || checkY === height - 1;
+            if (canSeeOutside) score += 3;
+            
+            // Small random factor to avoid predictable patterns
+            score += Math.random() * 2;
+            
+            if (score > bestScore) {
+                bestScore = score;
+                bestPosition = { x: checkX, y: checkY };
+            }
+        }
+    }
+    
+    return bestPosition;
+}
+
+/**
+ * Advances to the next turn in the turn order
+ * @param {Model} model 
+ * @returns {Model} Updated model with next turn
+ */
+function advanceTurn(model) {
+    let nextTurnIndex = model.currentTurnIndex + 1;
+    
+    // Find next living unit
+    while (nextTurnIndex < model.units.length) {
+        const nextUnit = model.units.find(u => u.turnOrder === nextTurnIndex);
+        if (nextUnit && !nextUnit.isDead) {
+            break;
+        }
+        nextTurnIndex++;
+    }
+    
+    // If we've gone through all units, start over
+    if (nextTurnIndex >= model.units.length) {
+        nextTurnIndex = 0;
+        // Reset all units' actions
+        const updatedUnits = model.units.map(unit => ({
+            ...unit,
+            hasMoved: false,
+            hasAttacked: false
+        }));
+        
+        return {
+            ...model,
+            units: updatedUnits,
+            currentTurnIndex: nextTurnIndex,
+            uiState: { type: 'AwaitingSelection' } // Reset UI state
+        };
+    }
+    
+    // Reset UI state when advancing to next turn
+    return {
+        ...model,
+        currentTurnIndex: nextTurnIndex,
+        uiState: { type: 'AwaitingSelection' }
+    };
+}
+
+/**
+ * Executes the current unit's turn (player or AI)
+ * @param {Model} model 
+ * @returns {Model} Updated model after turn execution
+ */
+function executeCurrentTurn(model) {
+    const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+    
+    if (!currentUnit || currentUnit.isDead) {
+        console.log('No current unit or unit is dead, advancing turn');
+        return advanceTurn(model);
+    }
+    
+    // If it's a player unit, wait for player input
+    if (currentUnit.owner === 'player') {
+        console.log('Player turn, waiting for input');
+        return model;
+    }
+    
+    // If it's an enemy unit, execute AI turn immediately
+    console.log('Enemy turn for unit', currentUnit.id, '- executing AI immediately');
+    
+    // Execute AI turn and advance immediately
+    const result = executeEnemyTurn(model, currentUnit);
+    console.log('AI turn completed for unit', currentUnit.id, 'advancing turn');
+    
+    // Always advance turn after enemy completes their actions
+    return advanceTurn(result);
+}
+
+
+
+/**
+ * Removes dead units from the turn order and adjusts turn indices
+ * @param {Unit[]} units 
+ * @returns {Unit[]} Updated units with dead units removed and turn order adjusted
+ */
+function cleanupDeadUnits(units) {
+    const livingUnits = units.filter(unit => !unit.isDead);
+    
+    // Reassign turn order for remaining units
+    return livingUnits.map((unit, index) => ({
+        ...unit,
+        turnOrder: index
+    }));
+}
+
+/**
+ * Updates action feedback timers for enemy units
+ * @param {Unit[]} units 
+ * @returns {Unit[]} Updated units
+ */
+function updateActionFeedbackTimers(units) {
+    return units.map(unit => {
+        if (unit.actionFeedbackTimer > 0) {
+            return { ...unit, actionFeedbackTimer: Math.max(0, unit.actionFeedbackTimer - 16) }; // 16ms per frame at 60fps
+        }
+        return unit;
+    });
+}
+
+
+
+
+
+/**
+ * Pure function: Executes enemy turn with guaranteed termination
+ * @param {Model} model 
+ * @param {Unit} enemyUnit 
+ * @returns {Model} Updated model with turn completed
+ */
+function executeEnemyTurn(model, enemyUnit) {
+    // Pre-condition: enemyUnit is an enemy unit that needs to act
+    if (enemyUnit.owner !== 'enemy') {
+        throw new Error('executeEnemyTurn called with non-enemy unit');
+    }
+    
+    console.log('Executing enemy turn for unit', enemyUnit.id);
+    
+    // Find visible player units
+    const visiblePlayers = model.units.filter(target => 
+        target.owner === 'player' && 
+        !target.isDead && 
+        target.isVisible
+    );
+    
+    if (visiblePlayers.length === 0) {
+        console.log('No visible players for unit', enemyUnit.id, 'skipping turn');
+        // Mark both actions as complete and return
+        const skippedUnit = { 
+            ...enemyUnit, 
+            hasMoved: true, 
+            hasAttacked: true,
+            isAnimating: false,
+            animationType: 'none',
+            animationProgress: 0,
+            path: [],
+            targetX: -1,
+            targetY: -1
+        };
+        return {
+            ...model,
+            units: model.units.map(u => u.id === enemyUnit.id ? skippedUnit : u)
+        };
+    }
+    
+    // Find closest visible player
+    const closestPlayer = visiblePlayers.reduce((closest, player) => {
+        const distance = Math.abs(enemyUnit.x - player.x) + Math.abs(enemyUnit.y - player.y);
+        const closestDistance = Math.abs(enemyUnit.x - closest.x) + Math.abs(enemyUnit.y - closest.y);
+        return distance < closestDistance ? player : closest;
+    });
+    
+    const distance = Math.abs(enemyUnit.x - closestPlayer.x) + Math.abs(enemyUnit.y - closestPlayer.y);
+    console.log('Unit', enemyUnit.id, 'distance to player:', distance, 'shoot range:', enemyUnit.shootRange);
+    
+    let updatedModel = model;
+    let updatedUnit = enemyUnit;
+    
+    // First action: Move if we can't attack yet
+    if (!updatedUnit.hasMoved && distance > enemyUnit.shootRange) {
+        console.log('Unit', enemyUnit.id, 'moving closer to attack');
+        const moveTarget = findMoveTowardTarget(updatedUnit, closestPlayer, updatedModel.grid, updatedModel.units);
+        if (moveTarget) {
+            const moveResult = executeEnemyMove(updatedModel, updatedUnit, { type: 'move', x: moveTarget.x, y: moveTarget.y });
+            updatedModel = moveResult;
+            updatedUnit = moveResult.units.find(u => u.id === enemyUnit.id);
+            console.log('Unit', enemyUnit.id, 'moved to', moveTarget.x, moveTarget.y);
+        } else {
+            // Can't move, mark as moved
+            updatedUnit = { ...updatedUnit, hasMoved: true };
+            updatedModel = {
+                ...updatedModel,
+                units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u)
+            };
+        }
+    } else if (!updatedUnit.hasMoved) {
+        // No movement needed, mark as moved
+        updatedUnit = { ...updatedUnit, hasMoved: true };
+        updatedModel = {
+            ...updatedModel,
+            units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u)
+        };
+    }
+    
+    // Second action: Attack if we can
+    if (!updatedUnit.hasAttacked && distance <= enemyUnit.shootRange) {
+        console.log('Unit', enemyUnit.id, 'attacking player at', closestPlayer.x, closestPlayer.y);
+        const attackResult = executeEnemyAttack(updatedModel, updatedUnit, { type: 'attack', targetX: closestPlayer.x, targetY: closestPlayer.y });
+        updatedModel = attackResult;
+        updatedUnit = attackResult.units.find(u => u.id === enemyUnit.id);
+        console.log('Unit', enemyUnit.id, 'attacked');
+    } else if (!updatedUnit.hasAttacked) {
+        // Can't attack, mark as attacked
+        updatedUnit = { ...updatedUnit, hasAttacked: true };
+        updatedModel = {
+            ...updatedModel,
+            units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u)
+        };
+    }
+    
+    // Ensure both actions are marked as complete
+    if (!updatedUnit.hasMoved) {
+        updatedUnit = { 
+            ...updatedUnit, 
+            hasMoved: true,
+            isAnimating: false,
+            animationType: 'none',
+            animationProgress: 0,
+            path: [],
+            targetX: -1,
+            targetY: -1
+        };
+    }
+    if (!updatedUnit.hasAttacked) {
+        updatedUnit = { 
+            ...updatedUnit, 
+            hasAttacked: true,
+            isAnimating: false,
+            animationType: 'none',
+            animationProgress: 0,
+            projectileX: -1,
+            projectileY: -1,
+            projectileTargetX: -1,
+            projectileTargetY: -1
+        };
+    }
+    
+    // Update the model with the final unit state
+    const finalModel = {
+        ...updatedModel,
+        units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u)
+    };
+    
+    console.log('Enemy turn completed for unit', enemyUnit.id, 'final state:', {
+        hasMoved: updatedUnit.hasMoved,
+        hasAttacked: updatedUnit.hasAttacked
+    });
+    
+    return finalModel;
+}
+
+/**
+ * Pure function: Executes enemy movement
+ * @param {Model} model 
+ * @param {Unit} enemyUnit 
+ * @param {Object} decision 
+ * @returns {Model} Updated model
+ */
+function executeEnemyMove(model, enemyUnit, decision) {
+    console.log('Executing enemy move for unit', enemyUnit.id, 'to', decision.x, decision.y);
+    
+    const path = findPath(enemyUnit.x, enemyUnit.y, decision.x, decision.y, model.grid);
+    console.log('Path found:', path);
+    
+    if (path.length > 1) {
+        // Start movement animation
+        const animatedUnit = startMovementAnimation(enemyUnit, decision.x, decision.y, model.grid);
+        animatedUnit.hasMoved = true;
+        console.log('Enemy unit', enemyUnit.id, 'started movement animation');
+        
+        return {
+            ...model,
+            units: model.units.map(u => u.id === enemyUnit.id ? animatedUnit : u)
+        };
+    } else {
+        // No movement needed, mark as moved
+        console.log('Enemy unit', enemyUnit.id, 'no movement needed, marked as moved');
+        return {
+            ...model,
+            units: model.units.map(u => 
+                u.id === enemyUnit.id ? { ...u, hasMoved: true } : u
+            )
+        };
+    }
+}
+
+/**
+ * Pure function: Executes enemy attack
+ * @param {Model} model 
+ * @param {Unit} enemyUnit 
+ * @param {Object} decision 
+ * @returns {Model} Updated model
+ */
+function executeEnemyAttack(model, enemyUnit, decision) {
+    console.log('Executing enemy attack for unit', enemyUnit.id, 'at target', decision.targetX, decision.targetY);
+    
+    const animatedUnit = startShootingAnimation(enemyUnit, decision.targetX, decision.targetY);
+    animatedUnit.hasAttacked = true;
+    console.log('Enemy unit', enemyUnit.id, 'started attack animation');
+    
+    return {
+        ...model,
+        units: model.units.map(u => u.id === enemyUnit.id ? animatedUnit : u)
+    };
+}
+
+/**
+ * Sanitizes unit animation state to prevent crashes
+ * @param {Unit} unit 
+ * @returns {Unit} Sanitized unit
+ */
+function sanitizeUnitState(unit) {
+    // If unit is not animating, ensure all animation properties are reset
+    if (!unit.isAnimating) {
+        return {
+            ...unit,
+            animationType: 'none',
+            animationProgress: 0,
+            path: [],
+            targetX: -1,
+            targetY: -1,
+            projectileX: -1,
+            projectileY: -1,
+            projectileTargetX: -1,
+            projectileTargetY: -1
+        };
+    }
+    
+    // If unit is animating, validate animation properties
+    const sanitizedUnit = { ...unit };
+    
+    // Validate path data
+    if (sanitizedUnit.animationType === 'moving') {
+        if (!sanitizedUnit.path || !Array.isArray(sanitizedUnit.path)) {
+            sanitizedUnit.path = [];
+        }
+        if (typeof sanitizedUnit.targetX !== 'number' || typeof sanitizedUnit.targetY !== 'number') {
+            sanitizedUnit.targetX = sanitizedUnit.x;
+            sanitizedUnit.targetY = sanitizedUnit.y;
+        }
+    }
+    
+    // Validate projectile data
+    if (sanitizedUnit.animationType === 'shooting') {
+        if (typeof sanitizedUnit.projectileX !== 'number' || typeof sanitizedUnit.projectileY !== 'number') {
+            sanitizedUnit.projectileX = sanitizedUnit.x;
+            sanitizedUnit.projectileY = sanitizedUnit.y;
+        }
+        if (typeof sanitizedUnit.projectileTargetX !== 'number' || typeof sanitizedUnit.projectileTargetY !== 'number') {
+            sanitizedUnit.projectileTargetX = sanitizedUnit.x;
+            sanitizedUnit.projectileTargetY = sanitizedUnit.y;
+        }
+    }
+    
+    // Validate animation progress
+    if (typeof sanitizedUnit.animationProgress !== 'number' || 
+        sanitizedUnit.animationProgress < 0 || 
+        sanitizedUnit.animationProgress > 1) {
+        sanitizedUnit.animationProgress = 0;
+    }
+    
+    return sanitizedUnit;
+}
+
+
diff --git a/html/XCOM/index.html b/html/XCOM/index.html
new file mode 100644
index 0000000..7ba4346
--- /dev/null
+++ b/html/XCOM/index.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>YCOM</title>
+    <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎯</text></svg>">
+    <link rel="stylesheet" href="style.css">
+</head>
+<body>
+    <div id="gameContainer">
+        <canvas id="gameCanvas"></canvas>
+        <div id="actionButtons">
+            <button id="endTurnBtn">End Turn</button>
+            <button id="skipMovementBtn">Skip Movement</button>
+            <button id="skipAttackBtn">Skip Attack</button>
+        </div>
+    </div>
+
+    <script src="game.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/html/XCOM/style.css b/html/XCOM/style.css
new file mode 100644
index 0000000..771009b
--- /dev/null
+++ b/html/XCOM/style.css
@@ -0,0 +1,104 @@
+/* XCOM-like Game Styles */
+/* Optimized for both desktop and mobile */
+
+body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+    background: #333;
+    color: #fff;
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+    overflow: hidden;
+}
+
+#gameContainer {
+    position: relative;
+    width: 100vw;
+    height: 100vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+canvas {
+    background: #222;
+    border: 1px solid #555;
+    max-width: 100vw;
+    max-height: 100vh;
+    display: block;
+}
+
+#actionButtons {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    z-index: 1000;
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+}
+
+#endTurnBtn, #skipMovementBtn, #skipAttackBtn {
+    margin: 0;
+    padding: 8px 16px;
+    font-size: 14px;
+    cursor: pointer;
+    background: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    transition: background-color 0.3s;
+    white-space: nowrap;
+}
+
+#endTurnBtn:hover:not(:disabled) {
+    background: #45a049;
+}
+
+#endTurnBtn:disabled {
+    background: #666;
+    cursor: not-allowed;
+}
+
+#skipMovementBtn {
+    background: #2196F3;
+}
+
+#skipMovementBtn:hover:not(:disabled) {
+    background: #1976D2;
+}
+
+#skipMovementBtn:disabled {
+    background: #666;
+    cursor: not-allowed;
+}
+
+#skipAttackBtn {
+    background: #FF9800;
+}
+
+#skipAttackBtn:hover:not(:disabled) {
+    background: #F57C00;
+}
+
+#skipAttackBtn:disabled {
+    background: #666;
+    cursor: not-allowed;
+}
+
+/* Mobile optimizations */
+@media (max-width: 768px) {
+    #endTurnBtn {
+        padding: 12px 20px;
+        font-size: 16px;
+        top: 5px;
+        right: 5px;
+    }
+}
+
+/* Touch-friendly interactions */
+@media (hover: none) and (pointer: coarse) {
+    #endTurnBtn {
+        min-height: 44px; /* iOS recommended minimum */
+    }
+}