Public
Edited
Jan 30, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
canvas = {
const context = DOM.context2d(width, height);
context.fillStyle = "#ffffff";
context.clearRect(0, 0, width, height);

context.canvas.id = "main-canvas";

return context.canvas;
}
Insert cell
ctx = canvas.getContext("2d");
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
startSimulation = {
// ballSpeed;
// barGrowthSpeed;
// resetGame();
init()
}
Insert cell
init = () => {
const tick = () => {
let endState = false;
if (
simulation.lives > 0 &&
(!simulation.areaCleared || simulation.areaCleared < 75)
) {
requestAnimationFrame(tick);
} else {
simulation.tick();
endState = true;
}
simulation.tick(endState);
};

tick();
}
Insert cell
scoredCells = simulation.grid.filter((a) => a.scored)
Insert cell
mutable clickPosition = null;
Insert cell
clickHandler = {
// Click handler
d3.select("#main-canvas").on("click", (e) => {
mutable clickPosition = d3.pointer(e);
const [x, y] = d3.pointer(e);
// If we're in a game over state, or between rounds, we're going to handle clicks differently
if (simulation.roundState !== "running") {
// Check if click is on button
if (x >= simulation.button.left && x <= simulation.button.right &&
y >= simulation.button.top && y <= simulation.button.bottom) {
if (simulation.button.type === "game") {
resetGame();
}
else {
nextRound();
}
}
return;
}
// Check if this grid position is already scored (occupied by bar or scored space)
let gridPositionFilled = false;
const gridX = roundToGrid(x) / gridSize;
const gridY = roundToGrid(y) / gridSize;
const cellId = gridX*gridHeight + gridY;
// If so, return without spawning new bars
if (simulation.grid[cellId].scored) {
return;
}
// No longer allowing new bars to spawn while there are still active bars (true to the original game)
if (simulation.bars.find(bar => bar.active === true)) {
return;
};
// Otherwise, spawn two new bar segments to the left/right or up/down, depending on selected orientation
if (barDirection === "Horizontal") {
addBar({ x, y, direction: "left" });
addBar({ x, y, direction: "right" });
}
else {
addBar({ x, y, direction: "up" });
addBar({ x, y, direction: "down" });
}
})

// Right-click handler
d3.select("#main-canvas").on("contextmenu", (e) => {
e.preventDefault();
const direction = barDirection === "Horizontal" ? "Vertical" : "Horizontal";
set(viewof barDirection, direction);
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class Simulation {
constructor({ width, height, ctx, balls, bars, grid }) {
this.width = width || 800;
this.height = height || 800;
this.center = [this.width / 2, this.height / 2];

this.ctx = ctx || null;

this.maxRadius = d3.max(balls, (a) => a.radius);
this.quadtree = null;

this.balls = balls || [];
this.bars = bars || [];
this.grid = grid;
this.grid.forEach((cell) => {
cell.scored = false;
});

this.fillQueue = [];
this.button = null;

// Game state
this.roundState = "running";
this.lives = 2;
this.totalScore = 0;
this.roundScore = 0;
this.level = 1;
this.areaCleared = 0;

// Collision methods
this.fastDetectBallCollision = fastDetectBallCollision;
this.detectBallWallCollision = detectBallWallCollision;
this.detectBarWallCollision = detectBarWallCollision;
this.detectBallBarCollision = detectBallBarCollision;
this.detectBarToBarCollision = detectBarToBarCollision;
this.evaluateGrid = evaluateGrid;

// End state methods
this.endGame = endGame;
this.endRound = endRound;

// this.resetGame = resetGame;
// this.nextRound = nextRound;
}

clearCanvas() {
this.ctx.fillStyle = "#ffffff";
this.ctx.clearRect(0, 0, width, height);
}

tick(endState = false) {
if (endState) {
mutable areaCleared = this.areaCleared;

if (this.lives <= 0) {
this.endGame();
} else {
this.endRound();
}

return;
}

this.clearCanvas();

this.grid.forEach((cell) => {
cell.drawCell();
});

this.quadtree = d3
.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.extent([-1, -1], [this.width + 1, this.height + 1])
.addAll(this.balls);

this.balls.forEach((ball) => {
this.detectBallBarCollision(ball);
this.detectBallWallCollision(ball);
this.fastDetectBallCollision(ball);

ball.tick();
ball.drawCircle();
});

this.bars = this.bars.filter((bar) => bar.remove === false);

this.bars.forEach((bar) => {
if (bar.active) {
this.detectBarWallCollision(bar);
this.detectBarToBarCollision(bar);
bar.tick();
}

bar.drawRect();
});

if (this.bars.filter((bar) => bar.active === true).length === 0) {
this.fillQueue.forEach((bar) => {
this.evaluateGrid(bar);
});

this.fillQueue = [];
}
}
}
Insert cell
nextRound = function() {
simulation.totalScore += simulation.roundScore;
simulation.roundScore = 0;
simulation.level += 1;
simulation.lives = simulation.level + 1;
simulation.areaCleared = 0;
mutable areaCleared = 0;
mutable currentLives = simulation.level + 1;
simulation.balls = ballSet((simulation.level + 1));
simulation.bars = [];
simulation.grid.forEach(cell => { cell.scored = false });
simulation.fillQueue = [];
simulation.button = null;
simulation.roundState = "running";
init();
}
Insert cell
resetGame = function() {
simulation.roundScore = 0;
simulation.totalScore = 0;
simulation.lives = 2;
simulation.level = 1;
simulation.areaCleared = 0;

mutable areaCleared = 0;
mutable currentLives = 2;

simulation.balls = ballSet(2);
simulation.bars = [];
simulation.grid.forEach(cell => { cell.scored = false });

simulation.fillQueue = [];
simulation.button = null;
simulation.roundState = "running";

init();
}
Insert cell
endGame = function() {
this.roundState = "game over";
this.ctx.font = `bold ${sizeModifier < 0.8 ? 35 : 50}px Courier`;
this.ctx.strokeStyle = "white";
this.ctx.fillStyle = "#787878";
this.ctx.textAlign = "center";
this.ctx.fillText("Game Over", this.center[0], this.center[1] - 60);
this.ctx.strokeText("Game Over", this.center[0], this.center[1] - 60);

this.ctx.fillStyle = "#787878";
this.ctx.strokeStyle = "white";

this.ctx.fillRect(this.center[0] - 70, this.center[1] - 40, 140, 35);
this.ctx.strokeRect(this.center[0] - 70, this.center[1] - 40, 140, 35);

this.ctx.font = "20px Courier";
this.ctx.fillStyle = "white";
this.ctx.fillText("New Game", this.center[0], this.center[1] - 17);

this.button = ({ type: "game", left: this.center[0] - 70, right: this.center[0] + 70, top: this.center[1] - 40, bottom: this.center[1] - 5});
return;
}
Insert cell
endRound = function() {
this.roundState = "between rounds";
this.ctx.font = `bold ${sizeModifier < 0.8 ? 32 : 50}px Courier`;
this.ctx.strokeStyle = "white";
this.ctx.fillStyle = "#787878";
this.ctx.textAlign = "center";
this.ctx.fillText("Round Complete!", this.center[0], this.center[1] - 60);
this.ctx.strokeText("Round Complete!", this.center[0], this.center[1] - 60);

this.ctx.fillStyle = "#787878";
this.ctx.strokeStyle = "white";

this.ctx.fillRect(this.center[0] - 70, this.center[1] - 40, 140, 35);
this.ctx.strokeRect(this.center[0] - 70, this.center[1] - 40, 140, 35);

this.ctx.font = "20px Courier";
this.ctx.fillStyle = "white";
this.ctx.fillText("Next Round", this.center[0], this.center[1] - 17);

this.button = ({ type: "round", left: this.center[0] - 70, right: this.center[0] + 70, top: this.center[1] - 40, bottom: this.center[1] - 5});
return;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more