Skip to content

Instantly share code, notes, and snippets.

@ericcitaire
Last active June 10, 2019 17:03
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ericcitaire/5408146 to your computer and use it in GitHub Desktop.
Save ericcitaire/5408146 to your computer and use it in GitHub Desktop.
D3 JezzBall
/.project
/.settings

A clone of JezzBall.

The goal is to fill at least 75% of the room. Click on any corner button or use mouse wheel to change the orientation of the wall builder. For more detailed instructions, see JezzBall Walkthrough/FAQ page on IGN.

Enjoy ! :)

<!DOCTYPE html>
<meta charset="utf-8">
<title>D3 JezzBall</title>
<link rel="icon" href="/favicon.png">
<style>
@import url("http://bl.ocks.org/style.css?20120730");
</style>
<header>
<a href="/ericcitaire">ericcitaire</a>’s block <a href="https://gist.github.com/ericcitaire/5408146">#5408146</a>
</header>
<h1>D3 JezzBall</h1>
<p><aside style="margin-top:-43px;">April 29, 2013</aside>
<iframe src="index.html" marginwidth="0" marginheight="0" scrolling="no"></iframe>
<p><aside><a style="position:relative;top:6px;" href="index.html" target="_blank">Open in a new window.</a></aside>
<footer>
<aside>April 29, 2013</aside>
<a href="/ericcitaire">ericcitaire</a>’s block <a href="https://gist.github.com/ericcitaire/5408146">#5408146</a>
</footer>
<!DOCTYPE html>
<meta charset="utf-8">
<title>D3 JezzBall</title>
<style>
body {
padding: 0px;
margin: 0px;
}
rect {
fill: none;
pointer-events: all;
}
.smallTextStroke {
font-family: sans-serif;
font-size: 12px;
fill: none;
stroke: black;
stroke-width: 1px;
}
.smallText {
font-family: sans-serif;
font-size: 12px;
fill: white;
}
.bigTextStroke {
font-family: sans-serif;
font-size: 24px;
font-weight: bold;
fill: none;
stroke: black;
stroke-width: 4px;
}
.bigText {
font-family: sans-serif;
font-size: 24px;
font-weight: bold;
fill: white;
}
.cell {
}
.cell.wall {
fill: #000;
stroke: #000;
}
.cell.air {
fill: #ddd;
stroke: #999;
}
.cell.blue {
stroke: blue;
stroke-opacity: .8;
}
.cell.red {
stroke: red;
stroke-opacity: .8;
}
.ball {
fill: #f00;
fill-opacity: 1;
stroke: #333;
stroke-opacity: .5;
}
.builder.head {
stroke: #333;
stroke-opacity: .5;
}
.builder.blue {
fill: blue;
}
.builder.red {
fill: red;
}
.builder.tail {
fill-opacity: .4;
}
.switch {
position: absolute;
top: 0px;
left: 0px;
padding: 0px;
width: 20px;
height: 20px;
}
#nextLevelButton, #playAgainButton {
position: absolute;
width: 100px;
visibility: hidden;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<form>
<input id="switchOrientationButton1" class="switch" type="button" value="V" />
<input id="switchOrientationButton2" class="switch" type="button" value="V" />
<input id="switchOrientationButton3" class="switch" type="button" value="V" />
<input id="switchOrientationButton4" class="switch" type="button" value="V" />
<input id="nextLevelButton" type="button" value="Next level" />
<input id="playAgainButton" type="button" value="Play again" />
</form>
<script type="text/javascript" src="jezzball.js"></script>
d3.selection.prototype.size = function() {
var n = 0;
this.each(function() { ++n; });
return n;
};
var level = 1;
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
if (key == "level") {
level = +value;
}
});
var w = 960,
h = 500,
sz = 20,
r = sz / 2,
sr = r * r,
ssz = sz * sz,
v = 3,
n = level + 1,
t = 5000;
var rows = Math.ceil(h / sz);
var cols = Math.ceil(w / sz);
var s = false;
d3.selectAll(".switch")
.on("click", function(e) { s = !s; d3.selectAll(".switch").attr("value", s ? "H" : "V"); });
d3.select("#switchOrientationButton2")
.style("left", (w - 20) + "px");
d3.select("#switchOrientationButton3")
.style("top", (h - 20) + "px");
d3.select("#switchOrientationButton4")
.style("top", (h - 20) + "px")
.style("left", (w - 20) + "px");
d3.select("#nextLevelButton")
.style("top", Math.round(h / 2 + 20) + "px")
.style("left", Math.round(w / 2 - 50) + "px")
.on("click", function(e) { window.location.href = "?level=" + (level + 1); });
d3.select("#playAgainButton")
.style("top", Math.round(h / 2 + 20) + "px")
.style("left", Math.round(w / 2 - 50) + "px")
.on("click", function(e) { window.location.href = "?level=1"; });
var cells = d3.range(0, rows * cols).map(function (d) {
var col = d % cols;
var row = (d - col) / cols;
return {
r: row,
c: col,
x: col * sz + r,
y: row * sz + r
};
});
var balls = d3.range(0, n).map(function (d) {
var bx = (w - sz * 4) * Math.random() + sz * 2;
var by = (h - sz * 4) * Math.random() + sz * 2;
var ball = {
x: bx,
y: by,
px: bx + v * (Math.random() > .5 ? 1 : -1),
py: by + v * (Math.random() > .5 ? 1 : -1),
id: d,
isMoving: true
};
return ball;
});
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var mousewheel = function(e) {
s = !s;
d3.selectAll(".switch").attr("value", s ? "H" : "V");
var p = d3.mouse(this);
var c1 = ballCell({x: p[0], y: p[1]});
previewLocation(c1, p);
d3.event.preventDefault();
};
svg.on("mousewheel.zoom", mousewheel)
.on("DOMMouseScroll.zoom", mousewheel);
var rectx = function(d) { return d.x - r; };
var recty = function(d) { return d.y - r; };
var tailx = function(d) { return d.dx > 0 ? d.sx - r : rectx(d) - d.dx * sz; };
var taily = function(d) { return d.dy > 0 ? d.sy - r : recty(d) - d.dy * sz; };
var tailw = function(d) { return d.dx == 0 ? sz : d.sz = (d.x - d.sx) * d.dx; };
var tailh = function(d) { return d.dy == 0 ? sz : d.sz = (d.y - d.sy) * d.dy; };
var ballCell = function(b) {
var row = (b.y - b.y % sz) / sz;
var col = (b.x - b.x % sz) / sz;
return cells[row * cols + col];
};
var topCell = function(c) { return cells[Math.max(0, c.r - 1) * cols + c.c]; };
var leftCell = function(c) { return cells[c.r * cols + Math.max(0, c.c - 1)]; };
var bottomCell = function(c) { return cells[Math.min(rows - 1, c.r + 1) * cols + c.c]; };
var rightCell = function(c) { return cells[c.r * cols + Math.min(cols - 1, c.c + 1)]; };
var topLeftCell = function(c) { return cells[Math.max(0, c.r - 1) * cols + Math.max(0, c.c - 1)]; };
var bottomLeftCell = function(c) { return cells[Math.min(rows - 1, c.r + 1) * cols + Math.max(0, c.c - 1)]; };
var bottomRightCell = function(c) { return cells[Math.min(rows - 1, c.r + 1) * cols + Math.min(cols - 1, c.c + 1)]; };
var topRightCell = function(c) { return cells[Math.max(0, c.r - 1) * cols + Math.min(cols - 1, c.c + 1)]; };
var cell = svg.selectAll(".cell")
.data(cells)
.enter().append("rect")
.attr("class", function(d) { return "cell " + ((d.isWall = d.c == 0 || d.c == cols - 1 || d.r == 0 || d.r == rows - 1) ? "wall" : "air"); })
.attr("x", rectx)
.attr("y", recty)
.attr("width", sz)
.attr("height", sz)
.each(function(d) {
d.elnt = d3.select(this);
});
function previewLocation(c1, p) {
if (blue) blue.classed("blue", false);
if (red) red.classed("red", false);
var c2, d;
if (s) {
d = p[0] - c1.x;
c2 = d > 0 ? rightCell(c1) : leftCell(c1);
} else {
d = p[1] - c1.y;
c2 = d > 0 ? bottomCell(c1) : topCell(c1);
}
if (c1.isWall || c2.isWall) {
blue = null;
red = null;
} else if (d > 0) {
blue = c1.elnt;
red = c2.elnt;
} else {
blue = c2.elnt;
red = c1.elnt;
}
if (blue) blue.classed("blue", function(d) { return !d.isWall; });
if (red) red.classed("red", function(d) { return !d.isWall; });
}
var blue, red;
svg.selectAll(".air")
.on("mouseover", function(c1) {
var p = d3.mouse(this);
previewLocation(c1, p);
}).on("mouseout", function() {
if (blue) blue.classed("blue", false);
if (red) red.classed("red", false);
}).on("click", function() {
if (percentageCleared < 75 && lives >= 0 && timeLeft > 0) {
var dx = s ? 1 : 0;
var dy = s ? 0 : 1;
if (blue) blue.each(function(d) { startWall(d, "blue", -dx, -dy); });
if (red) red.each(function(d) { startWall(d, "red", dx, dy); });
}
});
var areaCleared = 0;
var totalArea = svg.selectAll(".air").size();
var percentageCleared = 0;
svg.append("text")
.attr("x", w / 2 - 50)
.attr("y", h - 5)
.attr("class", "smallText")
.attr("id", "areaFilledText")
.text("Area cleared: 0%");
var lives = n;
svg.append("text")
.attr("x", 50)
.attr("y", 15)
.attr("class", "smallText")
.attr("id", "livesText")
.text("Lives: " + lives);
var gameStartedAt = new Date().getTime();
var timeLeft = t;
svg.append("text")
.attr("x", w - 130)
.attr("y", 15)
.attr("class", "smallText")
.attr("id", "timeLeftText")
.text("Time left: " + timeLeft);
var force = d3.layout.force()
.gravity(0)
.charge(0)
.friction(1)
.size([w, h]);
balls.forEach(function (b) {
svg.append("svg:circle")
.data([b])
.attr("class", "ball")
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("r", r);
force.nodes().push(b);
});
force.on("tick", function () {
var ball = svg.selectAll(".ball");
ball.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.each(function(b) {
detectCollisions(b);
var cc = ballCell(b);
var tc = topCell(cc);
var lc = leftCell(cc);
var bc = bottomCell(cc);
var rc = rightCell(cc);
if (cc.isWall || (tc.isWall && bc.isWall) || (lc.isWall && rc.isWall)) {
cc.elnt
.classed("air", true)
.classed("wall", false);
b.px = b.x = cc.x;
b.py = b.y = cc.y;
cc.isWall = b.isMoving = false;
}
});
var head = svg.selectAll(".head");
head.attr("x", function(d) { d.x += d.dx * (v * .4); return rectx(d); })
.attr("y", function(d) { d.y += d.dy * (v * .4); return recty(d); })
.each(function(d) {
svg.select("." + d.cl + ".tail")
.attr("x", tailx)
.attr("y", taily)
.attr("width", tailw)
.attr("height", tailh);
});
var air = svg.selectAll(".air");
var tail = svg.selectAll(".tail");
var wallWasBuilt = false;
head.filter(function(h) {
var hc = ballCell(h);
if (h.dy < 0) {
var tc = topCell(hc);
return tc.isWall && h.y - tc.y < sz;
}
if (h.dx < 0) {
var lc = leftCell(hc);
return lc.isWall && h.x - lc.x < sz;
}
if (h.dy > 0) {
var bc = bottomCell(hc);
return bc.isWall && bc.y - h.y < sz;
}
if (h.dx > 0) {
var rc = rightCell(hc);
return rc.isWall && rc.x - h.x < sz;
}
}).each(function(h) {
air.filter(function(a) {
return h.dx == 0 ? h.x == a.x && Math.min(h.sy, h.y) <= a.y && a.y <= Math.max(h.sy, h.y) : h.y == a.y && Math.min(h.sx, h.x) <= a.x && a.x <= Math.max(h.sx, h.x);
})
.classed("newWall", true)
.classed("air", false)
.each(function(d) { if (!d.isWall) { ++areaCleared; d.isWall = true; } });
tail.filter("." + h.cl)
.remove();
wallWasBuilt = true;
}).remove();
if (wallWasBuilt) {
fillEmptyRooms();
}
var timePlayed = Math.floor(((new Date().getTime()) - gameStartedAt) / 100);
timeLeft = timePlayed > t ? 0 : t - timePlayed;
svg.select("#timeLeftText")
.text("Time left: " + timeLeft);
if (percentageCleared < 75 && lives >= 0 && timeLeft > 0) {
force.resume();
} else {
force.stop();
var text, textWidth;
if (percentageCleared >= 75 && lives >= 0 && timeLeft > 0) {
text = "Level complete !";
textWidth = 188;
d3.select("#nextLevelButton")
.style("visibility", "visible");
} else {
text = "Game over !";
textWidth = 138;
d3.select("#playAgainButton")
.style("visibility", "visible");
}
svg.append("text")
.attr("x", w / 2 - textWidth / 2)
.attr("y", h / 2)
.attr("class", "bigTextStroke")
.text(text);
svg.append("text")
.attr("x", w / 2 - textWidth / 2)
.attr("y", h / 2)
.attr("class", "bigText")
.text(text);
}
});
force.start();
function detectCollisions(b) {
var dx = b.x - b.px > 0 ? 1 : -1;
var dy = b.y - b.py > 0 ? 1 : -1;
var d, sd;
// tail collision
svg.selectAll(".tail").filter(function(t) {
var w = tailw(t);
var h = tailh(t);
var x0 = tailx(t);
var x1 = x0 + w;
var y0 = taily(t);
var y1 = y0 + h;
return x0 - r < b.x && b.x < x1 - r && y0 - r < b.y && b.y < y1 + r;
})
.each(function(t) {
--lives;
svg.select("#livesText")
.text("Lives: " + (lives < 0 ? 0 : lives));
svg.selectAll(".head." + t.cl).remove();
})
.remove();
// wall borders collision
var cc = ballCell(b);
var tc = topCell(cc);
if (tc.isWall && dy < 0 && (d = b.y - tc.y) <= sz) {
bounceY(b, sz, d, dy);
}
var lc = leftCell(cc);
if (lc.isWall && dx < 0 && (d = b.x - lc.x) <= sz) {
bounceX(b, sz, d, dx);
}
var bc = bottomCell(cc);
if (bc.isWall && dy > 0 && (d = bc.y - b.y) <= sz) {
bounceY(b, sz, d, dy);
}
var rc = rightCell(cc);
if (rc.isWall && dx > 0 && (d = rc.x - b.x) <= sz) {
bounceX(b, sz, d, dx);
}
svg.selectAll(".head").each(function(h) {
if (h.y - r <= b.y && b.y <= h.y + r) {
if (dx < 0 && (d = b.x - h.x) <= sz && d > 0) {
bounceX(b, sz, d, dx);
}
if (dx > 0 && (d = h.x - b.x) <= sz && d > 0) {
bounceX(b, sz, d, dx);
}
}
if (h.x - r <= b.x && b.x <= h.x + r) {
if (dy < 0 && (d = b.y - h.y) <= sz && d > 0) {
bounceY(b, sz, d, dy);
}
if (dy > 0 && (d = h.y - b.y) <= sz && d > 0) {
bounceY(b, sz, d, dy);
}
}
});
// wall corners collision
var tlc = topLeftCell(cc);
if (!tc.isWall && !lc.isWall && tlc.isWall && (sd = cornerSquareDistance(b.x, b.y, tlc.x + r, tlc.y + r)) <= sr) {
d = Math.sqrt(sd);
if (dx < 0) {
bounceX(b, r, d, dx);
}
if (dy < 0) {
bounceY(b, r, d, dy);
}
}
var blc = bottomLeftCell(cc);
if (!bc.isWall && !lc.isWall && blc.isWall && (sd = cornerSquareDistance(b.x, b.y, blc.x + r, blc.y - r)) <= sr) {
d = Math.sqrt(sd);
if (dx < 0) {
bounceX(b, r, d, dx);
}
if (dy > 0) {
bounceY(b, r, d, dy);
}
}
var brc = bottomRightCell(cc);
if (!bc.isWall && !rc.isWall && brc.isWall && (sd = cornerSquareDistance(b.x, b.y, brc.x - r, brc.y - r)) <= sr) {
d = Math.sqrt(sd);
if (dx > 0) {
bounceX(b, r, d, dx);
}
if (dy > 0) {
bounceY(b, r, d, dy);
}
}
var trc = topRightCell(cc);
if (!tc.isWall && !rc.isWall && trc.isWall && (sd = cornerSquareDistance(b.x, b.y, trc.x - r, trc.y + r)) <= sr) {
d = Math.sqrt(sd);
if (dx > 0) {
bounceX(b, r, d, dx);
}
if (dy < 0) {
bounceY(b, r, d, dy);
}
}
svg.selectAll(".head").each(function(h) {
if ((sd = cornerSquareDistance(b.x, b.y, h.x + r, h.y + r)) <= sr && sd > 0) {
d = Math.sqrt(sd);
if (dx < 0) {
bounceX(b, r, d, dx);
}
if (dy < 0) {
bounceY(b, r, d, dy);
}
}
if ((sd = cornerSquareDistance(b.x, b.y, h.x + r, h.y - r)) <= sr && sd > 0) {
d = Math.sqrt(sd);
if (dx < 0) {
bounceX(b, r, d, dx);
}
if (dy > 0) {
bounceY(b, r, d, dy);
}
}
if ((sd = cornerSquareDistance(b.x, b.y, h.x - r, h.y - r)) <= sr && sd > 0) {
d = Math.sqrt(sd);
if (dx > 0) {
bounceX(b, r, d, dx);
}
if (dy > 0) {
bounceY(b, r, d, dy);
}
}
if ((sd = cornerSquareDistance(b.x, b.y, h.x - r, h.y + r)) <= sr && sd > 0) {
d = Math.sqrt(sd);
if (dx > 0) {
bounceX(b, r, d, dx);
}
if (dy < 0) {
bounceY(b, r, d, dy);
}
}
});
// ball collision
svg.selectAll(".ball").each(function(b2) {
if (b.id == b2.id) {
return;
}
var sd = cornerSquareDistance(b.x, b.y, b2.x, b2.y);
if (sd <= ssz) {
var dx2 = b2.x - b2.px > 0 ? 1 : -1;
var dy2 = b2.y - b2.py > 0 ? 1 : -1;
var d = Math.sqrt(sd);
if (b.isMoving && (b2.x - b.x) * dx > r / 2) {
bounceX(b, sz, d, dx);
}
if (b2.isMoving && (b.x - b2.x) * dx2 > r / 2) {
bounceX(b2, sz, d, dx2);
}
if (b.isMoving && (b2.y - b.y) * dy > r / 2) {
bounceY(b, sz, d, dy);
}
if (b2.isMoving && (b.y - b2.y) * dy2 > r / 2) {
bounceY(b2, sz, d, dy2);
}
}
});
}
function bounceX(b, m, d, dx) {
if (b.isMoving) {
b.x -= (m - d) * dx;
b.px = b.x + dx * v;
}
}
function bounceY(b, m, d, dy) {
if (b.isMoving) {
b.y -= (m - d) * dy;
b.py = b.y + dy * v;
}
}
function cornerSquareDistance(x0, y0, x1, y1) {
var w = x1 - x0;
var h = y1 - y0;
return (w * w + h * h);
}
function fillEmptyRooms() {
var air = d3.selectAll(".air");
air
.each(function (d) {
d.visited = false;
});
svg.selectAll(".ball")
.each(function (b) {
var cc = ballCell(b);
cc.visited = true;
visit(cc);
});
air
.classed("newWall", function(d) { return !d.visited; })
.classed("air", function(d) { return d.visited; })
.each(function(d) { if (!d.visited && !d.isWall) { ++areaCleared; d.isWall = true; } });
percentageCleared = Math.floor((areaCleared * 100) / totalArea);
svg.select("#areaFilledText")
.text("Area cleared: " + percentageCleared + "%");
svg.selectAll(".newWall")
.on("mouseover", null)
.on("mouseout", null)
.on("click", null)
.classed("wall", true)
.classed("blue", false)
.classed("red", false);
}
function visit(c) {
var tc = topCell(c);
if (!tc.isWall && !tc.visited) {
tc.visited = true;
visit(tc);
}
var lc = leftCell(c);
if (!lc.isWall && !lc.visited) {
lc.visited = true;
visit(lc);
}
var bc = bottomCell(c);
if (!bc.isWall && !bc.visited) {
bc.visited = true;
visit(bc);
}
var rc = rightCell(c);
if (!rc.isWall && !rc.visited) {
rc.visited = true;
visit(rc);
}
}
function startWall(cell, cl, dx, dy) {
var wallHead = {
sx: cell.x,
sy: cell.y,
x: cell.x,
y: cell.y,
dx: dx,
dy: dy,
cl: cl
};
if (svg.selectAll("." + cl + ".head").empty()) {
svg.selectAll("." + cl + ".head")
.data([wallHead]).enter().append("rect")
.classed("builder", true)
.classed(cl, true)
.classed("head", true)
.attr("x", rectx)
.attr("y", recty)
.attr("width", sz)
.attr("height", sz);
var tail = svg.selectAll("." + cl + ".tail")
.data([wallHead]);
tail.enter().append("rect")
.classed("builder", true)
.classed(cl, true)
.classed("tail", true)
.attr("x", tailx)
.attr("y", taily)
.attr("width", tailw)
.attr("height", tailh);
tail.exit().remove();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment