summaryrefslogtreecommitdiff
path: root/static/src/_posts/2021-04-11-ripple-v2.md
diff options
context:
space:
mode:
Diffstat (limited to 'static/src/_posts/2021-04-11-ripple-v2.md')
-rw-r--r--static/src/_posts/2021-04-11-ripple-v2.md436
1 files changed, 0 insertions, 436 deletions
diff --git a/static/src/_posts/2021-04-11-ripple-v2.md b/static/src/_posts/2021-04-11-ripple-v2.md
deleted file mode 100644
index cbde032..0000000
--- a/static/src/_posts/2021-04-11-ripple-v2.md
+++ /dev/null
@@ -1,436 +0,0 @@
----
-title: >-
- Ripple V2: A Better Game
-description: >-
- The sequel no one was waiting for!
-tags: tech
-series: ripple
----
-
-<p>
- <b>Movement:</b> Arrow keys or WASD<br/>
- <b>Jump:</b> Space<br/>
- <b>Goal:</b> Jump as many times as possible without touching a ripple!<br/>
- <br/>
- <b>Press Jump To Begin!</b>
-</p>
-
-_Who can make the muddy water clear?<br/>
-Let it be still, and it will gradually become clear._
-
-<canvas id="canvas"
- style="border:1px dashed #AAA"
- tabindex=0>
-Your browser doesn't support canvas. At this point in the world that's actually
-pretty cool, well done!
-</canvas>
-<button onclick="reset()">(R)eset</button>
-<span style="font-size: 2rem; margin-left: 1rem;">Score:
- <span style="font-weight: bold" id="score">0</span>
-</span>
-
-<script type="text/javascript">
-
-const palette = [
- "#264653",
- "#2A9D8F",
- "#E9C46A",
- "#F4A261",
- "#E76F51",
-];
-
-const width = 800;
-const height = 600;
-
-function hypotenuse(w, h) {
- return Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2));
-}
-
-let canvas = document.getElementById("canvas");
-canvas.width = width;
-canvas.height = height;
-
-const whitelistedKeys = {
- "ArrowUp": {},
- "KeyW": {map: "ArrowUp"},
- "ArrowLeft": {},
- "KeyA": {map: "ArrowLeft"},
- "ArrowRight": {},
- "KeyD": {map: "ArrowRight"},
- "ArrowDown": {},
- "KeyS": {map: "ArrowDown"},
- "Space": {},
- "KeyR": {},
-};
-
-let keyboard = {};
-
-canvas.addEventListener('keydown', (event) => {
- let keyInfo = whitelistedKeys[event.code];
- if (!keyInfo) return;
-
- let code = event.code;
- if (keyInfo.map) code = keyInfo.map;
-
- event.preventDefault();
- keyboard[code] = true;
-});
-
-canvas.addEventListener('keyup', (event) => {
- let keyInfo = whitelistedKeys[event.code];
- if (!keyInfo) return;
-
- let code = event.code;
- if (keyInfo.map) code = keyInfo.map;
-
- event.preventDefault();
- delete keyboard[code];
-});
-
-
-const C = 700; // scales the overall speed of the radius
-const T = 500; // on which tick the radius change becomes linear
-
-/*
- f(x) = sqrt(C*x) when x < T
- (C/(2*sqrt(CT)))(x-T) + sqrt(CT) when x >= T
-
- radius(x) = f(x) + playerRadius;
-*/
-
-const F1 = (x) => Math.sqrt(C*x);
-const F2C1 = C / (2 * Math.sqrt(C*T));
-const F2C2 = Math.sqrt(C * T);
-const F2 = (x) => (F2C1 * (x - T)) + F2C2;
-const F = (x) => {
- if (x < T) return F1(x);
- return F2(x);
-};
-
-class Ripple {
-
- constructor(id, currTick, x, y, bounces, color) {
- this.id = id;
- this.tick = currTick;
- this.x = x;
- this.y = y;
- this.thickness = Math.pow(bounces+1, 1.25);
- this.color = color;
- this.winner = false;
-
- this.maxRadius = hypotenuse(x, y);
- this.maxRadius = Math.max(this.maxRadius, hypotenuse(width-x, y));
- this.maxRadius = Math.max(this.maxRadius, hypotenuse(x, height-y));
- this.maxRadius = Math.max(this.maxRadius, hypotenuse(width-x, height-y));
- }
-
- radius(currTick) {
- const x = currTick - this.tick;
- return F(x) + playerRadius;
- }
-
- draw(ctx, currTick) {
- ctx.beginPath();
- ctx.arc(this.x, this.y, this.radius(currTick), 0, Math.PI * 2, false);
- ctx.closePath();
- ctx.lineWidth = this.thickness;
- ctx.strokeStyle = this.winner ? "#FF0000" : this.color;
- ctx.stroke();
- }
-
- canGC(currTick) {
- return this.radius(currTick) > this.maxRadius;
- }
-}
-
-const playerRadius = 10;
-const playerMoveAccel = 0.5;
-const playerMoveDecel = 0.7;
-const playerMaxMoveSpeed = 4;
-const playerJumpSpeed = 0.08;
-const playerMaxHeight = 1;
-const playerGravity = 0.01;
-
-class Player{
-
- constructor(x, y, color) {
- this.x = x;
- this.y = y;
- this.z = 0;
- this.xVelocity = 0;
- this.yVelocity = 0;
- this.zVelocity = 0;
- this.color = color;
- this.falling = false;
- this.lastJumpHeight = 0;
- this.loser = false;
- }
-
- act() {
- if (keyboard["ArrowUp"]) {
- this.yVelocity = Math.max(-playerMaxMoveSpeed, this.yVelocity - playerMoveAccel);
- } else if (keyboard["ArrowDown"]) {
- this.yVelocity = Math.min(playerMaxMoveSpeed, this.yVelocity + playerMoveAccel);
- } else if (this.yVelocity > 0) {
- this.yVelocity = Math.max(0, this.yVelocity - playerMoveDecel);
- } else if (this.yVelocity < 0) {
- this.yVelocity = Math.min(0, this.yVelocity + playerMoveDecel);
- }
-
- this.y += this.yVelocity;
- this.y = Math.max(0+playerRadius, this.y);
- this.y = Math.min(height-playerRadius, this.y);
-
- if (keyboard["ArrowLeft"]) {
- this.xVelocity = Math.max(-playerMaxMoveSpeed, this.xVelocity - playerMoveAccel);
- } else if (keyboard["ArrowRight"]) {
- this.xVelocity = Math.min(playerMaxMoveSpeed, this.xVelocity + playerMoveAccel);
- } else if (this.xVelocity > 0) {
- this.xVelocity = Math.max(0, this.xVelocity - playerMoveDecel);
- } else if (this.xVelocity < 0) {
- this.xVelocity = Math.min(0, this.xVelocity + playerMoveDecel);
- }
-
- this.x += this.xVelocity;
- this.x = Math.max(0+playerRadius, this.x);
- this.x = Math.min(width-playerRadius, this.x);
-
- let jumpHeld = keyboard["Space"];
-
- if (jumpHeld && !this.falling && this.z < playerMaxHeight) {
- this.lastJumpHeight = 0;
- this.zVelocity = playerJumpSpeed;
- } else {
- this.zVelocity = Math.max(-playerJumpSpeed, this.zVelocity - playerGravity);
- this.falling = this.z > 0;
- }
-
- let prevZ = this.z;
- this.z = Math.max(0, this.z + this.zVelocity);
- this.lastJumpHeight = Math.max(this.z, this.lastJumpHeight);
- }
-
- draw(ctx) {
- let y = this.y - (this.z * 40);
- let radius = playerRadius * (this.z+1)
-
- // draw main
- ctx.beginPath();
- ctx.arc(this.x, y, radius, 0, Math.PI * 2, false);
- ctx.closePath();
- ctx.lineWidth = 0;
- ctx.fillStyle = this.color;
- ctx.fill();
- if (this.loser) {
- ctx.strokeStyle = '#FF0000';
- ctx.lineWidth = 2;
- ctx.stroke();
- }
-
- // draw shadow, if in the air
- if (this.z > 0) {
- let radius = Math.max(0, playerRadius * (1.2 - this.z));
- ctx.beginPath();
- ctx.arc(this.x, this.y, radius, 0, Math.PI * 2, false);
- ctx.closePath();
- ctx.lineWidth = 0;
- ctx.fillStyle = this.color+"33";
- ctx.fill();
- }
- }
-}
-
-class Game {
-
- constructor(canvas, scoreEl) {
- this.currTick = 0;
- this.player = new Player(width/2, height/2, palette[0]);
- this.state = 'play';
- this.score = 0;
- this.scoreEl = scoreEl;
- this.canvas = canvas;
- this.ctx = canvas.getContext("2d");
- this.ripples = [];
- this.nextRippleID = 0;
- }
-
- shouldReset() {
- return keyboard['KeyR'];
- }
-
- newRippleID() {
- let id = this.nextRippleID;
- this.nextRippleID++;
- return id;
- }
-
- // newRipple initializes and stores a new ripple at the given coordinates, as
- // well as all sub-ripples which make up the initial ripple's reflections.
- newRipple(x, y, bounces, color) {
- color = color ? color : palette[Math.floor(Math.random() * palette.length)];
-
- let ripplePos = [];
- let nextRipples = [];
-
- let addRipple = (x, y) => {
- for (let i in ripplePos) {
- if (ripplePos[i][0] == x && ripplePos[i][1] == y) return;
- }
-
- let ripple = new Ripple(this.newRippleID(), this.currTick, x, y, bounces, color);
- nextRipples.push(ripple);
- ripplePos.push([x, y]);
- this.ripples.push(ripple);
- };
-
- // add initial ripple, after this we deal with the sub-ripples.
- addRipple(x, y);
-
- while (bounces > 0) {
- bounces--;
- let prevRipples = nextRipples;
- nextRipples = [];
-
- for (let i in prevRipples) {
- let prevX = prevRipples[i].x;
- let prevY = prevRipples[i].y;
- addRipple(prevX, -prevY);
- addRipple(-prevX, prevY);
- addRipple((2*this.canvas.width)-prevX, prevY);
- addRipple(prevX, (2*this.canvas.height)-prevY);
- }
- }
- }
-
- // playerRipplesState returns a mapping of rippleID -> boolean, where each
- // boolean indicates the ripple's relation to the player at the moment. true
- // indicates the player is outside the ripple, false indicates the player is
- // within the ripple.
- playerRipplesState() {
- let state = {};
- for (let i in this.ripples) {
- let ripple = this.ripples[i];
- let rippleRadius = ripple.radius(this.currTick);
- let hs = Math.pow(ripple.x-this.player.x, 2) + Math.pow(ripple.y-this.player.y, 2);
- state[ripple.id] = hs > Math.pow(rippleRadius + playerRadius, 2);
- }
- return state;
- }
-
- playerHasJumpedOverRipple(prev, curr) {
- for (const rippleID in prev) {
- if (!curr.hasOwnProperty(rippleID)) continue;
- if (curr[rippleID] != prev[rippleID]) return true;
- }
- return false;
- }
-
- update() {
- if (this.state != 'play') return;
-
- let playerPrevZ = this.player.z;
- this.player.act();
-
- if (playerPrevZ == 0 && this.player.z > 0) {
- // player has jumped
- this.prevPlayerRipplesState = this.playerRipplesState();
-
- } else if (playerPrevZ > 0 && this.player.z == 0) {
-
- // player has landed, don't produce a ripple unless there are no
- // existing ripples or the player jumped over an existing one.
- if (
- this.ripples.length == 0 ||
- this.playerHasJumpedOverRipple(
- this.prevPlayerRipplesState,
- this.playerRipplesState()
- )
- ) {
- let bounces = Math.floor((this.player.lastJumpHeight*1.8)+1);
- console.log("spawning ripple with bounces:", bounces);
- this.newRipple(this.player.x, this.player.y, bounces);
- this.score += bounces;
- }
- }
-
- if (this.player.z == 0) {
- for (let i in this.ripples) {
- let ripple = this.ripples[i];
- let rippleRadius = ripple.radius(this.currTick);
- if (rippleRadius < playerRadius * 1.5) continue;
- let hs = Math.pow(ripple.x-this.player.x, 2) + Math.pow(ripple.y-this.player.y, 2);
- if (hs > Math.pow(rippleRadius + playerRadius, 2)) {
- continue;
- } else if (hs <= Math.pow(rippleRadius - playerRadius, 2)) {
- continue;
- } else {
- console.log("game over", ripple);
- ripple.winner = true;
- this.player.loser = true;
- this.state = 'gameOver';
- // deliberately don't break here, in case multiple ripples hit
- // the player on the same frame
- }
- }
- }
-
- this.ripples = this.ripples.filter(ripple => !ripple.canGC(this.currTick));
-
- this.currTick++;
- }
-
- draw() {
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
- this.ripples.forEach(ripple => ripple.draw(this.ctx, this.currTick));
- this.player.draw(this.ctx)
- this.scoreEl.innerHTML = this.score;
- }
-}
-
-
-const requestAnimationFrame =
- window.requestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.msRequestAnimationFrame;
-
-let game = new Game(canvas, document.getElementById("score"));
-
-function reset() {
- game = new Game(canvas, document.getElementById("score"));
-}
-
-function nextFrame() {
- if (game.shouldReset()) reset();
-
- game.update()
- game.draw()
- requestAnimationFrame(nextFrame);
-}
-requestAnimationFrame(nextFrame);
-
-canvas.focus();
-
-</script>
-
-## Changelog
-
-There's been two major changes to the mechanics of the game since the previous
-version:
-
-* A new ripple is created _only_ if there are no ripples on the field already,
- or if the player has jumped over an existing ripple.
-
-* The score is increased only if a ripple is created, and is increased by the
- number of bounces off the wall that ripple will have. Put another way, the
- score is increased based on how high you jump.
-
-Other small changes include:
-
-* Ripple growth rate has been modified. It's now harder for a player to run into
- the ripple they just created.
-
-* Ripple thickness indicates how many bounces are left in the ripple. This was
- the case previously, but it's been made more obvious.
-
-* Small performance improvements.