monorepo/docs/pongwars/index.html
dogeystamp 9f971fe9f7
pr makeover
this repo is no longer garbage.
2024-07-27 15:11:52 -04:00

708 lines
17 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Pong wars | Koen van Gilst</title>
<link rel="canonical" href="https://pong-wars.koenvangilst.nl/" />
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to bottom, #172b36 0%, #d9e8e3 100%);
}
#container {
display: flex;
width: 100%;
max-width: 2000px;
align-items: center;
flex-direction: column;
height: 100%;
}
#pongCanvas {
display: block;
border-radius: 4px;
overflow: hidden;
max-width: 90%;
margin-top: auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
}
#score {
font-family: monospace;
margin-top: 30px;
font-size: 20px;
padding-left: 20px;
color: #172b36;
}
#made {
font-family: monospace;
margin-top: auto;
font-size: 12px;
padding-left: 20px;
}
#mode {
font-family: monospace;
font-size: 12px;
list-style-type: none;
margin-bottom: 20px;
}
#instr {
font-family: monospace;
font-size: 12px;
}
#threshold {
font-family: monospace;
font-size: 8px;
}
#made a {
color: #172b36;
}
</style>
</head>
<body>
<div id="container">
<canvas id="pongCanvas" width="1200" height="800"></canvas>
<div id="score"></div>
<p id="instr">
balls can be controlled with keys shown to the left of their names
</p>
<p id="made">
made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on
<a href="https://github.com/vnglst/pong-wars">github</a>
<br>
patches from dogeystamp | patched source on
<a href="https://github.com/dogeystamp/monorepo/tree/main/pongwars">github</a>
</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>
<a class="modeLink" data-mode="massive">massive</a>
<a class="modeLink" data-mode="battle-royale">battle-royale</a>
</div>
</div>
</body>
<script>
// Based on: https://github.com/vnglst/pong-wars
// Idea for Pong wars: https://twitter.com/nicolasdnl/status/1749715070928433161
// This code is patched: see https://github.com/dogeystamp/monorepo/tree/main/pongwars
// Main features added are controlling balls and colors being able to die
const canvas = document.getElementById("pongCanvas");
const ctx = canvas.getContext("2d");
const scoreElement = document.getElementById("score");
const thresholdElement = document.getElementById("threshold");
var suddenDeathStart = null;
var suddenDeathCoeff = 0;
const teams = [
{
name: "red",
color: "indianred",
backgroundColor: "darkred",
},
{
name: "blue",
color: "blue",
backgroundColor: "darkblue",
},
{
name: "green",
color: "green",
backgroundColor: "darkgreen",
},
{
name: "orange",
color: "coral",
backgroundColor: "#DD4500",
},
{
name: "white",
color: "white",
backgroundColor: "gainsboro",
},
{
name: "black",
color: "#333333",
backgroundColor: "black",
},
{
name: "ourple",
color: "violet",
backgroundColor: "purple",
},
{
name: "gray",
color: "gray",
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",
},
{
name: "light-grey",
color: "darkgrey",
backgroundColor: "dimgrey",
},
{
name: "olive-green",
color: "darkkhaki",
backgroundColor: "darkolivegreen",
},
{
name: "dark-grey",
color: "darkgrey",
backgroundColor: "#222222",
},
{
name: "brown",
color: "chocolate",
backgroundColor: "sienna",
},
{
name: "khaki",
color: "cornsilk",
backgroundColor: "darkkhaki",
},
{
name: "sand",
color: "#BA9500",
backgroundColor: "burlywood",
},
{
name: "blood-red",
color: "palevioletred",
backgroundColor: "#440000",
},
{
name: "sea-green",
color: "mediumspringgreen",
backgroundColor: "mediumseagreen",
},
{
name: "peachpuff",
color: "white",
backgroundColor: "peachpuff",
},
{
name: "dark-brown",
color: "saddlebrown",
backgroundColor: "#3B1503",
},
{
name: "swamp",
color: "darkseagreen",
backgroundColor: "#2B2503",
},
{
name: "light-sea-green",
color: "mediumspringgreen",
backgroundColor: "darkseagreen",
},
{
name: "wood",
color: "cornsilk",
backgroundColor: "#BE9867",
},
{
name: "lavender",
color: "honeydew",
backgroundColor: "thistle",
},
{
name: "pale-violet",
color: "coral",
backgroundColor: "palevioletred",
},
{
name: "violet",
color: "violet",
backgroundColor: "blueviolet",
},
{
name: "epaper-white",
color: "black",
backgroundColor: "lemonchiffon",
},
{
name: "lcd-green",
color: "#A8C64E",
backgroundColor: "#4C412C",
},
{
name: "orange-blue",
color: "tomato",
backgroundColor: "darkslateblue",
},
{
name: "cyan",
color: "cornsilk",
backgroundColor: "#00AAAA",
},
{
name: "neon-green",
color: "olivedrab",
backgroundColor: "greenyellow",
},
{
name: "r",
color: "white",
backgroundColor: "#DD0000",
},
{
name: "g",
color: "white",
backgroundColor: "#00DD00",
},
{
name: "b",
color: "white",
backgroundColor: "#0000DD",
},
];
var state = {}
var squareSize = 16;
var numSquaresX = canvas.width / squareSize;
var numSquaresY = canvas.height / squareSize;
let squares = [];
// matrix of "timestamps" where each square was claimed
let squareTaken = [];
var squareTime = 0;
// threshold for territory under which a color starts dying
let threshold = 0;
var nTeams = teams.length;
var origNTeams = nTeams;
// 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) => {
const idx = keyToIdx(event.key)
if (idx === null) return
state[idx].boostEnabled = true;
})
document.addEventListener("keyup", (event) => {
const idx = keyToIdx(event.key)
if (idx === null) return
state[idx].boostEnabled = false;
})
const params = new URLSearchParams(window.location.search);
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;
}
// a lot of things are based on canvas coords not real square coords
function compensateSquareSize(x) {
return x / 16 * squareSize;
}
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;
origNTeams = nTeams;
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,
x: 0,
y: 0,
dx: 0,
dy: 0,
score: 0,
// how many consecutive contested tiles it has hit
conflict: 0,
}));
// base noise pattern (failsafe in case the grid doesn't cover some part)
for (let i = 0; i < numSquaresX; i++) {
squares[i] = [];
squareTaken[i] = [];
for (let j = 0; j < numSquaresY; j++) {
const t = randInt(0, teams.length-1);
squares[i][j] = t;
squareTaken[i][j] = squareTime;
}
}
const enableGrid = true;
if (enableGrid) {
for (let i = 0; i < gridW; i++) {
for (let j = 0; j < gridH; j++) {
const gridIdx = j * gridW + i;
const t = (gridIdx < teams.length) ? gridIdx : null;
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.ceil((j+1)/gridH*numSquaresY); y++) {
if (t !== null) {
squares[x][y] = t;
}
}
}
if (t !== null) {
state[t].x = Math.floor((i+0.5)/gridW*canvas.width);
state[t].y = Math.floor((j+0.5)/gridH*canvas.height);
}
}
}
for (let i = 0; i < teams.length; i++) {
const angle = randomNum(0, 2 * Math.PI);
const initSpeed = compensateSquareSize(8);
state[i].dx = initSpeed * Math.cos(angle);
state[i].dy = initSpeed * 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 "massive":
initialState({gridW: 5, gridH: 4, cols: 20, canvasW: 2000, canvasH: 1500, squareSize: 10});
break;
case "battle-royale":
initialState({gridW: 9, gridH: 4, cols: 36, canvasW: 2000, canvasH: 1000, squareSize: 5});
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;
}
function randomNum(min, max) {
return Math.random() * (max - min) + min;
}
function randInt(min, max) {
return Math.floor(randomNum(min, max+1))
}
function elapsedSec() {
return ((new Date()) - suddenDeathStart)/1000
}
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) {
ctx.beginPath();
ctx.arc(x, y, squareSize / 2, 0, Math.PI * 2, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
function drawSquare(i, j) {
ctx.fillStyle = teams[squares[i][j]].backgroundColor;
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) {
return Math.min(max, Math.max(min, num))
}
function mix(x, y, v) {
return v*x + (1-v)*y;
}
function updateSquareAndBounce(x, y, dx, dy, color) {
if (state[color].elim) return
if (Math.max(Math.abs(dx), Math.abs(dy)) < compensateSquareSize(0.02)) state[color].elim = true
if (Math.min(x, y) < 0 || isNaN(x) || isNaN(y)) {
console.warn(`warped ${teams[color].name}`)
state[color].x = canvas.width * 0.1;
state[color].y = canvas.height * 0.1;
dx = 4;
dy = 4;
}
if (x > canvas.width || y > canvas.height) {
console.warn(`warped ${teams[color].name}`)
state[color].x = canvas.width * 0.9;
state[color].y = canvas.height * 0.9;
dx = -4;
dy = -4;
}
// death coefficient (if not enough territory, slow down)
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
const vacuumCoeff = coeff**(0.01);
let updatedDx = dx * vacuumCoeff;
let updatedDy = dy * vacuumCoeff;
// Check multiple points around the ball's circumference
for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) {
let checkX = x + Math.cos(angle) * (squareSize / 2);
let checkY = y + Math.sin(angle) * (squareSize / 2);
let i = Math.floor(checkX / squareSize);
let j = Math.floor(checkY / squareSize);
if (i >= 0 && i < numSquaresX && j >= 0 && j < numSquaresY) {
if (squares[i][j] !== color) {
const foreign = squares[i][j];
if (!state[foreign].elim) {
// in the event that the ball gets entangled with another on a contested square really long,
// have them duel and one of them will win (the one with more territory is more likely to win)
if (squareTime == squareTaken[i][j]) {
state[color].conflict++;
if (state[color].conflict >= 100 && randInt(0, 1) == 1) {
const ratio = state[color].score / (state[foreign].score + state[color].score);
console.log(`${teams[color].name} and ${teams[foreign].name} start a duel (territory ratio ${ratio.toFixed(2)})`);
const val = Math.random();
if (val == ratio) {
console.log(`nobody wins the duel`);
} else if (val < ratio) {
console.log(`${teams[color].name} vanquishes ${teams[foreign].name}`);
state[foreign].elim = true;
} else if (val > ratio) {
console.log(`${teams[foreign].name} vanquishes ${teams[color].name}`);
state[color].elim = true;
}
state[color].conflict = 0;
state[foreign].conflict = 0;
}
} else {
state[color].conflict = 0;
}
}
if (state[foreign].elim) {
console.log(`${teams[color].name} claims the remaining territory of ${teams[foreign].name}`)
for (let ai = 0; ai < numSquaresX; ai++) {
for (let aj = 0; aj < numSquaresY; aj++) {
if (squares[ai][aj] == foreign) takeSquare(ai, aj, color);
}
}
}
takeSquare(i, j, color);
// Determine bounce direction based on the angle
if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) {
updatedDx = -updatedDx;
} else {
updatedDy = -updatedDy;
}
updatedDx += compensateSquareSize(randomNum(-0.15, mix(0.153, 0.15, suddenDeathCoeff)))
updatedDy += compensateSquareSize(randomNum(-0.15, 0.15))
updatedDx *= coeff;
updatedDy *= coeff;
const speedLim = compensateSquareSize(mix(30, 15, suddenDeathCoeff))
const norm = (updatedDx**2 + updatedDy**2)**(1/2)
const scalar = Math.min(speedLim/norm, 1)
updatedDx *= scalar;
updatedDy *= scalar;
}
}
}
const theta = (Math.PI/180)*10;
const ct = Math.cos(theta);
const st = Math.sin(theta);
if (state[color].boostEnabled) {
const rotDx = (ct * updatedDx - st * updatedDy);
const rotDy = (st * updatedDx + ct * updatedDy);
updatedDx = rotDx;
updatedDy = rotDy;
}
return { dx: updatedDx, dy: updatedDy };
}
function updateScoreElement() {
if (!squares) {
return;
}
if (nTeams > 1) {
scoreElement.textContent = teams
.map((t, idx) => `(${idxKeyMap[idx]}) ${t.name} ${state[idx].score}`)
.filter((t, idx) => !state[idx].elim)
.join(" | ") + (suddenDeathCoeff > 0.1 ? " | sudden death!" : "");
} else {
scoreElement.textContent = teams
.filter((t, idx) => !state[idx].elim)
.map((t) => `${t.name} 👑 wins`)
}
thresholdElement.textContent = `threshold: ${Math.round(threshold)}`;
}
function draw() {
for (let i = 0; i < teams.length; i++) {
if (state[i].score <= 4) {
state[i].elim = true
}
if (state[i].score > 0 && state[i].elim) {
// draw over dead balls
coverBall(state[i].x, state[i].y)
}
}
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 %= 100000;
for (let i = 0; i < teams.length; i++) {
if (state[i].elim) continue
const t = state[i];
let bounce = updateSquareAndBounce(t.x, t.y, t.dx, t.dy, i);
t.dx = bounce.dx;
t.dy = bounce.dy;
if (
t.x + t.dx > canvas.width - squareSize / 2 ||
t.x + t.dx < squareSize / 2
) {
t.dx = -t.dx;
}
if (
t.y + t.dy > canvas.height - squareSize / 2 ||
t.y + t.dy < squareSize / 2
) {
t.dy = -t.dy;
}
coverBall(t.x, t.y);
t.x += t.dx;
t.y += t.dy;
drawBall(t.x, t.y, teams[i].color);
}
updateScoreElement();
if (suddenDeathStart !== null) {
suddenDeathCoeff = clamp(0, 1, (elapsedSec()/60/5)**15);
} else if (smoothNTeams <= origNTeams * 0.33) {
const delta = 1000 * 60 * 5 * randomNum(-0.75, 1);
suddenDeathStart = new Date((new Date()).getTime() + delta);
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
</script>
</html>