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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"
+]