about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--html/XCOM/game.js1662
1 files changed, 1419 insertions, 243 deletions
diff --git a/html/XCOM/game.js b/html/XCOM/game.js
index 20be052..6c4e256 100644
--- a/html/XCOM/game.js
+++ b/html/XCOM/game.js
@@ -36,7 +36,17 @@
  *   deathParticles: DeathParticle[],
  *   isDead: boolean,
  *   pendingDamage: number,
- *   justCompletedShot: boolean
+ *   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, 
@@ -49,7 +59,7 @@
  *   color: string
  * }} DeathParticle
  * @typedef {{ type: 'AwaitingSelection' } | { type: 'UnitSelected', unitId: number } | { type: 'AttackMode', unitId: number }} UIState
- * @typedef {{ grid: Tile[][], units: Unit[], turn: UnitOwner, uiState: UIState }} Model
+ * @typedef {{ grid: Tile[][], units: Unit[], selectedUnit: Unit | null, uiState: UIState, currentTurnIndex: number }} Model
  */
 
 // ------------------------------------------------
@@ -59,6 +69,10 @@
 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',
@@ -146,6 +160,8 @@ function calculateGridSize() {
  * @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);
     
@@ -156,17 +172,20 @@ function isPathBlocked(startX, startY, endX, endY, grid) {
         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;
 }
 
@@ -400,11 +419,20 @@ function getUnitPosition(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 };
     }
     
-    if (!unit.path || unit.path.length === 0) {
+    // 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 };
     }
     
@@ -424,7 +452,7 @@ function getUnitPosition(unit) {
     if (pathPoint && typeof pathPoint.x === 'number' && typeof pathPoint.y === 'number') {
         return pathPoint;
     } else {
-        console.error('getUnitPosition: invalid path point:', pathPoint);
+        console.warn('getUnitPosition: invalid path point at step', currentStep, 'for unit', unit.id, 'path:', unit.path);
         return { x: unit.x, y: unit.y };
     }
 }
@@ -1277,9 +1305,9 @@ function createPlayerUnit(id, x, y) {
         owner: 'player',
         x,
         y,
-        maxMovement: generateStat(2, 10),
+        maxMovement: generateStat(2, 6), // Reduced from 2-10 to 2-6
         movementRange: 0, // Will be set to maxMovement
-        shootRange: generateStat(4, 15),
+        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
@@ -1299,7 +1327,17 @@ function createPlayerUnit(id, x, y) {
         deathParticles: [],
         isDead: false,
         pendingDamage: 0,
-        justCompletedShot: false
+        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
@@ -1346,14 +1384,16 @@ function generateEnemySquad(grid) {
  * @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, 10),
+        maxMovement: generateStat(2, 6), // Reduced from 2-10 to 2-6
         movementRange: 0, // Will be set to maxMovement
-        shootRange: generateStat(4, 15),
+        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
@@ -1373,7 +1413,17 @@ function createEnemyUnit(id, x, y) {
         deathParticles: [],
         isDead: false,
         pendingDamage: 0,
-        justCompletedShot: false
+        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
@@ -1491,7 +1541,7 @@ function processCompletedShots(units, grid) {
     });
     
     return {
-        units: unitsUpdated ? units : updatedUnits,
+        units: unitsUpdated ? cleanupDeadUnits(units) : updatedUnits,
         grid: newGrid,
         updated: unitsUpdated || gridUpdated
     };
@@ -1514,13 +1564,19 @@ function init() {
     // Generate squads
     const playerUnits = generatePlayerSquad(grid);
     const enemyUnits = generateEnemySquad(grid);
-    const units = [...playerUnits, ...enemyUnits];
+    let units = [...playerUnits, ...enemyUnits];
+    
+    // Generate turn order for all units
+    const unitsWithTurnOrder = generateTurnOrder(units);
     
     // Update cover status for all units
-    units.forEach(unit => {
+    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;
@@ -1532,13 +1588,17 @@ function init() {
         }
     }
     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,
-        units,
-        turn: 'player',
+        grid: grid,
+        units: unitsWithVisibility,
+        selectedUnit: null,
         uiState: { type: 'AwaitingSelection' },
+        currentTurnIndex: 0
     };
 }
 
@@ -1553,7 +1613,11 @@ function init() {
  * @returns {Model} The new state.
  */
 function update(msg, model) {
-    if (model.turn !== 'player') {
+    // 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;
     }
 
@@ -1561,10 +1625,29 @@ function update(msg, model) {
         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);
@@ -1587,11 +1670,26 @@ function update(msg, model) {
                             unit.inCover = checkCover(unit, model.grid);
                         });
                         
-                        return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } };
+                        // 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;
                         
@@ -1707,14 +1805,40 @@ function update(msg, model) {
 
             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 }
@@ -1727,6 +1851,13 @@ function update(msg, 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 }
@@ -1737,12 +1868,8 @@ function update(msg, model) {
             return model;
         }
         case 'END_TURN_CLICKED': {
-            const nextUnits = model.units.map(u => ({
-                ...u,
-                hasMoved: false,
-                hasAttacked: false
-            }));
-            return { ...model, turn: 'enemy', units: nextUnits, uiState: { type: 'AwaitingSelection' } };
+            // Advance to next turn in turn order
+            return advanceTurn(model);
         }
         default:
             return model;
@@ -1823,7 +1950,12 @@ function drawHighlights(model, ctx) {
     if (model.uiState.type === 'AwaitingSelection') return;
 
     const selectedUnit = model.units.find(u => u.id === model.uiState.unitId);
-    if (!selectedUnit) return;
+    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
@@ -1858,7 +1990,9 @@ function drawHighlights(model, ctx) {
         // Highlight potential targets
         ctx.fillStyle = 'rgba(255, 0, 0, 0.6)';
         model.units.forEach(unit => {
-            if (unit.id !== selectedUnit.id) {
+            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();
@@ -1890,243 +2024,338 @@ function drawHighlights(model, ctx) {
  */
 function drawUnits(model, ctx) {
     model.units.forEach(unit => {
-        try {
-            const position = getUnitPosition(unit);
-            if (!position || typeof position.x !== 'number' || typeof position.y !== 'number') {
-                console.error('Invalid position for unit:', unit.id, position);
-                return; // Skip this 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 };
             }
-            
-            const { x, y } = position;
-            const centerX = x * TILE_SIZE + TILE_SIZE / 2;
-            const centerY = y * TILE_SIZE + TILE_SIZE / 2;
+        }
+        
+        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 (unit.inCover) {
-            ctx.fillStyle = COLORS.coverHighlight;
+    // 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.arc(centerX, centerY, UNIT_RADIUS + 5, 0, Math.PI * 2);
+            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();
-        }
-
-        // Draw shooting animation effect
-        if (unit.isAnimating && unit.animationType === 'shooting') {
-            const pulseSize = UNIT_RADIUS + 10 + Math.sin(unit.animationProgress * Math.PI * 4) * 5;
-            ctx.fillStyle = 'rgba(255, 255, 0, 0.3)';
+            
+            // Add glow effect
+            ctx.shadowColor = '#FFFF00';
+            ctx.shadowBlur = 8;
             ctx.beginPath();
-            ctx.arc(centerX, centerY, pulseSize, 0, Math.PI * 2);
+            ctx.arc(projectilePos.x, projectilePos.y, 2, 0, Math.PI * 2);
             ctx.fill();
+            ctx.shadowBlur = 0;
             
-            // Draw projectile
-            const projectilePos = getProjectilePosition(unit);
-            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)');
+            // 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;
                 
-                ctx.strokeStyle = gradient;
-                ctx.lineWidth = 3;
-                ctx.lineCap = 'round';
-                ctx.beginPath();
-                ctx.moveTo(trailStartX, trailStartY);
-                ctx.lineTo(projectilePos.x, projectilePos.y);
-                ctx.stroke();
+                // Impact explosion
+                const explosionSize = (sanitizedUnit.animationProgress - 0.8) * 20;
+                const alpha = 1 - (sanitizedUnit.animationProgress - 0.8) * 5;
                 
-                // Draw projectile bolt
-                ctx.fillStyle = '#FFFF00';
+                ctx.fillStyle = `rgba(255, 100, 0, ${alpha})`;
                 ctx.beginPath();
-                ctx.arc(projectilePos.x, projectilePos.y, 4, 0, Math.PI * 2);
+                ctx.arc(targetCenterX, targetCenterY, explosionSize, 0, Math.PI * 2);
                 ctx.fill();
                 
-                // Add glow effect
-                ctx.shadowColor = '#FFFF00';
-                ctx.shadowBlur = 8;
+                // Inner explosion
+                ctx.fillStyle = `rgba(255, 255, 0, ${alpha * 0.8})`;
                 ctx.beginPath();
-                ctx.arc(projectilePos.x, projectilePos.y, 2, 0, Math.PI * 2);
+                ctx.arc(targetCenterX, targetCenterY, explosionSize * 0.6, 0, Math.PI * 2);
                 ctx.fill();
-                ctx.shadowBlur = 0;
-                
-                // Draw impact effect when projectile reaches target
-                if (unit.animationProgress > 0.8) {
-                    const targetCenterX = unit.projectileTargetX * TILE_SIZE + TILE_SIZE / 2;
-                    const targetCenterY = unit.projectileTargetY * TILE_SIZE + TILE_SIZE / 2;
-                    
-                    // Impact explosion
-                    const explosionSize = (unit.animationProgress - 0.8) * 20;
-                    const alpha = 1 - (unit.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 death particles
-        if (unit.isAnimating && unit.animationType === 'dying' && unit.deathParticles) {
-            unit.deathParticles.forEach(particle => {
-                const alpha = particle.life / particle.maxLife;
-                const size = particle.size * 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]);
                 
-                ctx.fillStyle = particle.color;
-                ctx.globalAlpha = alpha;
+                const reticleSize = UNIT_RADIUS + 8;
                 ctx.beginPath();
-                ctx.arc(particle.x, particle.y, size, 0, Math.PI * 2);
-                ctx.fill();
+                ctx.arc(centerX, centerY, reticleSize, 0, Math.PI * 2);
+                ctx.stroke();
                 
-                // Add glow effect
-                ctx.shadowColor = particle.color;
-                ctx.shadowBlur = size * 2;
+                // Draw crosshairs
                 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 !== unit.id &&
-            unit.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 - unit.x) + Math.abs(selectedUnit.y - unit.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([]);
-                }
+                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 = unit.owner === 'player' ? COLORS.player : COLORS.enemy;
-        ctx.strokeStyle = (model.uiState.type === 'UnitSelected' && model.uiState.unitId === unit.id)
-            ? COLORS.selectedBorder
-            : COLORS.unitBorder;
-        ctx.lineWidth = 2;
+    // 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;
+    }
 
-        // Add pending damage indicator
-        if (unit.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();
         }
-
-        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);
+        // 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();
+        }
         
-        // HP bar
-        const hpPercentage = unit.currentHp / unit.maxHp;
-        ctx.fillStyle = hpPercentage > 0.5 ? '#4CAF50' : hpPercentage > 0.25 ? '#FF9800' : '#F44336';
-        ctx.fillRect(hpBarX, hpBarY, hpBarWidth * hpPercentage, hpBarHeight);
+        // 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();
         
-        // HP text
-        ctx.fillStyle = '#fff';
-        ctx.font = '10px Arial';
+        // Draw turn indicator text
+        ctx.fillStyle = sanitizedUnit.owner === 'player' ? '#00FF00' : '#FF0000';
+        ctx.font = '14px Arial';
         ctx.textAlign = 'center';
-        ctx.fillText(`${unit.currentHp}/${unit.maxHp}`, centerX, hpBarY - 2);
+        ctx.fillText(sanitizedUnit.owner === 'player' ? 'YOUR TURN' : 'ENEMY TURN', centerX, centerY + UNIT_RADIUS + 25);
         
-        // Draw action indicators
-        if (unit.owner === 'player') {
-            // Movement indicator
-            if (!unit.hasMoved) {
-                ctx.fillStyle = '#4CAF50';
-                ctx.beginPath();
-                ctx.arc(centerX - 8, centerY + UNIT_RADIUS + 5, 3, 0, Math.PI * 2);
-                ctx.fill();
-            }
+        // 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();
             
-            // Attack indicator
-            if (!unit.hasAttacked) {
-                ctx.fillStyle = unit.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();
-            }
+            // Draw action feedback text
+            ctx.fillStyle = `rgba(255, 255, 0, ${feedbackAlpha})`;
+            ctx.font = '12px Arial';
+            ctx.textAlign = 'center';
             
-            // Phase indicator text
-            if (model.uiState.type === 'UnitSelected' && model.uiState.unitId === unit.id) {
-                ctx.fillStyle = '#fff';
-                ctx.font = '12px Arial';
-                ctx.textAlign = 'center';
-                if (!unit.hasMoved) {
-                    ctx.fillText('MOVE', centerX, centerY - UNIT_RADIUS - 15);
-                } else if (!unit.hasAttacked) {
-                    ctx.fillText('ATTACK', centerX, centerY - UNIT_RADIUS - 15);
+            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);
             }
         }
-
-        // Draw movement path preview
-        if (unit.isAnimating && unit.animationType === 'moving' && unit.path.length > 0) {
-            ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)';
+        
+        // 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.setLineDash([5, 5]);
-            
             ctx.beginPath();
-            ctx.moveTo(unit.x * TILE_SIZE + TILE_SIZE / 2, unit.y * TILE_SIZE + TILE_SIZE / 2);
-            
-            for (const pathPoint of unit.path) {
-                ctx.lineTo(pathPoint.x * TILE_SIZE + TILE_SIZE / 2, pathPoint.y * TILE_SIZE + TILE_SIZE / 2);
-            }
-            
+            ctx.arc(centerX, centerY, UNIT_RADIUS + 12, 0, Math.PI * 2);
             ctx.stroke();
-            ctx.setLineDash([]);
+            
+            // 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);
         }
-    } catch (error) {
-        console.error('Error drawing unit:', unit.id, error);
-        return; // Skip this unit
     }
-});
-}
+    
+    // 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.
@@ -2141,9 +2370,69 @@ function view(model, canvas, ctx) {
     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
 // ------------------------------------------------
@@ -2172,15 +2461,22 @@ function App() {
      * Updates button states based on current game state
      */
     function updateButtonStates() {
-        const playerUnits = model.units.filter(u => u.owner === 'player');
-        const hasUnfinishedUnits = playerUnits.some(u => !u.hasMoved || !u.hasAttacked);
-        const hasAnimatingUnits = model.units.some(u => u.isAnimating);
+        const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex);
+        const isPlayerTurn = currentUnit && currentUnit.owner === 'player';
         
-        endTurnBtn.disabled = hasUnfinishedUnits || hasAnimatingUnits;
+        // 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
+        }
         
-        if (model.uiState.type === 'UnitSelected') {
+        // 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) {
+            if (selectedUnit && selectedUnit.id === currentUnit.id) {
                 skipMovementBtn.disabled = selectedUnit.hasMoved || selectedUnit.isAnimating;
                 skipAttackBtn.disabled = selectedUnit.hasAttacked || selectedUnit.isAnimating;
             } else {
@@ -2211,13 +2507,79 @@ function App() {
         // 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();
         
-        // Continue loop if there are animations
-        if (model.units.some(u => u.isAnimating)) {
-            animationId = requestAnimationFrame(gameLoop);
+        // 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();
         }
     }
 
@@ -2226,16 +2588,26 @@ function App() {
      * @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();
         }
@@ -2253,14 +2625,7 @@ function App() {
             return;
         }
 
-        if (model.turn === 'enemy') {
-            setTimeout(() => {
-                console.log("Enemy turn ends.");
-                const nextUnits = model.units.map(u => ({...u}));
-                model = { ...model, turn: 'player', units: nextUnits };
-                dispatch({ type: 'NO_OP' }); // Dispatch a dummy event to trigger re-render
-            }, 500);
-        }
+
     }
     
     // Setup Event Listeners
@@ -2322,8 +2687,22 @@ function checkLineOfSight(startX, startY, endX, endY, grid, units) {
     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;
@@ -2344,7 +2723,8 @@ function checkLineOfSight(startX, startY, endX, endY, grid, units) {
                 blocked: true, 
                 blocker: null, 
                 obstacleX: checkX, 
-                obstacleY: checkY 
+                obstacleY: checkY,
+                reason: 'obstacle'
             };
         }
         
@@ -2358,12 +2738,13 @@ function checkLineOfSight(startX, startY, endX, endY, grid, units) {
                 blocked: true, 
                 blocker: blockingUnit, 
                 obstacleX: null, 
-                obstacleY: null 
+                obstacleY: null,
+                reason: 'unit'
             };
         }
     }
     
-    return { blocked: false, blocker: null, obstacleX: null, obstacleY: null };
+    return { blocked: false, blocker: null, obstacleX: null, obstacleY: null, reason: 'clear' };
 }
 
 /**
@@ -2420,3 +2801,798 @@ function createObstacle(x, y) {
         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;
+}
+
+