558 lines
17 KiB
HTML
558 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Connect Four</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a237e; touch-action: none; font-family: Arial, sans-serif; }
|
|
canvas { display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="game"></canvas>
|
|
<script>
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
let audioCtx = null;
|
|
let musicEnabled = true;
|
|
let sfxEnabled = true;
|
|
let musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (!audioCtx) audioCtx = new AudioContext();
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
}
|
|
|
|
// Fun board game melody
|
|
const melody = [
|
|
{n: 523, d: 0.25}, {n: 587, d: 0.25}, {n: 659, d: 0.25}, {n: 698, d: 0.5},
|
|
{n: 659, d: 0.25}, {n: 587, d: 0.25}, {n: 523, d: 0.5},
|
|
{n: 440, d: 0.25}, {n: 494, d: 0.25}, {n: 523, d: 0.25}, {n: 587, d: 0.5},
|
|
{n: 523, d: 0.25}, {n: 494, d: 0.25}, {n: 440, d: 0.5}, {n: 0, d: 0.25}
|
|
];
|
|
|
|
const bass = [
|
|
{n: 131, d: 0.5}, {n: 165, d: 0.5}, {n: 131, d: 0.5}, {n: 110, d: 0.5},
|
|
{n: 110, d: 0.5}, {n: 131, d: 0.5}, {n: 110, d: 0.5}, {n: 98, d: 0.5}
|
|
];
|
|
|
|
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
|
|
|
|
function playTone(freq, dur, type = 'square', vol = 0.08) {
|
|
if (!audioCtx || !musicEnabled || freq === 0) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = type;
|
|
osc.frequency.value = freq;
|
|
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + dur);
|
|
}
|
|
|
|
function playSfx(type) {
|
|
if (!audioCtx || !sfxEnabled) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
if (type === 'drop') {
|
|
osc.frequency.setValueAtTime(600, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(200, audioCtx.currentTime + 0.15);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
|
} else if (type === 'win') {
|
|
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
|
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.15);
|
|
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.3);
|
|
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.45);
|
|
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
|
|
} else if (type === 'lose') {
|
|
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
|
osc.frequency.setValueAtTime(300, audioCtx.currentTime + 0.15);
|
|
osc.frequency.setValueAtTime(200, audioCtx.currentTime + 0.3);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.45);
|
|
} else if (type === 'draw') {
|
|
osc.frequency.value = 350;
|
|
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
|
|
}
|
|
osc.type = 'square';
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.7);
|
|
}
|
|
|
|
function updateMusic() {
|
|
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
|
const now = audioCtx.currentTime;
|
|
if (now >= nextMelody) {
|
|
const note = melody[melodyIdx];
|
|
playTone(note.n, note.d, 'square', 0.06);
|
|
nextMelody = now + note.d;
|
|
melodyIdx = (melodyIdx + 1) % melody.length;
|
|
}
|
|
if (now >= nextBass) {
|
|
const note = bass[bassIdx];
|
|
playTone(note.n, note.d, 'triangle', 0.1);
|
|
nextBass = now + note.d;
|
|
bassIdx = (bassIdx + 1) % bass.length;
|
|
}
|
|
}
|
|
|
|
function startMusic() {
|
|
if (!musicPlaying && musicEnabled) {
|
|
initAudio();
|
|
musicPlaying = true;
|
|
nextMelody = audioCtx.currentTime;
|
|
nextBass = audioCtx.currentTime;
|
|
}
|
|
}
|
|
|
|
const canvas = document.getElementById('game');
|
|
const ctx = canvas.getContext('2d');
|
|
let W, H, safeTop, safeBottom;
|
|
|
|
function resize() {
|
|
W = window.innerWidth;
|
|
H = window.innerHeight;
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
safeTop = H * 0.1;
|
|
safeBottom = H * 0.9;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
const COLS = 7;
|
|
const ROWS = 6;
|
|
let board = [];
|
|
let currentPlayer = 1; // 1 = player (red), 2 = CPU (yellow)
|
|
let gameOver = false;
|
|
let winner = 0;
|
|
let gameRunning = false;
|
|
let hoverCol = -1;
|
|
let cpuThinking = false;
|
|
let winningCells = [];
|
|
let aiDifficulty = 0.5; // Start easy, increase over time
|
|
|
|
function createBoard() {
|
|
board = [];
|
|
for (let r = 0; r < ROWS; r++) {
|
|
board.push(new Array(COLS).fill(0));
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
createBoard();
|
|
currentPlayer = 1;
|
|
gameOver = false;
|
|
winner = 0;
|
|
winningCells = [];
|
|
cpuThinking = false;
|
|
aiDifficulty = 0.3; // Easy start
|
|
gameRunning = true;
|
|
startMusic();
|
|
}
|
|
|
|
function getLowestRow(col) {
|
|
for (let r = ROWS - 1; r >= 0; r--) {
|
|
if (board[r][col] === 0) return r;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function dropPiece(col, player) {
|
|
const row = getLowestRow(col);
|
|
if (row === -1) return false;
|
|
board[row][col] = player;
|
|
playSfx('drop');
|
|
return true;
|
|
}
|
|
|
|
function checkWin(player) {
|
|
// Horizontal
|
|
for (let r = 0; r < ROWS; r++) {
|
|
for (let c = 0; c <= COLS - 4; c++) {
|
|
if (board[r][c] === player && board[r][c+1] === player &&
|
|
board[r][c+2] === player && board[r][c+3] === player) {
|
|
winningCells = [[r,c], [r,c+1], [r,c+2], [r,c+3]];
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// Vertical
|
|
for (let r = 0; r <= ROWS - 4; r++) {
|
|
for (let c = 0; c < COLS; c++) {
|
|
if (board[r][c] === player && board[r+1][c] === player &&
|
|
board[r+2][c] === player && board[r+3][c] === player) {
|
|
winningCells = [[r,c], [r+1,c], [r+2,c], [r+3,c]];
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// Diagonal down-right
|
|
for (let r = 0; r <= ROWS - 4; r++) {
|
|
for (let c = 0; c <= COLS - 4; c++) {
|
|
if (board[r][c] === player && board[r+1][c+1] === player &&
|
|
board[r+2][c+2] === player && board[r+3][c+3] === player) {
|
|
winningCells = [[r,c], [r+1,c+1], [r+2,c+2], [r+3,c+3]];
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// Diagonal up-right
|
|
for (let r = 3; r < ROWS; r++) {
|
|
for (let c = 0; c <= COLS - 4; c++) {
|
|
if (board[r][c] === player && board[r-1][c+1] === player &&
|
|
board[r-2][c+2] === player && board[r-3][c+3] === player) {
|
|
winningCells = [[r,c], [r-1,c+1], [r-2,c+2], [r-3,c+3]];
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isBoardFull() {
|
|
return board[0].every(cell => cell !== 0);
|
|
}
|
|
|
|
function evaluateWindow(window, player) {
|
|
const opp = player === 1 ? 2 : 1;
|
|
const playerCount = window.filter(c => c === player).length;
|
|
const oppCount = window.filter(c => c === opp).length;
|
|
const emptyCount = window.filter(c => c === 0).length;
|
|
|
|
if (playerCount === 4) return 100;
|
|
if (playerCount === 3 && emptyCount === 1) return 5;
|
|
if (playerCount === 2 && emptyCount === 2) return 2;
|
|
if (oppCount === 3 && emptyCount === 1) return -4;
|
|
return 0;
|
|
}
|
|
|
|
function scorePosition(player) {
|
|
let score = 0;
|
|
|
|
// Center column preference
|
|
const centerCol = board.map(row => row[3]).filter(c => c === player).length;
|
|
score += centerCol * 3;
|
|
|
|
// Horizontal
|
|
for (let r = 0; r < ROWS; r++) {
|
|
for (let c = 0; c <= COLS - 4; c++) {
|
|
const window = [board[r][c], board[r][c+1], board[r][c+2], board[r][c+3]];
|
|
score += evaluateWindow(window, player);
|
|
}
|
|
}
|
|
// Vertical
|
|
for (let c = 0; c < COLS; c++) {
|
|
for (let r = 0; r <= ROWS - 4; r++) {
|
|
const window = [board[r][c], board[r+1][c], board[r+2][c], board[r+3][c]];
|
|
score += evaluateWindow(window, player);
|
|
}
|
|
}
|
|
// Diagonals
|
|
for (let r = 0; r <= ROWS - 4; r++) {
|
|
for (let c = 0; c <= COLS - 4; c++) {
|
|
const window = [board[r][c], board[r+1][c+1], board[r+2][c+2], board[r+3][c+3]];
|
|
score += evaluateWindow(window, player);
|
|
}
|
|
}
|
|
for (let r = 3; r < ROWS; r++) {
|
|
for (let c = 0; c <= COLS - 4; c++) {
|
|
const window = [board[r][c], board[r-1][c+1], board[r-2][c+2], board[r-3][c+3]];
|
|
score += evaluateWindow(window, player);
|
|
}
|
|
}
|
|
return score;
|
|
}
|
|
|
|
function cpuMove() {
|
|
if (gameOver || currentPlayer !== 2) return;
|
|
|
|
cpuThinking = true;
|
|
|
|
setTimeout(() => {
|
|
// Sometimes make random move (easier for kids)
|
|
if (Math.random() > aiDifficulty) {
|
|
const validCols = [];
|
|
for (let c = 0; c < COLS; c++) {
|
|
if (getLowestRow(c) !== -1) validCols.push(c);
|
|
}
|
|
const col = validCols[Math.floor(Math.random() * validCols.length)];
|
|
dropPiece(col, 2);
|
|
} else {
|
|
// Smart move
|
|
let bestScore = -Infinity;
|
|
let bestCol = 3;
|
|
|
|
for (let c = 0; c < COLS; c++) {
|
|
const row = getLowestRow(c);
|
|
if (row === -1) continue;
|
|
|
|
board[row][c] = 2;
|
|
|
|
// Check for winning move
|
|
if (checkWin(2)) {
|
|
board[row][c] = 0;
|
|
winningCells = [];
|
|
bestCol = c;
|
|
break;
|
|
}
|
|
|
|
// Check if blocking player win
|
|
board[row][c] = 1;
|
|
if (checkWin(1)) {
|
|
board[row][c] = 0;
|
|
winningCells = [];
|
|
bestCol = c;
|
|
break;
|
|
}
|
|
|
|
board[row][c] = 2;
|
|
const score = scorePosition(2);
|
|
board[row][c] = 0;
|
|
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestCol = c;
|
|
}
|
|
}
|
|
|
|
winningCells = [];
|
|
dropPiece(bestCol, 2);
|
|
}
|
|
|
|
cpuThinking = false;
|
|
|
|
if (checkWin(2)) {
|
|
gameOver = true;
|
|
winner = 2;
|
|
gameRunning = false;
|
|
playSfx('lose');
|
|
} else if (isBoardFull()) {
|
|
gameOver = true;
|
|
winner = 0;
|
|
gameRunning = false;
|
|
playSfx('draw');
|
|
} else {
|
|
currentPlayer = 1;
|
|
}
|
|
|
|
// Gradually increase difficulty
|
|
aiDifficulty = Math.min(0.8, aiDifficulty + 0.02);
|
|
}, 800);
|
|
}
|
|
|
|
function playerMove(col) {
|
|
if (gameOver || currentPlayer !== 1 || cpuThinking) return;
|
|
if (getLowestRow(col) === -1) return;
|
|
|
|
dropPiece(col, 1);
|
|
|
|
if (checkWin(1)) {
|
|
gameOver = true;
|
|
winner = 1;
|
|
gameRunning = false;
|
|
playSfx('win');
|
|
} else if (isBoardFull()) {
|
|
gameOver = true;
|
|
winner = 0;
|
|
gameRunning = false;
|
|
playSfx('draw');
|
|
} else {
|
|
currentPlayer = 2;
|
|
cpuMove();
|
|
}
|
|
}
|
|
|
|
canvas.addEventListener('mousemove', e => {
|
|
const availH = safeBottom - safeTop - 60;
|
|
const availW = W - 40;
|
|
const cellSize = Math.min(availW / COLS, availH / ROWS);
|
|
const boardW = cellSize * COLS;
|
|
const boardX = (W - boardW) / 2;
|
|
|
|
hoverCol = Math.floor((e.clientX - boardX) / cellSize);
|
|
if (hoverCol < 0 || hoverCol >= COLS) hoverCol = -1;
|
|
});
|
|
|
|
canvas.addEventListener('click', e => {
|
|
initAudio();
|
|
if (gameOver) { init(); return; }
|
|
|
|
const availH = safeBottom - safeTop - 60;
|
|
const availW = W - 40;
|
|
const cellSize = Math.min(availW / COLS, availH / ROWS);
|
|
const boardW = cellSize * COLS;
|
|
const boardX = (W - boardW) / 2;
|
|
|
|
const col = Math.floor((e.clientX - boardX) / cellSize);
|
|
if (col >= 0 && col < COLS) playerMove(col);
|
|
});
|
|
|
|
canvas.addEventListener('touchstart', e => {
|
|
e.preventDefault();
|
|
initAudio();
|
|
if (gameOver) { init(); return; }
|
|
|
|
const availH = safeBottom - safeTop - 60;
|
|
const availW = W - 40;
|
|
const cellSize = Math.min(availW / COLS, availH / ROWS);
|
|
const boardW = cellSize * COLS;
|
|
const boardX = (W - boardW) / 2;
|
|
|
|
const col = Math.floor((e.touches[0].clientX - boardX) / cellSize);
|
|
if (col >= 0 && col < COLS) playerMove(col);
|
|
});
|
|
|
|
document.addEventListener('keydown', e => {
|
|
initAudio();
|
|
if ((e.key === ' ' || e.key === 'Enter') && gameOver) init();
|
|
if (e.key >= '1' && e.key <= '7' && !gameOver && currentPlayer === 1) {
|
|
playerMove(parseInt(e.key) - 1);
|
|
}
|
|
});
|
|
|
|
function update() {
|
|
updateMusic();
|
|
}
|
|
|
|
function draw() {
|
|
// Background
|
|
ctx.fillStyle = '#1a237e';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Safe zones
|
|
ctx.fillStyle = '#0d1657';
|
|
ctx.fillRect(0, 0, W, safeTop);
|
|
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
|
|
|
// Header
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 24px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('CONNECT FOUR', W / 2, 40);
|
|
|
|
const turnText = gameOver ? '' : (currentPlayer === 1 ? 'Your Turn (Red)' : 'CPU Thinking...');
|
|
ctx.font = '18px Arial';
|
|
ctx.fillStyle = currentPlayer === 1 ? '#ef5350' : '#ffca28';
|
|
ctx.fillText(turnText, W / 2, 65);
|
|
|
|
// Calculate board
|
|
const availH = safeBottom - safeTop - 80;
|
|
const availW = W - 40;
|
|
const cellSize = Math.min(availW / COLS, availH / ROWS);
|
|
const boardW = cellSize * COLS;
|
|
const boardH = cellSize * ROWS;
|
|
const boardX = (W - boardW) / 2;
|
|
const boardY = safeTop + 80 + (availH - boardH) / 2;
|
|
|
|
// Board background
|
|
ctx.fillStyle = '#1565c0';
|
|
ctx.beginPath();
|
|
ctx.roundRect(boardX - 10, boardY - 10, boardW + 20, boardH + 20, 10);
|
|
ctx.fill();
|
|
|
|
// Hover indicator
|
|
if (hoverCol >= 0 && hoverCol < COLS && currentPlayer === 1 && !gameOver && !cpuThinking) {
|
|
ctx.fillStyle = 'rgba(239, 83, 80, 0.5)';
|
|
ctx.beginPath();
|
|
ctx.arc(boardX + hoverCol * cellSize + cellSize / 2, boardY - 25, cellSize / 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Cells
|
|
for (let r = 0; r < ROWS; r++) {
|
|
for (let c = 0; c < COLS; c++) {
|
|
const x = boardX + c * cellSize + cellSize / 2;
|
|
const y = boardY + r * cellSize + cellSize / 2;
|
|
const radius = cellSize * 0.4;
|
|
|
|
// Hole
|
|
ctx.fillStyle = '#0d47a1';
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Piece
|
|
if (board[r][c] !== 0) {
|
|
const isWinning = winningCells.some(cell => cell[0] === r && cell[1] === c);
|
|
|
|
if (board[r][c] === 1) {
|
|
ctx.fillStyle = isWinning ? '#ff8a80' : '#ef5350';
|
|
} else {
|
|
ctx.fillStyle = isWinning ? '#fff59d' : '#ffca28';
|
|
}
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius - 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Highlight
|
|
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
|
ctx.beginPath();
|
|
ctx.arc(x - radius * 0.2, y - radius * 0.2, radius * 0.3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Controls hint
|
|
ctx.fillStyle = '#90caf9';
|
|
ctx.font = '14px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('Tap column or press 1-7 to drop', W / 2, safeBottom + 25);
|
|
|
|
// Game over
|
|
if (gameOver) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.8)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
ctx.font = 'bold 36px Arial';
|
|
ctx.textAlign = 'center';
|
|
|
|
if (winner === 1) {
|
|
ctx.fillStyle = '#4caf50';
|
|
ctx.fillText('YOU WIN!', W / 2, H / 2 - 30);
|
|
} else if (winner === 2) {
|
|
ctx.fillStyle = '#f44336';
|
|
ctx.fillText('CPU WINS!', W / 2, H / 2 - 30);
|
|
} else {
|
|
ctx.fillStyle = '#ffca28';
|
|
ctx.fillText("IT'S A DRAW!", W / 2, H / 2 - 30);
|
|
}
|
|
|
|
ctx.fillStyle = '#1565c0';
|
|
ctx.beginPath();
|
|
ctx.roundRect(W / 2 - 100, H / 2 + 20, 200, 50, 10);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 18px Arial';
|
|
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 52);
|
|
}
|
|
}
|
|
|
|
function gameLoop() {
|
|
update();
|
|
draw();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
window.gameScore = 0;
|
|
Object.defineProperty(window, 'gameScore', { get: () => winner === 1 ? 100 : 0 });
|
|
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
|
|
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
|
|
init();
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html>
|