diff options
-rwxr-xr-x | awk/scheme/scheme/bin/compiler.awk | 5 | ||||
-rwxr-xr-x | awk/scheme/scheme/bin/vm.awk | 7 | ||||
-rw-r--r-- | html/tower/index.html | 110 | ||||
-rw-r--r-- | html/tower/js/game.js | 227 | ||||
-rw-r--r-- | html/tower/js/gameState.js | 82 | ||||
-rw-r--r-- | html/tower/js/path.js | 83 | ||||
-rw-r--r-- | html/tower/js/renderer.js | 199 |
7 files changed, 705 insertions, 8 deletions
diff --git a/awk/scheme/scheme/bin/compiler.awk b/awk/scheme/scheme/bin/compiler.awk index e7e8081..5371bb2 100755 --- a/awk/scheme/scheme/bin/compiler.awk +++ b/awk/scheme/scheme/bin/compiler.awk @@ -7,7 +7,7 @@ BEGIN { next_label = 0 program = "" - # Debug mode + # Debug mode, 0 off, 1 on DEBUG = 0 } @@ -15,7 +15,7 @@ function debug(msg) { if (DEBUG) printf("[DEBUG] %s\n", msg) > "/dev/stderr" } -# Process input line - just accumulate the raw input +# Process input line - accumulate the raw input { if (program != "") program = program "\n" program = program $0 @@ -29,7 +29,6 @@ END { split_expressions(program) } -# New function to handle multiple expressions function split_expressions(prog, current, paren_count, i, c, expr, cleaned) { current = "" paren_count = 0 diff --git a/awk/scheme/scheme/bin/vm.awk b/awk/scheme/scheme/bin/vm.awk index cb2b992..aa85280 100755 --- a/awk/scheme/scheme/bin/vm.awk +++ b/awk/scheme/scheme/bin/vm.awk @@ -1,6 +1,5 @@ #!/usr/bin/awk -f -# VM State Initialization BEGIN { # Type tags T_NUMBER = "N" @@ -10,24 +9,22 @@ BEGIN { T_FUNCTION = "F" T_NIL = "NIL" - # VM registers + # Registers stack_ptr = 0 # Stack pointer heap_ptr = 0 # Heap pointer pc = 0 # Program counter - # Debug mode + # Debug mode, 0 off, 1 on DEBUG = 0 # Environment for variables env_size = 0 - # Function table (make it persistent) delete func_name delete func_pc delete func_code func_size = 0 - # Call stack call_stack_ptr = 0 # State persistence diff --git a/html/tower/index.html b/html/tower/index.html new file mode 100644 index 0000000..23755fa --- /dev/null +++ b/html/tower/index.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Tower Defense</title> + <style> + body { + margin: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f0f0f0; + } + .game-container { + position: relative; + display: flex; + gap: 20px; + } + #gameCanvas { + border: 2px solid #333; + background-color: white; + } + .tower-palette { + width: 100px; + background: white; + border: 2px solid #333; + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; + } + .tower-option { + width: 80px; + height: 80px; + border: 2px solid #666; + cursor: grab; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + .tower-option:active { + cursor: grabbing; + } + .tower-preview { + width: 40px; + height: 40px; + margin-bottom: 5px; + } + .tower-cost { + font-size: 12px; + color: #666; + } + .start-button { + margin-top: 20px; + padding: 10px; + background-color: #2ecc71; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + width: 100%; + } + .start-button:hover { + background-color: #27ae60; + } + .start-button:disabled { + background-color: #95a5a6; + cursor: not-allowed; + } + </style> +</head> +<body> + <div class="game-container"> + <div class="tower-palette"> + <div class="tower-option" draggable="true" data-tower-type="BASIC"> + <div class="tower-preview" style="background: #4a90e2;"></div> + <div class="tower-cost">$20</div> + </div> + <div class="tower-option" draggable="true" data-tower-type="SNIPER"> + <div class="tower-preview" style="background: #9b59b6;"></div> + <div class="tower-cost">$40</div> + </div> + <div class="tower-option" draggable="true" data-tower-type="RAPID"> + <div class="tower-preview" style="background: #2ecc71;"></div> + <div class="tower-cost">$35</div> + </div> + <button id="startCombat" class="start-button">Start Combat</button> + </div> + <canvas id="gameCanvas" width="600" height="600"></canvas> + </div> + + <!-- Core game modules --> + <script src="js/grid.js"></script> + <script src="js/path.js"></script> + <script src="js/tower.js"></script> + <script src="js/enemy.js"></script> + + <!-- Rendering modules --> + <script src="js/renderer.js"></script> + + <!-- Game state and main loop --> + <script src="js/gameState.js"></script> + <script src="js/game.js"></script> +</body> +</html> \ No newline at end of file diff --git a/html/tower/js/game.js b/html/tower/js/game.js new file mode 100644 index 0000000..e36c291 --- /dev/null +++ b/html/tower/js/game.js @@ -0,0 +1,227 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); + +// Initialize game state +let gameState = { + grid: Array(20).fill().map(() => Array(20).fill('empty')), + path: [], + towers: [], + enemies: [], + playerCurrency: 100, + phase: 'placement', + isGameOver: false, + particles: [], + projectiles: [] +}; + +let lastTimestamp = 0; +const ENEMY_SPAWN_INTERVAL = 1000; // 1 second +let lastEnemySpawn = 0; +let enemiesRemaining = 0; + +let draggedTowerType = null; +let hoverCell = null; + +function gameLoop(timestamp) { + const deltaTime = timestamp - lastTimestamp; + lastTimestamp = timestamp; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (gameState.phase === GamePhase.COMBAT) { + // Spawn enemies + if (enemiesRemaining > 0 && timestamp - lastEnemySpawn > ENEMY_SPAWN_INTERVAL) { + gameState.enemies.push(createEnemy({ x: 0, y: gameState.path[0].y })); + lastEnemySpawn = timestamp; + enemiesRemaining--; + } + + // Update enemy positions + gameState.enemies.forEach(enemy => { + if (enemy.pathIndex < gameState.path.length - 1) { + const targetPos = gameState.path[enemy.pathIndex + 1]; + const dx = targetPos.x - enemy.position.x; + const dy = targetPos.y - enemy.position.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < enemy.speed * deltaTime / 1000) { + enemy.position = { ...targetPos }; + enemy.pathIndex++; + } else { + enemy.position.x += (dx / distance) * enemy.speed * deltaTime / 1000; + enemy.position.y += (dy / distance) * enemy.speed * deltaTime / 1000; + } + } + }); + + // Update particles + gameState.particles = gameState.particles.filter(particle => { + const age = timestamp - particle.createdAt; + if (age > particle.lifetime) return false; + + particle.position.x += particle.velocity.x * deltaTime; + particle.position.y += particle.velocity.y * deltaTime; + return true; + }); + + // Update projectiles + gameState.projectiles = gameState.projectiles.filter(projectile => { + return timestamp - projectile.createdAt < projectile.lifetime; + }); + + // Process tower attacks + gameState.towers.forEach(tower => { + if (timestamp - tower.lastAttackTime > 1000 / tower.attackSpeed) { + const enemiesInRange = gameState.enemies.filter(enemy => { + const dx = enemy.position.x - tower.position.x; + const dy = enemy.position.y - tower.position.y; + return Math.sqrt(dx * dx + dy * dy) <= tower.range; + }); + + if (enemiesInRange.length > 0) { + const target = enemiesInRange[0]; + + // Create projectile + gameState.projectiles.push({ + startPos: tower.position, + targetPos: target.position, + createdAt: timestamp, + lifetime: 300 // 300ms travel time + }); + + // Apply damage + target.currentHealth -= tower.damage; + tower.lastAttackTime = timestamp; + + // Create death particles if enemy dies + if (target.currentHealth <= 0) { + const cellSize = canvas.width / 20; + const centerX = (target.position.x + 0.5) * cellSize; + const centerY = (target.position.y + 0.5) * cellSize; + + // Create explosion particles + for (let i = 0; i < 12; i++) { + const angle = (Math.PI * 2 * i) / 12; + gameState.particles.push( + createParticle( + ParticleTypes.DEATH_PARTICLE, + { x: centerX, y: centerY }, + angle + ) + ); + } + } + } + } + }); + + // Remove dead enemies + gameState.enemies = gameState.enemies.filter(enemy => enemy.currentHealth > 0); + } + + // Render game state + renderGrid(ctx, gameState.grid); + renderProjectiles(ctx, gameState.projectiles); + renderEnemies(ctx, gameState.enemies); + renderTowers(ctx, gameState.towers); + renderParticles(ctx, gameState.particles); + renderUI(ctx, gameState); + + // Request next frame + requestAnimationFrame(gameLoop); +} + +// Start the game +generatePath(gameState.grid).then(path => { + gameState.path = path; + enemiesRemaining = Math.floor(Math.random() * 26) + 5; // 5-30 enemies + initializeEventListeners(); + requestAnimationFrame(gameLoop); +}); + +function startCombat() { + if (gameState.phase === GamePhase.PLACEMENT && gameState.towers.length > 0) { + gameState.phase = GamePhase.COMBAT; + document.getElementById('startCombat').disabled = true; + + // Disable tower palette + document.querySelectorAll('.tower-option').forEach(option => { + option.draggable = false; + option.style.cursor = 'not-allowed'; + option.style.opacity = '0.5'; + }); + } +} + +// Add event listeners for tower placement +function initializeEventListeners() { + // Tower palette drag events + document.querySelectorAll('.tower-option').forEach(option => { + option.addEventListener('dragstart', (e) => { + draggedTowerType = e.target.dataset.towerType; + e.dataTransfer.setData('text/plain', ''); // Required for Firefox + }); + + option.addEventListener('dragend', () => { + draggedTowerType = null; + hoverCell = null; + }); + }); + + // Canvas drag events + canvas.addEventListener('dragover', (e) => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / (canvas.width / 20)); + const y = Math.floor((e.clientY - rect.top) / (canvas.height / 20)); + + if (x >= 0 && x < 20 && y >= 0 && y < 20) { + hoverCell = { x, y }; + } else { + hoverCell = null; + } + }); + + canvas.addEventListener('dragleave', () => { + hoverCell = null; + }); + + canvas.addEventListener('drop', (e) => { + e.preventDefault(); + if (!draggedTowerType || !hoverCell) return; + + const tower = TowerTypes[draggedTowerType]; + if ( + gameState.grid[hoverCell.y][hoverCell.x] === 'empty' && + gameState.playerCurrency >= tower.cost + ) { + gameState.grid[hoverCell.y][hoverCell.x] = 'tower'; + gameState.towers.push(createTower(draggedTowerType, { ...hoverCell })); + gameState.playerCurrency -= tower.cost; + } + + draggedTowerType = null; + hoverCell = null; + }); + + // Replace the space key event with button click + document.getElementById('startCombat').addEventListener('click', startCombat); + + // Update button state when towers are placed + const updateStartButton = () => { + const button = document.getElementById('startCombat'); + button.disabled = gameState.towers.length === 0; + }; + + // Add tower placement observer + const originalPush = gameState.towers.push; + gameState.towers.push = function(...args) { + const result = originalPush.apply(this, args); + updateStartButton(); + return result; + }; + + // Initial button state + updateStartButton(); +} \ No newline at end of file diff --git a/html/tower/js/gameState.js b/html/tower/js/gameState.js new file mode 100644 index 0000000..a1ab765 --- /dev/null +++ b/html/tower/js/gameState.js @@ -0,0 +1,82 @@ +const GamePhase = { + PLACEMENT: 'placement', + COMBAT: 'combat' +}; + +const TowerTypes = { + BASIC: { + name: 'Basic Tower', + cost: 20, + range: 3, + damage: 1, + attackSpeed: 1, + color: '#4a90e2' + }, + SNIPER: { + name: 'Sniper Tower', + cost: 40, + range: 6, + damage: 2, + attackSpeed: 0.5, + color: '#9b59b6' + }, + RAPID: { + name: 'Rapid Tower', + cost: 35, + range: 2, + damage: 0.5, + attackSpeed: 2, + color: '#2ecc71' + } +}; + +const ParticleTypes = { + DEATH_PARTICLE: { + lifetime: 1000, // milliseconds + speed: 0.1, + colors: ['#e74c3c', '#c0392b', '#f1c40f', '#e67e22'] + }, + PROJECTILE: { + lifetime: 300, + speed: 0.3, + color: '#ffffff' + } +}; + +function createTower(type, position) { + return { + ...TowerTypes[type], + position, + lastAttackTime: 0 + }; +} + +function createEnemy(startPosition) { + return { + position: { ...startPosition }, + currentHealth: Math.floor(Math.random() * 5) + 2, // 2-6 health + maxHealth: this.currentHealth, + speed: 1 + Math.random() * 0.5, // 1-1.5 speed + pathIndex: 0 + }; +} + +function createParticle(type, position, angle) { + return { + position: { ...position }, + velocity: { + x: Math.cos(angle) * type.speed, + y: Math.sin(angle) * type.speed + }, + color: Array.isArray(type.colors) + ? type.colors[Math.floor(Math.random() * type.colors.length)] + : type.color, + createdAt: performance.now(), + lifetime: type.lifetime, + size: 3 + Math.random() * 2 + }; +} + +// Add to gameState object +gameState.particles = []; +gameState.projectiles = []; \ No newline at end of file diff --git a/html/tower/js/path.js b/html/tower/js/path.js new file mode 100644 index 0000000..f594ee2 --- /dev/null +++ b/html/tower/js/path.js @@ -0,0 +1,83 @@ +// Path generation using a modified depth-first search algorithm +function generatePath(grid) { + const width = grid[0].length; + const height = grid.length; + + // Pick random start point on left edge + const startY = Math.floor(Math.random() * height); + let currentPos = { x: 0, y: startY }; + + // Initialize path with start position + const path = [currentPos]; + grid[startY][0] = 'path'; + + function getValidMoves(pos) { + const moves = []; + const directions = [ + { x: 1, y: 0 }, // right + { x: 0, y: -1 }, // up + { x: 0, y: 1 } // down + ]; + + for (const dir of directions) { + const newX = pos.x + dir.x; + const newY = pos.y + dir.y; + + // Check bounds + if (newX < 0 || newX >= width || newY < 0 || newY >= height) { + continue; + } + + // Check if cell is empty and not adjacent to path (except previous cell) + if (grid[newY][newX] === 'empty' && !hasAdjacentPath(newX, newY, grid)) { + moves.push({ x: newX, y: newY }); + } + } + + return moves; + } + + function hasAdjacentPath(x, y, grid) { + const adjacentCells = [ + { x: x, y: y - 1 }, // up + { x: x, y: y + 1 }, // down + { x: x - 1, y: y }, // left + { x: x + 1, y: y }, // right + ]; + + return adjacentCells.some(cell => { + if (cell.x < 0 || cell.x >= width || cell.y < 0 || cell.y >= height) { + return false; + } + return grid[cell.y][cell.x] === 'path' && + !path.some(p => p.x === cell.x && p.y === cell.y); + }); + } + + // Generate path until we reach the right edge + while (currentPos.x < width - 1) { + const moves = getValidMoves(currentPos); + + if (moves.length === 0) { + // If no valid moves, backtrack + if (path.length <= 1) { + // Start over if we can't backtrack + return generatePath(grid); + } + + path.pop(); + const lastPos = path[path.length - 1]; + grid[currentPos.y][currentPos.x] = 'empty'; + currentPos = lastPos; + continue; + } + + // Choose random valid move + const nextMove = moves[Math.floor(Math.random() * moves.length)]; + currentPos = nextMove; + path.push(currentPos); + grid[currentPos.y][currentPos.x] = 'path'; + } + + return Promise.resolve(path); +} \ No newline at end of file diff --git a/html/tower/js/renderer.js b/html/tower/js/renderer.js new file mode 100644 index 0000000..10444aa --- /dev/null +++ b/html/tower/js/renderer.js @@ -0,0 +1,199 @@ +function renderGrid(ctx, grid) { + const cellSize = canvas.width / 20; + + // Draw grid lines + ctx.strokeStyle = '#ccc'; + ctx.lineWidth = 1; + + for (let i = 0; i <= 20; i++) { + // Vertical lines + ctx.beginPath(); + ctx.moveTo(i * cellSize, 0); + ctx.lineTo(i * cellSize, canvas.height); + ctx.stroke(); + + // Horizontal lines + ctx.beginPath(); + ctx.moveTo(0, i * cellSize); + ctx.lineTo(canvas.width, i * cellSize); + ctx.stroke(); + } + + // Draw cells + grid.forEach((row, y) => { + row.forEach((cell, x) => { + if (cell === 'path') { + ctx.fillStyle = '#f4a460'; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + }); + }); + + // Draw hover preview + if (gameState.phase === GamePhase.PLACEMENT && draggedTowerType && hoverCell) { + const tower = TowerTypes[draggedTowerType]; + const canPlace = grid[hoverCell.y][hoverCell.x] === 'empty' && + gameState.playerCurrency >= tower.cost; + + ctx.fillStyle = canPlace ? tower.color + '80' : 'rgba(255, 0, 0, 0.3)'; + ctx.fillRect( + hoverCell.x * cellSize, + hoverCell.y * cellSize, + cellSize, + cellSize + ); + + // Draw range preview + ctx.beginPath(); + ctx.arc( + (hoverCell.x + 0.5) * cellSize, + (hoverCell.y + 0.5) * cellSize, + tower.range * cellSize, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = canPlace ? tower.color + '40' : 'rgba(255, 0, 0, 0.2)'; + ctx.stroke(); + } +} + +function renderEnemies(ctx, enemies) { + const cellSize = canvas.width / 20; + + enemies.forEach(enemy => { + // Draw enemy health bar + const healthPercent = enemy.currentHealth / enemy.maxHealth; + const barWidth = cellSize * 0.8; + const barHeight = 4; + + ctx.fillStyle = '#e74c3c'; + ctx.fillRect( + (enemy.position.x + 0.1) * cellSize, + (enemy.position.y - 0.1) * cellSize - barHeight, + barWidth, + barHeight + ); + + ctx.fillStyle = '#2ecc71'; + ctx.fillRect( + (enemy.position.x + 0.1) * cellSize, + (enemy.position.y - 0.1) * cellSize - barHeight, + barWidth * healthPercent, + barHeight + ); + + // Draw enemy + ctx.fillStyle = 'red'; + ctx.beginPath(); + ctx.arc( + (enemy.position.x + 0.5) * cellSize, + (enemy.position.y + 0.5) * cellSize, + cellSize / 3, + 0, + Math.PI * 2 + ); + ctx.fill(); + }); +} + +function renderUI(ctx, gameState) { + ctx.fillStyle = 'black'; + ctx.font = '20px Arial'; + ctx.fillText(`Currency: ${gameState.playerCurrency}`, 10, 30); + ctx.fillText(`Phase: ${gameState.phase}`, 10, 60); +} + +function renderTowers(ctx, towers) { + const cellSize = canvas.width / 20; + + towers.forEach(tower => { + // Draw tower body + ctx.fillStyle = tower.color; + ctx.fillRect( + tower.position.x * cellSize + cellSize * 0.1, + tower.position.y * cellSize + cellSize * 0.1, + cellSize * 0.8, + cellSize * 0.8 + ); + + // Draw range indicator (only during placement phase) + if (gameState.phase === GamePhase.PLACEMENT) { + ctx.beginPath(); + ctx.arc( + (tower.position.x + 0.5) * cellSize, + (tower.position.y + 0.5) * cellSize, + tower.range * cellSize, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = tower.color + '40'; // 40 is hex for 25% opacity + ctx.stroke(); + } + }); +} + +// Add new render function for particles +function renderParticles(ctx, particles) { + particles.forEach(particle => { + const age = performance.now() - particle.createdAt; + const lifePercent = age / particle.lifetime; + + if (lifePercent <= 1) { + ctx.globalAlpha = 1 - lifePercent; + ctx.fillStyle = particle.color; + ctx.beginPath(); + ctx.arc( + particle.position.x, + particle.position.y, + particle.size * (1 - lifePercent), + 0, + Math.PI * 2 + ); + ctx.fill(); + } + }); + ctx.globalAlpha = 1; +} + +// Add new render function for projectiles +function renderProjectiles(ctx, projectiles) { + const cellSize = canvas.width / 20; + + projectiles.forEach(projectile => { + const age = performance.now() - projectile.createdAt; + const progress = age / projectile.lifetime; + + if (progress <= 1) { + // Draw projectile trail + ctx.beginPath(); + ctx.moveTo( + projectile.startPos.x * cellSize + cellSize / 2, + projectile.startPos.y * cellSize + cellSize / 2 + ); + + const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress; + const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress; + + ctx.lineTo( + currentX * cellSize + cellSize / 2, + currentY * cellSize + cellSize / 2 + ); + + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw projectile head + ctx.beginPath(); + ctx.arc( + currentX * cellSize + cellSize / 2, + currentY * cellSize + cellSize / 2, + 4, + 0, + Math.PI * 2 + ); + ctx.fillStyle = '#fff'; + ctx.fill(); + } + }); +} \ No newline at end of file |