Compare commits

...

4 Commits

Author SHA1 Message Date
53c8aa5012
[pongwars] optimisation 2024-03-01 16:12:10 -05:00
a7be3e1cd2
[pongwars] add threshold display 2024-03-01 15:47:27 -05:00
e02d1b855b
[pongwars]: add new modes 2024-03-01 14:58:08 -05:00
fa71caa020
[pongwars] ease nTeams 2024-02-29 21:50:50 -05:00

View File

@ -47,15 +47,25 @@
#made { #made {
font-family: monospace; font-family: monospace;
margin-top: auto; margin-top: auto;
margin-bottom: 20px;
font-size: 12px; font-size: 12px;
padding-left: 20px; padding-left: 20px;
} }
#mode {
font-family: monospace;
font-size: 12px;
list-style-type: none;
margin-bottom: 20px;
}
#instr { #instr {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
padding-left: 20px; }
#threshold {
font-family: monospace;
font-size: 8px;
} }
#made a { #made a {
@ -69,7 +79,7 @@
<canvas id="pongCanvas" width="1200" height="800"></canvas> <canvas id="pongCanvas" width="1200" height="800"></canvas>
<div id="score"></div> <div id="score"></div>
<p id="instr"> <p id="instr">
balls can be controlled with number keys balls can be controlled with keys shown to the left of their names
</p> </p>
<p id="made"> <p id="made">
made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on
@ -78,6 +88,13 @@
patches from dogeystamp | patched source on patches from dogeystamp | patched source on
<a href="https://github.com/dogeystamp/garbage-monorepo/tree/main/pongwars">github</a> <a href="https://github.com/dogeystamp/garbage-monorepo/tree/main/pongwars">github</a>
</p> </p>
<p id="threshold"></p>
<div id="mode">
change mode:
<a class="modeLink" data-mode="normal">normal</a>
<a class="modeLink" data-mode="big">big</a>
<a class="modeLink" data-mode="small">small</a>
</div>
</div> </div>
</body> </body>
@ -91,6 +108,7 @@
const canvas = document.getElementById("pongCanvas"); const canvas = document.getElementById("pongCanvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const scoreElement = document.getElementById("score"); const scoreElement = document.getElementById("score");
const thresholdElement = document.getElementById("threshold");
var suddenDeathStart = null; var suddenDeathStart = null;
var suddenDeathCoeff = 0; var suddenDeathCoeff = 0;
@ -114,7 +132,7 @@
{ {
name: "orange", name: "orange",
color: "coral", color: "coral",
backgroundColor: "chocolate", backgroundColor: "#DD4500",
}, },
{ {
name: "white", name: "white",
@ -136,33 +154,91 @@
color: "gray", color: "gray",
backgroundColor: "#333333", backgroundColor: "#333333",
}, },
{
name: "yellow",
color: "yellow",
backgroundColor: "goldenrod",
},
{
name: "pink",
color: "darksalmon",
backgroundColor: "mediumvioletred",
},
{
name: "turquoise",
color: "turquoise",
backgroundColor: "royalblue",
},
{
name: "salmon",
color: "peachpuff",
backgroundColor: "salmon",
},
]; ];
var state = {} var state = {}
const squareSize = 16;
const numSquaresX = canvas.width / squareSize; var squareSize = 16;
const numSquaresY = canvas.height / squareSize; var numSquaresX = canvas.width / squareSize;
var numSquaresY = canvas.height / squareSize;
let squares = []; let squares = [];
// matrix of "timestamps" where each square was claimed // matrix of "timestamps" where each square was claimed
let squareTaken = []; let squareTaken = [];
var squareTime = 0; var squareTime = 0;
// threshold for territory under which a color starts dying
let threshold = 0;
var nTeams = teams.length;
// NTeams but with ease in to avoid instant deaths
var smoothNTeams = nTeams;
// do not edit without editing the code below for key to idx
const idxKeyMap = [..."0123456789abcdefghijklmnopqrstuvwxyz"];
function keyToIdx(key) {
const keyInt = parseInt(key, 36);
if (!isNaN(keyInt) && keyInt < teams.length) return keyInt;
return null
}
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
const key = parseInt(event.key) const idx = keyToIdx(event.key)
if (isNaN(key)) return if (idx === null) return
state[key].boostEnabled = true; state[idx].boostEnabled = true;
}) })
document.addEventListener("keyup", (event) => { document.addEventListener("keyup", (event) => {
const key = parseInt(event.key) const idx = keyToIdx(event.key)
if (isNaN(key)) return if (idx === null) return
state[key].boostEnabled = false; state[idx].boostEnabled = false;
}) })
function initialState() { const params = new URLSearchParams(window.location.search);
state = teams.map((team) => ({
elim: false, const modeLinks = document.getElementsByClassName("modeLink");
for (let i = 0; i < modeLinks.length; i++) {
let loc = new URL(window.location);
loc.searchParams.set("size", modeLinks[i].getAttribute("data-mode"));
modeLinks[i].href = loc.href;
}
function initialState({gridW = 4, gridH = 2, cols = teams.length, canvasW=1200, canvasH=800, squareSize=16} = {}) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
nTeams = cols;
smoothNTeams = cols;
canvas.width = canvasW;
canvas.height = canvasH;
window.squareSize = squareSize;
numSquaresX = canvas.width / squareSize;
numSquaresY = canvas.height / squareSize;
state = {};
state = teams.map((team, idx) => ({
elim: idx >= cols,
boostEnabled: false, boostEnabled: false,
x: 0, x: 0,
y: 0, y: 0,
@ -171,7 +247,7 @@
score: 0, score: 0,
// how many consecutive contested tiles it has hit // how many consecutive contested tiles it has hit
conflict: 0, conflict: 0,
})) }));
// base noise pattern (failsafe in case the grid doesn't cover some part) // base noise pattern (failsafe in case the grid doesn't cover some part)
for (let i = 0; i < numSquaresX; i++) { for (let i = 0; i < numSquaresX; i++) {
@ -186,15 +262,13 @@
const enableGrid = true; const enableGrid = true;
if (enableGrid) { if (enableGrid) {
const gridW = 4;
const gridH = 2;
for (let i = 0; i < gridW; i++) { for (let i = 0; i < gridW; i++) {
for (let j = 0; j < gridH; j++) { for (let j = 0; j < gridH; j++) {
const gridIdx = j * gridW + i; const gridIdx = j * gridW + i;
const t = (gridIdx < teams.length) ? gridIdx : null; const t = (gridIdx < teams.length) ? gridIdx : null;
for (let x = Math.floor(i/gridW*numSquaresX); x < Math.floor((i+1)/gridW*numSquaresX); x++) { for (let x = Math.floor(i/gridW*numSquaresX); x < Math.ceil((i+1)/gridW*numSquaresX); x++) {
for (let y = Math.floor(j/gridH*numSquaresY); y < Math.floor((j+1)/gridH*numSquaresY); y++) { for (let y = Math.floor(j/gridH*numSquaresY); y < Math.ceil((j+1)/gridH*numSquaresY); y++) {
if (t !== null) { if (t !== null) {
squares[x][y] = t; squares[x][y] = t;
} }
@ -214,8 +288,33 @@
state[i].dy = 8 * Math.sin(angle); state[i].dy = 8 * Math.sin(angle);
} }
} }
state.forEach((t) => (t.score = 0));
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
state[squares[i][j]].score++;
}
}
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
drawSquare(i, j);
}
}
}
switch (params.get("size")) {
case "big":
initialState({gridW: 4, gridH: 3, cols: 12, canvasW: 1600, canvasH: 1000});
break;
case "small":
initialState({gridW: 2, gridH: 1, cols: 2, canvasW: 600, canvasH: 600, squareSize: 25});
break;
case "normal":
default:
initialState({gridW: 4, gridH: 2, cols: 8});
break;
} }
initialState();
function randomNum(min, max) { function randomNum(min, max) {
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
@ -231,6 +330,19 @@
updateScoreElement(); updateScoreElement();
function coverBall(x, y) {
// draw over the last ball
let i = Math.floor(x / squareSize);
let j = Math.floor(y / squareSize);
for (let checkI = i-1; checkI <= i+1; checkI++) {
for (let checkJ = j-1; checkJ <= j+1; checkJ++) {
if (checkI >= 0 && checkI < numSquaresX && checkJ >= 0 && checkJ < numSquaresY) {
drawSquare(checkI, checkJ);
}
}
}
}
function drawBall(x, y, color) { function drawBall(x, y, color) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, squareSize / 2, 0, Math.PI * 2, false); ctx.arc(x, y, squareSize / 2, 0, Math.PI * 2, false);
@ -239,13 +351,17 @@
ctx.closePath(); ctx.closePath();
} }
function drawSquares() { function drawSquare(i, j) {
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
ctx.fillStyle = teams[squares[i][j]].backgroundColor; ctx.fillStyle = teams[squares[i][j]].backgroundColor;
ctx.fillRect(i * squareSize, j * squareSize, squareSize, squareSize); ctx.fillRect(i * squareSize, j * squareSize, squareSize, squareSize);
} }
}
function takeSquare(i, j, team) {
state[team].score++;
state[squares[i][j]].score--;
squares[i][j] = team;
squareTaken[i][j] = squareTime;
drawSquare(i, j);
} }
function clamp(min, max, num) { function clamp(min, max, num) {
@ -256,14 +372,10 @@
return v*x + (1-v)*y; return v*x + (1-v)*y;
} }
var nTeams = teams.length;
function updateSquareAndBounce(x, y, dx, dy, color) { function updateSquareAndBounce(x, y, dx, dy, color) {
if (state[color].elim) return if (state[color].elim) return
if (Math.max(Math.abs(dx), Math.abs(dy)) < 0.02) state[color].elim = true if (Math.max(Math.abs(dx), Math.abs(dy)) < 0.02) state[color].elim = true
nTeams = state.map(t => !t.elim).filter(Boolean).length
if (Math.min(x, y) < 0 || isNaN(x) || isNaN(y)) { if (Math.min(x, y) < 0 || isNaN(x) || isNaN(y)) {
console.warn(`warped ${teams[color].name}`) console.warn(`warped ${teams[color].name}`)
state[color].x = canvas.width * 0.1; state[color].x = canvas.width * 0.1;
@ -281,7 +393,10 @@
} }
// death coefficient (if not enough territory, slow down) // death coefficient (if not enough territory, slow down)
const coeff = Math.min((state[color].score/(numSquaresX*numSquaresY/nTeams/mix(1.2, 4, suddenDeathCoeff))), 1.00); var coeff = Math.min((state[color].score/threshold), 1.00);
// winners don't slow down
if (nTeams === 1) coeff = 1;
// death coefficient when no collision // death coefficient when no collision
const vacuumCoeff = coeff**(0.01); const vacuumCoeff = coeff**(0.01);
@ -329,13 +444,12 @@
console.log(`${teams[color].name} claims the remaining territory of ${teams[foreign].name}`) console.log(`${teams[color].name} claims the remaining territory of ${teams[foreign].name}`)
for (let ai = 0; ai < numSquaresX; ai++) { for (let ai = 0; ai < numSquaresX; ai++) {
for (let aj = 0; aj < numSquaresY; aj++) { for (let aj = 0; aj < numSquaresY; aj++) {
if (squares[ai][aj] == foreign) squares[ai][aj] = color; if (squares[ai][aj] == foreign) takeSquare(ai, aj, color);
} }
} }
} }
squareTaken[i][j] = squareTime; takeSquare(i, j, color);
squares[i][j] = color;
// Determine bounce direction based on the angle // Determine bounce direction based on the angle
if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) { if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) {
@ -375,21 +489,10 @@
if (!squares) { if (!squares) {
return; return;
} }
state.forEach((t) => (t.score = 0));
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
state[squares[i][j]].score++;
}
}
for (let i = 0; i < teams.length; i++) {
if (state[i].score <= 4) {
state[i].elim = true
}
}
if (nTeams > 1) { if (nTeams > 1) {
scoreElement.textContent = teams scoreElement.textContent = teams
.map((t, idx) => `(${idx}) ${t.name} ${state[idx].score}`) .map((t, idx) => `(${idxKeyMap[idx]}) ${t.name} ${state[idx].score}`)
.filter((t, idx) => !state[idx].elim) .filter((t, idx) => !state[idx].elim)
.join(" | ") + (suddenDeathCoeff > 0.1 ? " | sudden death!" : ""); .join(" | ") + (suddenDeathCoeff > 0.1 ? " | sudden death!" : "");
} else { } else {
@ -397,11 +500,26 @@
.filter((t, idx) => !state[idx].elim) .filter((t, idx) => !state[idx].elim)
.map((t) => `${t.name} 👑 wins`) .map((t) => `${t.name} 👑 wins`)
} }
thresholdElement.textContent = `threshold: ${Math.round(threshold)}`;
} }
function draw() { function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < teams.length; i++) {
drawSquares(); if (state[i].score <= 4) {
state[i].elim = true
}
}
nTeams = state.map(t => !t.elim).filter(Boolean).length;
// practically inertia (0-1)
const smoothCoeff = 0.999;
smoothNTeams = smoothNTeams * (smoothCoeff) + nTeams * (1 - smoothCoeff);
if (isNaN(smoothNTeams)) smoothNTeams = nTeams;
threshold = numSquaresX*numSquaresY/smoothNTeams/mix(1.2, 4, suddenDeathCoeff);
squareTime++; squareTime++;
squareTime %= 100000; squareTime %= 100000;
@ -409,7 +527,6 @@
for (let i = 0; i < teams.length; i++) { for (let i = 0; i < teams.length; i++) {
if (state[i].elim) continue if (state[i].elim) continue
const t = state[i]; const t = state[i];
drawBall(t.x, t.y, teams[i].color);
let bounce = updateSquareAndBounce(t.x, t.y, t.dx, t.dy, i); let bounce = updateSquareAndBounce(t.x, t.y, t.dx, t.dy, i);
t.dx = bounce.dx; t.dx = bounce.dx;
t.dy = bounce.dy; t.dy = bounce.dy;
@ -427,8 +544,10 @@
t.dy = -t.dy; t.dy = -t.dy;
} }
coverBall(t.x, t.y);
t.x += t.dx; t.x += t.dx;
t.y += t.dy; t.y += t.dy;
drawBall(t.x, t.y, teams[i].color);
} }
updateScoreElement(); updateScoreElement();