/* About: Pingpong for HTML, you can insert it in anny Div tagged with an id * This is a simple elegant sample for how to work with a Canvas */ class PingPong_CTX2D { static get DEBUG_PARENT_OBJECT() { return true; }; static get AUTO_CONTINUE_ON_FOCUS() { return false; }; static get SHOW_FPS_INTERVAL() { return 1000 / 4; }; // four times per second static get STATE_COUNTDOWN() { return 0; }; static get STATE_PLAYING() { return 1; }; static get STATE_ENDED() { return 2; }; static get BALL_SIZE() { return 8; }; // static get SPEED_HUMAN() { return 180; }; // static get SPEED_CPU() { return 210; }; static get PADDLE_WIDTH() { return 10; }; static get PADDLE_HEIGHT() { return 60; }; static get PADDLE_MARGIN() { return 10; }; static get KEY_ARROW_UP() { return 'ArrowUp'; }; static get KEY_ARROW_DOWN() { return 'ArrowDown'; }; static get KEY_W() { return 'w'; }; static get KEY_S() { return 's'; }; static get KEY_ENTER() { return 'Enter'; }; static get KEY_SPACEBAR() { return ' '; }; static get KEY_ESCAPE() { return 'Escape'; }; animationRequestId = 0; running = false; constructor(divId, width, height, difficulty = 1, showFps = false) { this.createCanvas(divId, width, height); this.canvasEl.title = "Playing: PingPong"; this.ctx = this.canvasEl.getContext("2d"); this.ctx.textAlign = "center"; this.difficulty = difficulty; this.audioCtx = new AudioContext(); this.sounds = []; this.sounds[0] = new Audio('./snd/glass-knock.mp3'); this.audioCtx.createMediaElementSource(this.sounds[0]).connect(this.audioCtx.destination); this.sounds[1] = new Audio('./snd/short-success.mp3'); this.audioCtx.createMediaElementSource(this.sounds[1]).connect(this.audioCtx.destination); this.showFps = showFps; if (showFps) { // add framecounter and fps variables and html this.frameCounter = 0; this.initTime = performance.now(); const el = document.createElement('p'); this.frameLabel = document.createElement('span'); const floatRight = document.createElement('span'); floatRight.style = "float: right;"; this.fpsCounter = 0; this.fpsLabel = document.createElement('span'); el.appendChild(document.createTextNode('fps: ')); el.appendChild(this.fpsLabel); floatRight.appendChild(document.createTextNode('frame: ')); floatRight.appendChild(this.frameLabel); el.appendChild(floatRight); this.divEl.appendChild(el); } this.restartGame(); this.canvasEl.addEventListener("keydown", (e) => this.onKeyDown(e)); this.canvasEl.addEventListener("keyup", (e) => this.onKeyUp(e)); this.canvasEl.addEventListener("blur", (e) => this.onBlur(e)); this.canvasEl.addEventListener("focus", (e) => this.onFocus(e)); if (PingPong_CTX2D.DEBUG_PARENT_OBJECT) window.game = this; if (typeof WTerminal === "function") { WTerminal.terminalAddCommand("restartgame", (t) => this.terminalRestartGame(t)); WTerminal.terminalAddCommand("printgame", (t) => t.printVar(this, "pong")); WTerminal.printLn("new PingPong: @", divId, ' ', width, 'x', height, ' difficulty=', difficulty, ' showFps=', showFps); } this.drawCanvas(); } playSound(index) { try { const snd = this.sounds[index]; snd.currentTime = 0; snd.play(); } catch (e) { console.log(`Failed to play sound '${index}': ${e}}`) } } restartGame() { this.scoreCpu = 0; this.scoreHuman = 0; this.newRound(); } newRound() { this.countDown = 3; // seconds; this.gameState = PingPong_CTX2D.STATE_COUNTDOWN; // set paddle speeds if (this.difficulty == 0) { this.speedHuman = 100; this.speedCPU = 80; } else if (this.difficulty == 1) { this.speedHuman = 180; this.speedCPU = 210; } else { this.speedHuman = 150 + 30 * this.difficulty; this.speedCPU = 170 + 40 * this.difficulty; } // randomize ball speed let vx = 100; let vy = 0; if (this.difficulty == 0) { vx = 100 + Math.random() * 50; vy = -10 + Math.random() * 10; } else if (this.difficulty == 1) { vx = 200 + Math.random() * 100; vy = -20 + Math.random() * 20; if (Math.random() > 0.5) vx = - vx; } else { vx = 200 + 100 * this.difficulty + Math.random() * 80 * this.difficulty; vy = -30 + 10 * this.difficulty + Math.random() * 10 * this.difficulty; if (Math.random() > 0.5) vx = - vx; } if (Math.random() > 0.5) vy = - vy; // generate objects this.ball = new Ball(this.width / 2 - PingPong_CTX2D.BALL_SIZE / 2, this.height / 2 - PingPong_CTX2D.BALL_SIZE / 2, PingPong_CTX2D.BALL_SIZE, PingPong_CTX2D.BALL_SIZE, vx, vy); this.human = new Paddle(PingPong_CTX2D.PADDLE_MARGIN, this.height / 2 - PingPong_CTX2D.PADDLE_HEIGHT / 2, PingPong_CTX2D.PADDLE_WIDTH, PingPong_CTX2D.PADDLE_HEIGHT); this.cpu = new Paddle(this.width - PingPong_CTX2D.PADDLE_MARGIN - PingPong_CTX2D.PADDLE_WIDTH, this.height / 2 - PingPong_CTX2D.PADDLE_HEIGHT / 2, PingPong_CTX2D.PADDLE_WIDTH, PingPong_CTX2D.PADDLE_HEIGHT); this.prevNow = performance.now(); } createCanvas(divId, width = 0, height = 0, zoom = 1) { this.divEl = document.getElementById(divId); if (this.divEl === null) throw new Error("elementId not found: " + divId); while (this.divEl.firstChild) { this.divEl.removeChild(this.divEl.lastChild); } this.canvasEl = this.divEl.appendChild(document.createElement("canvas")); const c = this.canvasEl; this.width = width; this.height = height; if (width > 0 && height > 0) { c.style.width = width * zoom + 'px'; c.style.height = height * zoom + 'px'; c.width = width; c.height = height; } c.tabIndex = 0; // improtant for keyboard focus! // c.style.imageRendering = 'pixelated'; } drawCanvas() { if (this.showFps) { this.frameCounter++; this.fpsCounter++; const now = performance.now(); const diff = now - this.initTime; if (PingPong_CTX2D.SHOW_FPS_INTERVAL < diff || !this.running) { this.initTime = now; const seconds = diff / 1000; const fps = this.fpsCounter / seconds; this.fpsCounter = 0; if (this.frameLabel) this.frameLabel.innerHTML = this.frameCounter; if (this.fpsLabel) { this.fpsLabel.innerHTML = Math.round((fps + Number.EPSILON) * 100) / 100; if (!this.running) this.fpsLabel.innerHTML += " (not running)"; } } } const ctx = this.ctx; ctx.clearRect(0, 0, this.width, this.height); //# print score const FONT_SIZE = this.width > 440 ? 24 : 16; ctx.font = FONT_SIZE + "px serif"; ctx.fillStyle = 'red'; ctx.fillText(this.scoreHuman + " - " + this.scoreCpu, this.width / 2, FONT_SIZE * 2); //# print count down if (this.gameState == PingPong_CTX2D.STATE_COUNTDOWN) { ctx.font = 2 * FONT_SIZE + "px serif"; ctx.fillText(Math.ceil(this.countDown), this.width / 2, this.height / 2); } //# shadow ctx.save(); ctx.shadowColor = '#000000bf'; ctx.shadowBlur = 3; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; //# draw ball ctx.strokeStyle = 'black'; ctx.fillStyle = 'orange'; ctx.globalAlpha = 0.3; ctx.beginPath(); ctx.arc(this.ball.prevX + PingPong_CTX2D.BALL_SIZE / 2, this.ball.prevY + PingPong_CTX2D.BALL_SIZE / 2, PingPong_CTX2D.BALL_SIZE / 2, 0, Math.PI * 2, true); ctx.fill(); ctx.stroke(); ctx.globalAlpha = 1; ctx.beginPath(); ctx.arc(this.ball.x + PingPong_CTX2D.BALL_SIZE / 2, this.ball.y + PingPong_CTX2D.BALL_SIZE / 2, PingPong_CTX2D.BALL_SIZE / 2, 0, Math.PI * 2, true); ctx.fill(); ctx.stroke(); //# draw players paddle ctx.fillStyle = 'green'; ctx.fillRect(this.human.x, this.human.y, this.human.width, this.human.height); ctx.strokeRect(this.human.x, this.human.y, this.human.width, this.human.height); ctx.fillStyle = 'blue'; ctx.fillRect(this.cpu.x, this.cpu.y, this.cpu.width, this.cpu.height); ctx.strokeRect(this.cpu.x, this.cpu.y, this.cpu.width, this.cpu.height); ctx.restore(); //end shadow // unfocused & paused banner if (!this.running) { const x = this.width / 2; const y = this.height / 2; const y2 = y - FONT_SIZE / 2; ctx.strokeStyle = 'black'; const g = ctx.createLinearGradient(0, 0, this.width, 0); g.addColorStop(0, '#404040a0'); g.addColorStop(0.2, '#404040df'); g.addColorStop(0.8, '#404040df'); g.addColorStop(1, '#404040a0'); ctx.fillStyle = g; ctx.fillRect(0, y2 - 2, this.width + 1, FONT_SIZE + 5 * 2); ctx.strokeRect(0, y2 - 2, this.width + 1, FONT_SIZE + 5 * 2); ctx.fillStyle = (this.gameState === PingPong_CTX2D.STATE_ENDED) ? 'red' : 'gray'; ctx.font = 4 * FONT_SIZE + "px serif"; let text = (this.gameState === PingPong_CTX2D.STATE_ENDED) ? "Score!" : "Paused"; ctx.fillText(text, x, y - 2 * FONT_SIZE); ctx.strokeText(text, x, y - 2 * FONT_SIZE); ctx.fillStyle = this.isFocused ? 'goldenrod' : 'lightgray'; ctx.font = FONT_SIZE + "px serif"; text = this.isFocused ? `Press space or enter to ${(this.gameState === PingPong_CTX2D.STATE_ENDED) ? "start next round" : "continue"}.` : "Click here to continue. (unfocused)"; ctx.strokeText(text, x, y2 + FONT_SIZE); ctx.fillText(text, x, y2 + FONT_SIZE); } } updateCanvas() { const now = performance.now(); const timeDelta = (now - this.prevNow) / 1000; //timeDelta = (milli - milli) / toSeconds this.prevNow = now; //#state switch if (this.gameState == PingPong_CTX2D.STATE_COUNTDOWN) { this.human.move(timeDelta); this.human.borderTopAndBottom(this.height); this.cpu.move(timeDelta); this.cpu.borderTopAndBottom(this.height); this.countDown -= timeDelta;//PONG_UPDATE_INTERVAL; if (this.countDown < 0) { this.gameState = PingPong_CTX2D.STATE_PLAYING; } } else if (this.gameState == PingPong_CTX2D.STATE_PLAYING) { //cpu actions if (this.ball.y + this.ball.height / 2 < this.cpu.y + this.cpu.height / 2) { this.cpu.vy = -this.speedCPU; } else if (this.ball.y > this.cpu.y + this.cpu.height / 2) { this.cpu.vy = this.speedCPU; } else { this.cpu.vy = 0; } //move ball and paddles this.ball.move(timeDelta); if (this.ball.borderTopAndBottom(this.height)) { this.playSound(0); } //Horizontal if (this.ball.x < 0) { this.win(0); this.playSound(1); } else if (this.ball.x > this.width - this.ball.width) { this.win(1); this.playSound(1); } this.human.move(timeDelta); this.human.borderTopAndBottom(this.height); this.cpu.move(timeDelta); this.cpu.borderTopAndBottom(this.height); // collide ball vs paddles if (this.human.x + this.human.width < this.ball.prevX && this.human.x + this.human.width > this.ball.x) { // console.log("pass 1.1"); if (this.human.y < this.ball.y + this.ball.height && this.human.y + this.human.height > this.ball.y) { // console.log("pass 1.2"); this.ball.vx = this.ball.vx * -1.05; this.ball.x = this.human.x + this.human.width; this.ball.vy += ((this.ball.height / 2 + this.ball.y) - (this.human.height / 2 + this.human.y)) * 10; this.playSound(0); } } if (this.cpu.x < this.ball.x + this.ball.width && this.cpu.x > this.ball.prevX + this.ball.width) { // console.log("pass 2.1"); if (this.cpu.y < this.ball.y + this.ball.height && this.cpu.y + this.cpu.height > this.ball.y) { // console.log("pass 2.2"); this.ball.vx = this.ball.vx * -1.05; this.ball.x = this.cpu.x - this.ball.width; let temp = ((this.ball.height / 2 + this.ball.y) - (this.cpu.height / 2 + this.cpu.y)) * 10; this.ball.vy += temp; this.playSound(0); } } } this.drawCanvas(); if (this.running) { // loop this.animationRequestId = requestAnimationFrame(() => this.updateCanvas()); } else { // not looping this.animationRequestId = 0; } } startRunning(fn) { if (this.animationRequestId != 0) cancelAnimationFrame(this.animationRequestId); this.running = true; this.prevNow = performance.now(); if (this.showFps) { this.initTime = performance.now(); } this.animationRequestId = requestAnimationFrame(fn); } close() { if (this.animationRequestId != 0) { cancelAnimationFrame(this.animationRequestId); this.animationRequestId = 0; } this.running = false; } pausePlayGame() { if (this.gameState == PingPong_CTX2D.STATE_ENDED) return; this.running = !this.running; if (this.running) { this.startRunning(() => this.updateCanvas()); } } terminalPrintGame(term) { term.printVar(this, "pong"); } terminalRestartGame(term) { term.terminalClose(); this.restartGame(); // this.canvasEl.focus(); setTimeout(() => { this.canvasEl.focus(); this.pausePlayGame(); }, 200); } win(winner) { this.running = false; //this.stopRunning(); this.gameState = PingPong_CTX2D.STATE_ENDED; if (winner == 0) { this.scoreCpu++; if (typeof WTerminal === "function") WTerminal.printLn("CPU scored?!"); } else { this.scoreHuman++; if (typeof WTerminal === "function") WTerminal.printLn("Human scored!"); } if (typeof WTerminal === "function") WTerminal.printLn("Scores: Human " + this.scoreHuman + " - " + this.scoreCpu + " CPU"); } onKeyDown(e) { if (e.key == PingPong_CTX2D.KEY_ESCAPE) { if (this.running) this.pausePlayGame() e.preventDefault(); return false; } if (e.key == PingPong_CTX2D.KEY_ARROW_UP || e.key == PingPong_CTX2D.KEY_W) { this.human.vy = -this.speedHuman; e.preventDefault(); return false; } else if (e.key == PingPong_CTX2D.KEY_ARROW_DOWN || e.key == PingPong_CTX2D.KEY_S) { this.human.vy = this.speedHuman; e.preventDefault(); return false; } else if (e.key == PingPong_CTX2D.KEY_ENTER || e.key == PingPong_CTX2D.KEY_SPACEBAR) { //# next round/pause/play if (this.gameState == PingPong_CTX2D.STATE_ENDED) { this.newRound(); this.startRunning(() => this.updateCanvas()); } else { this.pausePlayGame(); } e.preventDefault(); return false; } return true; } onKeyUp(e) { if (e.key == PingPong_CTX2D.KEY_ARROW_UP || e.key == PingPong_CTX2D.KEY_W) { this.human.vy = 0; e.preventDefault(); return false; } else if (e.key == PingPong_CTX2D.KEY_ARROW_DOWN || e.key == PingPong_CTX2D.KEY_S) { this.human.vy = 0; e.preventDefault(); return false; } return true; } onBlur() { this.isFocused = false; this.canvasEl.style.borderColor = null; if (this.running) { this.pausePlayGame(); } else { this.drawCanvas(); } } onFocus() { this.isFocused = true; this.canvasEl.style.borderColor = "red"; if (!this.running && PingPong_CTX2D.AUTO_CONTINUE_ON_FOCUS) { this.pausePlayGame(); } else { this.drawCanvas(); } } } class Rectangle { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; } } class Ball extends Rectangle { constructor(x, y, width, height, vx, vy) { super(x, y, width, height); this.vx = vx; this.vy = vy; this.prevX = this.x; this.prevY = this.y; } move(timeDelta) { this.prevX = this.x; this.prevY = this.y; this.x += this.vx * timeDelta; this.y += this.vy * timeDelta; } borderTopAndBottom(height) { if (this.y < 0) { this.y = 0; this.vy = -this.vy; return true; } else if (this.y > height - this.height) { this.y = height - this.height; this.vy = - this.vy; return true; } return false; } } class Paddle extends Rectangle { constructor(x, y, width = 20, height = 20) { super(x, y, width, height); this.vy = 0; } move(timeDelta) { this.y += this.vy * timeDelta; } borderTopAndBottom(height) { if (this.y < 0) { this.y = 0; this.vy = 0; } else if (this.y > height - this.height) { this.y = height - this.height; this.vy = 0; } } } function startPingPong(divId, width = 480, height = 320, difficulty = 1, showFps = true) { return new PingPong_CTX2D(divId, width, height, difficulty, showFps); }