race / index.html
LULDev's picture
2d multiplayer race game with scoreboard and chat - Initial Deployment
dac14e4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2D Multiplayer Race Game</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Custom CSS for game elements */
.game-container {
position: relative;
width: 100%;
height: 400px;
background-color: #2c3e50;
overflow: hidden;
}
.track {
position: absolute;
width: 100%;
height: 100%;
background-color: #34495e;
}
.lane {
position: absolute;
width: 100%;
height: 80px;
border-top: 2px dashed rgba(255, 255, 255, 0.2);
border-bottom: 2px dashed rgba(255, 255, 255, 0.2);
}
.player-car {
position: absolute;
width: 60px;
height: 30px;
bottom: 20px;
left: 50px;
z-index: 10;
transition: left 0.1s;
}
.obstacle {
position: absolute;
width: 50px;
height: 50px;
z-index: 5;
}
.finish-line {
position: absolute;
width: 10px;
height: 100%;
right: 50px;
background: repeating-linear-gradient(
to bottom,
white,
white 20px,
black 20px,
black 40px
);
z-index: 1;
}
.car-icon {
font-size: 30px;
}
.chat-message {
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes crash {
0% { transform: rotate(0deg); }
25% { transform: rotate(10deg); }
50% { transform: rotate(-10deg); }
75% { transform: rotate(10deg); }
100% { transform: rotate(0deg); }
}
.crash-animation {
animation: crash 0.5s ease-in-out;
}
</style>
</head>
<body class="bg-gray-900 text-white">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<h1 class="text-4xl font-bold text-center mb-6 text-yellow-400">
<i class="fas fa-flag-checkered mr-2"></i>Multiplayer Race
</h1>
<div class="flex flex-col lg:flex-row gap-6">
<!-- Game Area -->
<div class="flex-1">
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<!-- Scoreboard Header -->
<div class="bg-gray-700 px-4 py-3 flex justify-between items-center">
<h2 class="text-xl font-bold">
<i class="fas fa-trophy mr-2 text-yellow-400"></i>Race Scoreboard
</h2>
<div class="flex items-center space-x-4">
<div class="flex items-center">
<i class="fas fa-users mr-1 text-blue-400"></i>
<span id="player-count">0</span>
</div>
<div class="flex items-center">
<i class="fas fa-clock mr-1 text-green-400"></i>
<span id="game-time">0:00</span>
</div>
</div>
</div>
<!-- Actual Scoreboard -->
<div class="px-4 py-3 bg-gray-800 max-h-40 overflow-y-auto" id="scoreboard">
<div class="flex justify-between py-1 px-2 rounded bg-gray-700 mb-2">
<span class="font-bold">Player</span>
<span class="font-bold">Position</span>
<span class="font-bold">Speed</span>
</div>
<div class="text-center py-3 text-gray-500" id="empty-scoreboard">
Waiting for players to join...
</div>
</div>
<!-- The Game Canvas -->
<div class="game-container relative" id="game-container">
<div class="track">
<div class="lane" style="top: 60px;"></div>
<div class="lane" style="top: 160px;"></div>
<div class="lane" style="top: 260px;"></div>
<div class="finish-line"></div>
<!-- Player cars will be added dynamically -->
</div>
</div>
<!-- Controls -->
<div class="bg-gray-700 px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-2">
<button id="start-game" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg font-medium flex items-center">
<i class="fas fa-play mr-2"></i> Start Game
</button>
<button id="reset-game" class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded-lg font-medium flex items-center">
<i class="fas fa-sync-alt mr-2"></i> Reset
</button>
</div>
<div class="flex items-center space-x-4">
<span id="player-speed">Speed: 0</span>
<span id="player-position">Position: -</span>
</div>
</div>
</div>
</div>
<!-- Chat Area -->
<div class="w-full lg:w-80 flex flex-col">
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden flex-1 flex flex-col">
<div class="bg-gray-700 px-4 py-3">
<h2 class="text-xl font-bold flex items-center">
<i class="fas fa-comments mr-2 text-blue-400"></i>Race Chat
</h2>
</div>
<div class="flex-1 p-4 overflow-y-auto" id="chat-messages">
<div class="text-gray-400 text-center py-8">Send a message to start chatting!</div>
</div>
<div class="p-4 bg-gray-700">
<div class="flex space-x-2">
<input type="text" id="chat-input" placeholder="Type your message..."
class="flex-1 bg-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<button id="send-message" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg font-medium">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<!-- Player List -->
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden mt-4">
<div class="bg-gray-700 px-4 py-3">
<h2 class="text-xl font-bold flex items-center">
<i class="fas fa-users mr-2 text-purple-400"></i>Online Racers
</h2>
</div>
<div class="p-4" id="player-list">
<div class="text-gray-400 text-center py-4">No other players connected</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Game State
const gameState = {
gameRunning: false,
gameTime: 0,
playerId: 'player-' + Math.random().toString(36).substr(2, 9),
playerName: 'Racer ' + Math.floor(Math.random() * 1000),
players: {},
obstacles: [],
chatMessages: [],
keysPressed: {}
};
// DOM Elements
const gameContainer = document.getElementById('game-container');
const scoreboard = document.getElementById('scoreboard');
const emptyScoreboard = document.getElementById('empty-scoreboard');
const playerCount = document.getElementById('player-count');
const gameTime = document.getElementById('game-time');
const playerSpeed = document.getElementById('player-speed');
const playerPosition = document.getElementById('player-position');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const sendMessage = document.getElementById('send-message');
const playerList = document.getElementById('player-list');
const startGame = document.getElementById('start-game');
const resetGame = document.getElementById('reset-game');
// Car colors for players
const carColors = [
'text-red-500', 'text-blue-500', 'text-green-500',
'text-yellow-500', 'text-purple-500', 'text-pink-500'
];
// Initialize game
function initGame() {
// Create player's car
createPlayer();
// Add keyboard event listeners
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Add button event listeners
startGame.addEventListener('click', startGameHandler);
resetGame.addEventListener('click', resetGameHandler);
sendMessage.addEventListener('click', sendChatMessage);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendChatMessage();
});
// Add some demo players (in a real game, these would come from a server)
setTimeout(() => {
addDemoPlayer('Racer-1', 160);
addDemoPlayer('Racer-2', 260);
}, 500);
// Start game loop
gameLoop();
}
// Create player element
function createPlayer() {
const playerCar = document.createElement('div');
playerCar.id = gameState.playerId;
playerCar.className = `player-car ${carColors[0]} car-icon`;
playerCar.innerHTML = '<i class="fas fa-car"></i>';
gameContainer.querySelector('.track').appendChild(playerCar);
gameState.players[gameState.playerId] = {
id: gameState.playerId,
name: gameState.playerName,
x: 50,
y: 60,
speed: 0,
maxSpeed: 10,
acceleration: 0.2,
deceleration: 0.1,
position: 0,
finished: false,
finishTime: 0,
element: playerCar
};
updatePlayerList();
updateScoreboard();
}
// Add some demo players (simulating multiplayer)
function addDemoPlayer(name, yPos) {
const playerId = 'demo-' + Math.random().toString(36).substr(2, 6);
const carColor = carColors[Object.keys(gameState.players).length % carColors.length];
const playerCar = document.createElement('div');
playerCar.className = `player-car ${carColor} car-icon`;
playerCar.innerHTML = '<i class="fas fa-car"></i>';
playerCar.style.top = `${yPos}px`;
playerCar.style.left = '50px';
gameContainer.querySelector('.track').appendChild(playerCar);
gameState.players[playerId] = {
id: playerId,
name: name,
x: 50,
y: yPos,
speed: 0,
maxSpeed: 8 + Math.random() * 4,
acceleration: 0.1 + Math.random() * 0.1,
deceleration: 0.05 + Math.random() * 0.05,
position: 0,
finished: false,
finishTime: 0,
element: playerCar,
isDemo: true
};
const demoPlayer = gameState.players[playerId];
// Demo AI for computer players
setInterval(() => {
if (gameState.gameRunning && !demoPlayer.finished) {
if (Math.random() > 0.3) {
demoPlayer.speed = Math.min(demoPlayer.speed + demoPlayer.acceleration, demoPlayer.maxSpeed);
} else if (Math.random() > 0.7) {
demoPlayer.speed = Math.max(demoPlayer.speed - demoPlayer.deceleration, 0);
}
// Randomly change speed
if (Math.random() > 0.8) {
demoPlayer.speed = Math.max(0, demoPlayer.speed + (Math.random() - 0.5) * 2);
}
}
}, 200);
updatePlayerList();
updateScoreboard();
}
// Handle keyboard input
function handleKeyDown(e) {
gameState.keysPressed[e.key] = true;
}
function handleKeyUp(e) {
gameState.keysPressed[e.key] = false;
}
// Start game handler
function startGameHandler() {
gameState.gameRunning = true;
gameState.gameTime = 0;
startGame.disabled = true;
startGame.classList.remove('bg-green-600', 'hover:bg-green-700');
startGame.classList.add('bg-gray-600', 'cursor-not-allowed');
// Reset all players
for (const playerId in gameState.players) {
const player = gameState.players[playerId];
player.x = 50;
player.speed = 0;
player.finished = false;
player.finishTime = 0;
player.position = 0;
}
addChatMessage('System', 'The race has started! Good luck everyone!', 'text-yellow-400');
}
// Reset game handler
function resetGameHandler() {
gameState.gameRunning = false;
gameState.gameTime = 0;
startGame.disabled = false;
startGame.classList.add('bg-green-600', 'hover:bg-green-700');
startGame.classList.remove('bg-gray-600', 'cursor-not-allowed');
// Reset all players
for (const playerId in gameState.players) {
const player = gameState.players[playerId];
player.x = 50;
player.speed = 0;
player.finished = false;
player.finishTime = 0;
player.position = 0;
player.element.style.left = `${player.x}px`;
}
addChatMessage('System', 'The race has been reset.', 'text-yellow-400');
updateScoreboard();
}
// Main game loop
function gameLoop() {
if (gameState.gameRunning) {
gameState.gameTime += 0.1;
updateGameTimeDisplay();
// Update all players
for (const playerId in gameState.players) {
const player = gameState.players[playerId];
// Player control (only for main player)
if (playerId === gameState.playerId) {
if (gameState.keysPressed['ArrowRight'] || gameState.keysPressed['d']) {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed);
}
if (gameState.keysPressed['ArrowLeft'] || gameState.keysPressed['a']) {
player.speed = Math.max(player.speed - player.deceleration, 0);
}
// Apply friction
if (!gameState.keysPressed['ArrowRight'] && !gameState.keysPressed['ArrowLeft'] &&
!gameState.keysPressed['d'] && !gameState.keysPressed['a']) {
player.speed = Math.max(player.speed - player.deceleration, 0);
}
}
// Move player
if (!player.finished) {
player.x += player.speed;
// Check if player finished
if (player.x >= gameContainer.clientWidth - 100) {
player.finished = true;
player.finishTime = gameState.gameTime;
player.position = Object.values(gameState.players).filter(p => p.finished).length;
if (playerId === gameState.playerId) {
addChatMessage('System', `You finished in ${ordinalSuffix(player.position)} place!`, 'text-green-400');
} else {
addChatMessage('System', `${player.name} finished in ${ordinalSuffix(player.position)} place!`, 'text-blue-400');
}
}
}
// Update player position on screen
player.element.style.left = `${player.x}px`;
// Update stats for main player
if (playerId === gameState.playerId) {
playerSpeed.textContent = `Speed: ${Math.round(player.speed * 10)}`;
if (player.finished) {
playerPosition.textContent = `Position: ${ordinalSuffix(player.position)}`;
} else {
playerPosition.textContent = `Position: Racing`;
}
}
}
// Calculate positions
calculatePositions();
}
// Update UI
updateScoreboard();
requestAnimationFrame(gameLoop);
}
// Calculate player positions
function calculatePositions() {
const players = Object.values(gameState.players);
// Filter and sort players who haven't finished
const racingPlayers = players
.filter(p => !p.finished)
.sort((a, b) => b.x - a.x);
// Assign positions
let currentPosition = Object.values(gameState.players).filter(p => p.finished).length + 1;
for (const player of racingPlayers) {
player.position = currentPosition++;
}
}
// Update game time display
function updateGameTimeDisplay() {
const minutes = Math.floor(gameState.gameTime / 60);
const seconds = Math.floor(gameState.gameTime % 60);
const paddedSeconds = seconds.toString().padStart(2, '0');
gameTime.textContent = `${minutes}:${paddedSeconds}`;
}
// Update scoreboard
function updateScoreboard() {
const players = Object.values(gameState.players);
if (players.length === 0) {
emptyScoreboard.style.display = 'block';
return;
}
emptyScoreboard.style.display = 'none';
// Sort players by position (finished first, then by x position)
const sortedPlayers = players.sort((a, b) => {
if (a.finished && b.finished) return a.finishTime - b.finishTime;
if (a.finished) return -1;
if (b.finished) return 1;
return b.x - a.x;
});
// Clear scoreboard except header
const header = scoreboard.firstElementChild;
while (scoreboard.children.length > 1) {
scoreboard.removeChild(scoreboard.lastChild);
}
// Add player entries
sortedPlayers.forEach((player, index) => {
const playerRow = document.createElement('div');
playerRow.className = `flex justify-between py-2 px-2 rounded ${player.id === gameState.playerId ? 'bg-gray-700' : 'bg-gray-800'} mb-1 items-center`;
const playerName = document.createElement('div');
playerName.className = 'flex items-center';
playerName.innerHTML = `<span class="w-5 text-gray-400">${index + 1}.</span> ${player.name} ${player.id === gameState.playerId ? '<span class="text-xs bg-blue-600 px-1.5 py-0.5 rounded ml-2">You</span>' : ''}`;
const playerPosition = document.createElement('div');
if (player.finished) {
playerPosition.textContent = `${ordinalSuffix(player.position)} (${formatTime(player.finishTime)})`;
} else {
playerPosition.textContent = 'Racing';
}
const playerSpeed = document.createElement('div');
playerSpeed.textContent = `${Math.round(player.speed * 10)}`;
playerRow.appendChild(playerName);
playerRow.appendChild(playerPosition);
playerRow.appendChild(playerSpeed);
scoreboard.appendChild(playerRow);
});
// Update player count
playerCount.textContent = players.length;
}
// Chat functionality
function sendChatMessage() {
const message = chatInput.value.trim();
if (message) {
addChatMessage(gameState.playerName, message);
chatInput.value = '';
// Simulate other players responding
if (gameState.gameRunning) {
setTimeout(() => {
const demoPlayers = Object.values(gameState.players).filter(p => p.isDemo);
if (demoPlayers.length > 0 && Math.random() > 0.5) {
const demoPlayer = demoPlayers[Math.floor(Math.random() * demoPlayers.length)];
const responses = [
"Good luck everyone!",
"I'm in the lead!",
"Watch out for that turn!",
"Almost at the finish line!",
"Nice move!",
"This is intense!",
"I'm catching up!",
"Too fast for me!"
];
addChatMessage(demoPlayer.name, responses[Math.floor(Math.random() * responses.length)]);
}
}, 1000 + Math.random() * 3000);
}
}
}
function addChatMessage(name, message, specialClass = '') {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message mb-2 ${specialClass}`;
const nameSpan = document.createElement('span');
nameSpan.className = 'font-bold mr-2';
nameSpan.textContent = name + ':';
if (name === 'System') {
nameSpan.className += ' text-yellow-400';
}
const messageSpan = document.createElement('span');
messageSpan.className = 'text-gray-300';
messageSpan.textContent = message;
messageDiv.appendChild(nameSpan);
messageDiv.appendChild(messageSpan);
chatMessages.appendChild(messageDiv);
// Remove "empty chat" message if present
if (chatMessages.firstChild && chatMessages.firstChild.classList.contains('text-center')) {
chatMessages.removeChild(chatMessages.firstChild);
}
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Update player list display
function updatePlayerList() {
playerList.innerHTML = '';
const players = Object.values(gameState.players);
if (players.length === 0) {
playerList.innerHTML = '<div class="text-gray-400 text-center py-4">No other players connected</div>';
return;
}
players.forEach((player, index) => {
const playerItem = document.createElement('div');
playerItem.className = `flex items-center py-2 px-3 rounded mb-1 ${player.id === gameState.playerId ? 'bg-gray-700' : 'bg-gray-800'}`;
const carIcon = document.createElement('div');
carIcon.className = `car-icon mr-3 ${player.id === gameState.playerId ? 'text-red-500' : carColors[(index % carColors.length) + 1]}`;
carIcon.innerHTML = '<i class="fas fa-car"></i>';
const playerName = document.createElement('div');
playerName.className = 'flex-1';
playerName.textContent = player.name;
const playerStatus = document.createElement('div');
playerStatus.className = 'text-xs px-2 py-1 rounded-full';
if (player.finished) {
playerStatus.textContent = ordinalSuffix(player.position);
playerStatus.className += ' bg-green-800 text-green-300';
} else {
playerStatus.textContent = 'Racing';
playerStatus.className += ' bg-blue-800 text-blue-300';
}
playerItem.appendChild(carIcon);
playerItem.appendChild(playerName);
playerItem.appendChild(playerStatus);
playerList.appendChild(playerItem);
});
}
// Helper functions
function ordinalSuffix(i) {
const j = i % 10, k = i % 100;
if (j === 1 && k !== 11) return i + "st";
if (j === 2 && k !== 12) return i + "nd";
if (j === 3 && k !== 13) return i + "rd";
return i + "th";
}
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
const millis = Math.floor((seconds % 1) * 10);
return `${minutes}:${secs.toString().padStart(2, '0')}.${millis}`;
}
// Fire it up!
document.addEventListener('DOMContentLoaded', initGame);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LULDev/race" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>