diff --git a/racetrack/editor/editor.js b/racetrack/editor/editor.js new file mode 100644 index 0000000..1277d42 --- /dev/null +++ b/racetrack/editor/editor.js @@ -0,0 +1,206 @@ +const canvas = document.getElementById('editorCanvas'); +const ctx = canvas.getContext('2d'); +const cellSize = 20; + +const COLORS = { + road: [255, 255, 255], + wall: [0, 0, 255], + start: [0, 255, 255], + finish: [0, 0, 0], + gravel: [255, 255, 0], + curb: [127, 127, 127] +}; + +let currentColor = 'road'; +let grid = []; +let cols = 40; +let rows = 30; + +function triggerFileInput() { + document.getElementById('fileInput').click(); +} +document.getElementById('fileInput').addEventListener('change', loadFromFile); + +function loadFromFile() { + const input = document.getElementById('fileInput'); + if (!input.files.length) return; + + const file = input.files[0]; + const reader = new FileReader(); + + reader.onload = function(e) { + const img = new Image(); + img.onload = function() { + const offCanvas = document.createElement('canvas'); + offCanvas.width = img.width; + offCanvas.height = img.height; + const offCtx = offCanvas.getContext('2d'); + offCtx.drawImage(img, 0, 0); + + const imageData = offCtx.getImageData(0, 0, img.width, img.height).data; + cols = img.width; + rows = img.height; + canvas.width = cols * cellSize; + canvas.height = rows * cellSize; + + grid = Array.from({length: rows}, () => Array(cols).fill('wall')); + + const rgbToKey = (r, g, b) => { + for (let key in COLORS) { + const [cr, cg, cb] = COLORS[key]; + if (r === cr && g === cg && b === cb) return key; + } + return 'wall'; // default fallback + }; + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const i = (y * cols + x) * 4; + const r = imageData[i]; + const g = imageData[i + 1]; + const b = imageData[i + 2]; + grid[y][x] = rgbToKey(r, g, b); + } + } + + drawPalette(); + drawGrid(); + }; + + img.src = e.target.result; + }; + + reader.readAsDataURL(file); +} + + +function resetGrid() { + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + grid[y][x] = 'road'; + } + } + drawGrid(); +} + +function initGridEditor(width, height) { + cols = width; + rows = height; + canvas.width = cols * cellSize; + canvas.height = rows * cellSize; + + grid = Array.from({length: rows}, () => Array(cols).fill('road')); + + drawPalette(); + drawGrid(); +} + +function drawPalette() { + const paletteDiv = document.getElementById('palette'); + paletteDiv.innerHTML = ''; // Clear existing + + for (let key in COLORS) { + const div = document.createElement('div'); + div.classList.add('palette-color'); + if (key === currentColor) div.classList.add('selected'); + div.dataset.color = key; + + const box = document.createElement('div'); + box.classList.add('palette-color-box'); + box.style.backgroundColor = `rgb(${COLORS[key].join(',')})`; + + div.appendChild(box); + div.appendChild(document.createTextNode(key)); + + div.onclick = () => { + document.querySelectorAll('.palette-color') + .forEach(el => el.classList.remove('selected')); + div.classList.add('selected'); + currentColor = key; + }; + + paletteDiv.appendChild(div); + } +} + +function goBackToGame() { + window.location.href = '../index.html'; +} + +function drawGrid() { + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const color = COLORS[grid[y][x]]; + ctx.fillStyle = `rgb(${color.join(',')})`; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + ctx.strokeStyle = '#ccc'; + ctx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } +} + +let isDrawing = false; + +function drawCellAtMouse(e) { + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / cellSize); + const y = Math.floor((e.clientY - rect.top) / cellSize); + if (x >= 0 && x < cols && y >= 0 && y < rows) { + grid[y][x] = currentColor; + drawGrid(); + } +} + +canvas.addEventListener('mousedown', (e) => { + isDrawing = true; + drawCellAtMouse(e); +}); + +canvas.addEventListener('mousemove', (e) => { + if (isDrawing) { + drawCellAtMouse(e); + } +}); + +canvas.addEventListener('mouseup', () => { + isDrawing = false; +}); + +canvas.addEventListener('mouseleave', () => { + isDrawing = false; +}); + +function exportImage() { + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = cols; + exportCanvas.height = rows; + const exportCtx = exportCanvas.getContext('2d'); + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const [r, g, b] = COLORS[grid[y][x]]; + exportCtx.fillStyle = `rgb(${r},${g},${b})`; + exportCtx.fillRect(x, y, 1, 1); + } + } + + const link = document.createElement('a'); + link.download = 'track.png'; + link.href = exportCanvas.toDataURL(); + link.click(); +} + +// Form to set canvas/grid size +document.getElementById('sizeForm').addEventListener('submit', (e) => { + e.preventDefault(); + const width = parseInt(document.getElementById('colsInput').value); + const height = parseInt(document.getElementById('rowsInput').value); + if (width > 0 && height > 0) { + initGridEditor(width, height); + } +}); + +// Start default grid on load +window.onload = () => { + initGridEditor(40, 30); +}; diff --git a/racetrack/editor/index.html b/racetrack/editor/index.html new file mode 100644 index 0000000..661a577 --- /dev/null +++ b/racetrack/editor/index.html @@ -0,0 +1,30 @@ + + + + + Racetrack Editor + + + +

Racetrack Editor

+ + + + + + + + +
+ Width (cells): + Height (cells): + +
+ +
+ + + + + + diff --git a/racetrack/index.html b/racetrack/index.html new file mode 100644 index 0000000..d6fa491 --- /dev/null +++ b/racetrack/index.html @@ -0,0 +1,30 @@ + + + + + Racetrack Game + + + +

Racetrack Game

+ + + + + + + + + + + + + + + + diff --git a/racetrack/script.js b/racetrack/script.js new file mode 100644 index 0000000..e92a4d9 --- /dev/null +++ b/racetrack/script.js @@ -0,0 +1,406 @@ +const canvas = document.getElementById('trackCanvas'); +const ctx = canvas.getContext('2d'); +const cellSize = 20; + +let trackImageData = null; +let trackWidth = 0; +let trackHeight = 0; + +let players = []; +let currentPlayerIndex = 0; +let moveTrails = []; +let hoverTarget = null; +let legalMoves = []; +let selectedMoveIndex = 0; +let previousMovesKey = ''; +const PLAYER_COLORS = ['red', 'blue', 'green', 'orange']; + +function setupPlayers() { + const count = parseInt(document.getElementById('playerCount').value); + players = []; + moveTrails = []; + currentPlayerIndex = 0; + + const starts = []; + + for (let y = 0; y < trackHeight; y++) { + for (let x = 0; x < trackWidth; x++) { + const color = getPixelColor(x, y); + if (color === 'start') starts.push({x, y}); + } + } + + if (starts.length < count) { + alert('Not enough start tiles!'); + return; + } + + for (let i = 0; i < count; i++) { + players.push({ + x: starts[i].x, + y: starts[i].y, + vx: 0, + vy: 0, + alive: true, + color: PLAYER_COLORS[i] + }); + moveTrails.push([]); + } + + redrawGame(); +} + +function getPixelColor(x, y) { + if (!trackImageData || x < 0 || y < 0 || x >= trackWidth || y >= trackHeight) + return 'wall'; + const i = (y * trackWidth + x) * 4; + const r = trackImageData[i], g = trackImageData[i + 1], + b = trackImageData[i + 2]; + const hex = (r << 16) | (g << 8) | b; + switch (hex) { + case 0x0000ff: + return 'wall'; + case 0x7f7f7f: + return 'curb'; + case 0xffff00: + return 'gravel'; + case 0x000000: + return 'finish'; + case 0x00ffff: + return 'start'; + default: + return 'road'; + } +} + +function getLegalMoves() { + const p = players[currentPlayerIndex]; + const moves = []; + const surface = getPixelColor(p.x, p.y); + + for (let ax = -1; ax <= 1; ax++) { + for (let ay = -1; ay <= 1; ay++) { + const vx = p.vx + ax, vy = p.vy + ay; + const x = p.x + vx, y = p.y + vy; + + if (!isInsideTrack(x, y)) continue; + + let allowed = true; + if (surface === 'curb') + allowed = (ax === 0 && ay === 0); + else if (surface === 'gravel') + allowed = Math.sqrt(vx * vx + vy * vy) <= 1.01; + + if (allowed && !checkCollision(p.x, p.y, x, y)) { + moves.push({x, y, vx, vy}); + } + } + } + + selectedMoveIndex = 0; + return moves; +} + +function isInsideTrack(x, y) { + return x >= 0 && y >= 0 && x < trackWidth && y < trackHeight; +} + +function checkCollision(x1, y1, x2, y2) { + const dx = x2 - x1, dy = y2 - y1; + const steps = Math.max(Math.abs(dx), Math.abs(dy)); + for (let i = 0; i <= steps; i++) { + const x = Math.round(x1 + dx * i / steps); + const y = Math.round(y1 + dy * i / steps); + if (getPixelColor(x, y) === 'wall') return true; + } + return false; +} + +function checkFinishCrossed(x1, y1, x2, y2) { + const dx = x2 - x1, dy = y2 - y1; + const steps = Math.max(Math.abs(dx), Math.abs(dy)); + for (let i = 0; i <= steps; i++) { + const x = Math.round(x1 + dx * i / steps); + const y = Math.round(y1 + dy * i / steps); + if (getPixelColor(x, y) === 'finish') return true; + } + return false; +} + +function drawGrid() { + ctx.strokeStyle = '#ddd'; + for (let x = 0; x <= trackWidth; x++) { + ctx.beginPath(); + ctx.moveTo(x * cellSize, 0); + ctx.lineTo(x * cellSize, trackHeight * cellSize); + ctx.stroke(); + } + for (let y = 0; y <= trackHeight; y++) { + ctx.beginPath(); + ctx.moveTo(0, y * cellSize); + ctx.lineTo(trackWidth * cellSize, y * cellSize); + ctx.stroke(); + } +} + +function drawTrack() { + for (let y = 0; y < trackHeight; y++) { + for (let x = 0; x < trackWidth; x++) { + const i = (y * trackWidth + x) * 4; + drawCellByColor( + x, y, trackImageData[i], trackImageData[i + 1], + trackImageData[i + 2]); + } + } +} + +function drawCellByColor(x, y, r, g, b) { + const hex = (r << 16) | (g << 8) | b; + let fill = null; + switch (hex) { + case 0x00ffff: + fill = 'cyan'; + break; + case 0xffffff: + fill = 'white'; + break; + case 0x7f7f7f: + fill = 'gray'; + break; + case 0xffff00: + fill = 'yellow'; + break; + case 0x0000ff: + fill = 'blue'; + break; + case 0x000000: + fill = 'black'; + break; + default: + return; + } + ctx.fillStyle = fill; + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); +} + +function drawTrail() { + players.forEach((p, i) => { + if (!p.alive) return; + const trail = moveTrails[i]; + if (trail.length === 0) return; + ctx.strokeStyle = p.color; + ctx.beginPath(); + ctx.moveTo((trail[0].x + 0.5) * cellSize, (trail[0].y + 0.5) * cellSize); + trail.forEach( + t => ctx.lineTo((t.x + 0.5) * cellSize, (t.y + 0.5) * cellSize)); + ctx.lineTo((p.x + 0.5) * cellSize, (p.y + 0.5) * cellSize); + ctx.stroke(); + }); +} + +function drawPlayers() { + players.forEach((p, i) => { + if (!p.alive) return; + ctx.fillStyle = (i === currentPlayerIndex) ? + (hoverTarget ? rgba(p.color, 0.5) : p.color) : + rgba(p.color, 0.4); + const drawX = hoverTarget && i === currentPlayerIndex ? hoverTarget.x : p.x; + const drawY = hoverTarget && i === currentPlayerIndex ? hoverTarget.y : p.y; + ctx.beginPath(); + ctx.arc( + (drawX + 0.5) * cellSize, (drawY + 0.5) * cellSize, cellSize / 3, 0, + 2 * Math.PI); + ctx.fill(); + }); +} + +function drawLegalMoves() { + const p = players[currentPlayerIndex]; + const playerColor = p.color; + const validSet = new Set(legalMoves.map(m => `${m.x},${m.y}`)); + + for (let ax = -1; ax <= 1; ax++) { + for (let ay = -1; ay <= 1; ay++) { + const vx = p.vx + ax, vy = p.vy + ay; + const x = p.x + vx, y = p.y + vy; + if (!isInsideTrack(x, y)) continue; + + const key = `${x},${y}`; + const isValid = validSet.has(key); + const isSelected = legalMoves[selectedMoveIndex] && + legalMoves[selectedMoveIndex].x === x && + legalMoves[selectedMoveIndex].y === y; + + ctx.strokeStyle = isSelected ? playerColor : + isValid ? playerColor : + 'gray'; + + ctx.lineWidth = isSelected ? 3 : 1; + ctx.setLineDash(isValid ? [] : [3, 3]); + ctx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize); + ctx.setLineDash([]); + } + } +} + + +function drawHoverPreview() { + if (!hoverTarget) return; + + const p = players[currentPlayerIndex]; + ctx.setLineDash([4, 4]); + ctx.strokeStyle = p.color; + ctx.beginPath(); + ctx.moveTo((p.x + 0.5) * cellSize, (p.y + 0.5) * cellSize); + ctx.lineTo( + (hoverTarget.x + 0.5) * cellSize, (hoverTarget.y + 0.5) * cellSize); + ctx.stroke(); + ctx.setLineDash([]); +} + + +function rgba(color, alpha = 1) { + const COLOR_MAP = { + red: [255, 0, 0], + blue: [0, 0, 255], + green: [0, 128, 0], + orange: [255, 165, 0] + }; + + const rgb = COLOR_MAP[color] || [0, 0, 0]; + return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`; +} + + +function redrawGame() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + legalMoves = getLegalMoves(); + drawTrack(); + drawGrid(); + drawTrail(); + drawHoverPreview(); + drawPlayers(); + drawLegalMoves(); + + if (players[currentPlayerIndex].alive && legalMoves.length === 0) { + alert(`Player ${currentPlayerIndex + 1} crashed!`); + players[currentPlayerIndex].alive = false; + advanceTurn(); + redrawGame(); + } +} + +function advanceTurn() { + const alive = players.filter(p => p.alive); + if (alive.length <= 1) return; // Don't rotate if solo or winner + + do { + currentPlayerIndex = (currentPlayerIndex + 1) % players.length; + } while (!players[currentPlayerIndex].alive); + hoverTarget = null; +} + + +canvas.addEventListener('mousemove', (e) => { + const p = players[currentPlayerIndex]; + if (!p || !p.alive) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / cellSize); + const y = Math.floor((e.clientY - rect.top) / cellSize); + const match = legalMoves.find(m => m.x === x && m.y === y); + hoverTarget = match ? {x, y} : null; + redrawGame(); +}); + +canvas.addEventListener('click', (e) => { + const p = players[currentPlayerIndex]; + if (!p || !p.alive) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / cellSize); + const y = Math.floor((e.clientY - rect.top) / cellSize); + const move = legalMoves.find(m => m.x === x && m.y === y); + if (move) makeMove(p, move); +}); + +document.addEventListener('keydown', (e) => { + if (legalMoves.length === 0) return; + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + selectedMoveIndex = (selectedMoveIndex + 1) % legalMoves.length; + redrawGame(); + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + selectedMoveIndex = + (selectedMoveIndex - 1 + legalMoves.length) % legalMoves.length; + redrawGame(); + } else if (e.key === 'Enter') { + const move = legalMoves[selectedMoveIndex]; + if (move) makeMove(players[currentPlayerIndex], move); + } +}); + +function makeMove(player, move) { + moveTrails[currentPlayerIndex].push({x: player.x, y: player.y}); + const prevX = player.x, prevY = player.y; + player.x = move.x; + player.y = move.y; + player.vx = move.vx; + player.vy = move.vy; + if (checkFinishCrossed(prevX, prevY, move.x, move.y)) { + alert(`Player ${currentPlayerIndex + 1} wins!`); + players.forEach(p => p.alive = false); + } else { + advanceTurn(); + } + redrawGame(); +} + +function loadTrack(trackName) { + const image = new Image(); + image.src = `tracks/${trackName}`; + image.onload = () => { + const width = image.width, height = image.height; + canvas.width = width * cellSize; + canvas.height = height * cellSize; + trackWidth = width; + trackHeight = height; + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.drawImage(image, 0, 0); + trackImageData = tempCtx.getImageData(0, 0, width, height).data; + setupPlayers(); // auto-start + }; +} + +function populateTrackList() { + fetch('tracks/tracks.json').then(res => res.json()).then(tracks => { + const select = document.getElementById('trackSelect'); + select.innerHTML = ''; + tracks.forEach(name => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name.replace('.png', ''); + select.appendChild(option); + }); + if (tracks.length > 0) loadTrack(tracks[0]); + select.addEventListener('change', e => { + loadTrack(e.target.value); + }); + }); +} + +function openEditor() { + window.location.href = 'editor/index.html'; +} + +window.onload = () => { + resizeCanvas(); + populateTrackList(); +}; + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} + +window.addEventListener('resize', resizeCanvas); diff --git a/racetrack/serve_app.sh b/racetrack/serve_app.sh new file mode 100755 index 0000000..ce0d837 --- /dev/null +++ b/racetrack/serve_app.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python3 -m http.server 8000 \ No newline at end of file diff --git a/racetrack/style.css b/racetrack/style.css new file mode 100644 index 0000000..66f45c8 --- /dev/null +++ b/racetrack/style.css @@ -0,0 +1,70 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + background: #f7f7f7; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; +} + +h1 { + margin-top: 10px; + text-align: center; +} + +form, #palette, #trackSelect, button { + margin: 10px; +} + +canvas { + border: 1px solid #333; + background: white; + display: block; + margin: 10px auto; + max-width: 100%; +} + +#palette { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.palette-color { + display: flex; + align-items: center; + border: 2px solid transparent; + margin: 5px; + cursor: pointer; + padding: 2px 6px; + background: #fff; + font-size: 14px; +} + +.palette-color-box { + width: 20px; + height: 20px; + margin-right: 6px; +} + +.palette-color.selected { + border-color: black; +} + +button, select, input[type="number"] { + padding: 6px 12px; + font-size: 14px; + border-radius: 4px; + border: 1px solid #ccc; +} + +button:hover { + background-color: #eee; + cursor: pointer; +} diff --git a/racetrack/tracks/oval.png b/racetrack/tracks/oval.png new file mode 100644 index 0000000..94d699a Binary files /dev/null and b/racetrack/tracks/oval.png differ diff --git a/racetrack/tracks/ovalsand.png b/racetrack/tracks/ovalsand.png new file mode 100644 index 0000000..18e7393 Binary files /dev/null and b/racetrack/tracks/ovalsand.png differ diff --git a/racetrack/tracks/s-track.png b/racetrack/tracks/s-track.png new file mode 100644 index 0000000..3c33917 Binary files /dev/null and b/racetrack/tracks/s-track.png differ diff --git a/racetrack/tracks/tracks.json b/racetrack/tracks/tracks.json new file mode 100644 index 0000000..f9bae1f --- /dev/null +++ b/racetrack/tracks/tracks.json @@ -0,0 +1,5 @@ +[ + "oval.png", + "ovalsand.png", + "s-track.png" +]