Skip to content

Instantly share code, notes, and snippets.

@denjn5
Last active September 23, 2017 05:44
Show Gist options
  • Save denjn5/878d1ef107a3962db18a01720a1b551f to your computer and use it in GitHub Desktop.
Save denjn5/878d1ef107a3962db18a01720a1b551f to your computer and use it in GitHub Desktop.
Memory Game
license: gpl-3.0
height: 410
border: no

Memorizinator Game

A little memory game to help learn big ideas.

  1. Game timer starts automatically (1.5 seconds / word).
  2. Click the words in the proper order.
  3. Game provides feedback about Win, Loss, Player Error.
  4. Need a hint? (All are toggles)
    • Show It. Show the full quote.
    • Say It. Audio of the quote. [Works in Chrome/Safari.]
    • Bold It. The next correct word will be bolded.
    • Punctiate It. Turns on capitalization and punctuation.
  5. Mistake? Incorrect answers are red. Click a circle to undo.
  6. Select a new quote (top-right) or Restart game (bottom-right).

D3 Force Simulation Notes

  • This game leverages the D3 Force Simulation (version 4) mechanics. It incorporates:
    • Multi-foci
    • Adds nodes
    • Adds links and removes links on the fly
    • Event handling
    • Text centered in nodes (overcomes Firefox vertical-align problem)
function colorPalette(id) {
var palettes = [["#3dc8ff", "#ff3d87", "#ff703d", "#ffc83d", "#e1ff3d"],
["#493548", "#4b4e6d", "#6a8d92", "#80b192", "#a1e887"],
["#335c67", "#fff3b0", "#e09f3e", "#9e2a2b", "#540b0e"],
["#4d9de0", "#e15554", "#e1bc29", "#3bb273", "#7768ae"],
["#202030", "#39304a", "#635c51", "#7d7461", "#b0a990"],
["#f1e0c5", "#c9b79c", "#71816d", "#342a21", "#da667b"],
["#7b7554", "#17183b", "#a11692", "#ff4f79", "#ffb49a"]];
return palettes[(id % palettes.length)];
}
<!DOCTYPE html>
<meta charset="utf-8">
<title>Memorizinator</title>
<link rel="stylesheet" href="skinny_skel.css" type="text/css" media="screen" />
<style>
#container {
position: relative;
display: inline-block;
}
#gameboard {
border: 1px solid #840000;
position: relative;
width: 100%;
height: 100%;
}
.gnode > circle:hover {
stroke: #e0e0e2;
stroke-width: 2px;
}
.text { /* centers text in node */
text-anchor: middle;
alignment-baseline: central;
pointer-events: none;
transform: translateY(1%); /* Firefox */
}
.next { /* for boldIt hint */
font-weight: bold;
}
.link {
stroke: lightgray;
stroke-width: 1px;
}
#selectQuote {
position: absolute;
top: 3px;
right: 2px;
}
#message {
position: absolute;
bottom: 2px;
left: 3px;
}
#restart {
bottom: 2px;
right: 4px;
}
#settings {
bottom: 2px;
right: 28px;
}
#setsPop {
left: 15%;
top: 20%;
width: 66%;
height: 40%;
}
#timerBar {
fill: #840000;
opacity: 0.7;
}
#timerBarBox {
fill: #e0e0e2;
}
</style>
<body>
<div id="container">
<svg id="gameboard">
<rect width="0" height="10" x="3" y="3" id="timerBarBox"></rect>
<rect width="0" height="10" x="3" y="3" id="timerBar"></rect>
</svg>
<select id="selectQuote"></select>
<div id="message"><b>Memorizinator</b> (A little game to help you learn big ideas.)</div>
<svg id="settings" class="iconButton" viewBox="0 0 20 20" xml:space="preserve"><path d="M5,1.6C5,1.047,4.552,1,4,1C3.447,1,3,1.047,3,1.6V10h2V1.6z M3,18.4C3,18.951,3.447,19,4,19c0.552,0,1-0.049,1-0.6V15H3 V18.4z M6.399,11H1.599C1.046,11,1,11.448,1,12v1c0,0.553,0.046,1,0.599,1h4.801C6.95,14,7,13.553,7,13v-1 C7,11.448,6.95,11,6.399,11z M18.399,12h-4.801C13.046,12,13,12.448,13,13v1c0,0.553,0.046,1,0.599,1h4.801 C18.95,15,19,14.553,19,14v-1C19,12.448,18.95,12,18.399,12z M13,7c0-0.552-0.05-1-0.601-1H7.599C7.046,6,7,6.448,7,7v1 c0,0.553,0.046,1,0.599,1h4.801C12.95,9,13,8.553,13,8V7z M11,1.6C11,1.047,10.552,1,10,1C9.447,1,9,1.047,9,1.6V5h2V1.6z M9,18.4 c0,0.551,0.447,0.6,1,0.6c0.552,0,1-0.049,1-0.6V10H9V18.4z M17,1.6C17,1.047,16.552,1,16,1c-0.553,0-1,0.047-1,0.6V11h2V1.6z M15,18.4c0,0.551,0.447,0.6,1,0.6c0.552,0,1-0.049,1-0.6V16h-2V18.4z"><title>Show Settings</title></path></svg>
<svg id="restart" class="iconButton" viewBox="0 0 20 20" xml:space="preserve"><path d="M5.516,14.224c-2.262-2.432-2.222-6.244,0.128-8.611c0.962-0.969,2.164-1.547,3.414-1.736L8.989,1.8 C7.234,2.013,5.537,2.796,4.192,4.151c-3.149,3.17-3.187,8.289-0.123,11.531l-1.741,1.752l5.51,0.301l-0.015-5.834L5.516,14.224z M12.163,2.265l0.015,5.834l2.307-2.322c2.262,2.434,2.222,6.246-0.128,8.611c-0.961,0.969-2.164,1.547-3.414,1.736l0.069,2.076 c1.755-0.213,3.452-0.996,4.798-2.35c3.148-3.172,3.186-8.291,0.122-11.531l1.741-1.754L12.163,2.265z"><title>Restart game</title></path></svg>
<div id="setsPop" class="popup" tabindex="-1">
<div class="row">
<div class="eleven columns"><h5>Settings</h5></div>
<div class="one columns"><svg class="close iconButton" viewBox="0 0 20 20" xml:space="preserve"><path d="M14.348,14.849c-0.469,0.469-1.229,0.469-1.697,0L10,11.819l-2.651,3.029c-0.469,0.469-1.229,0.469-1.697,0 c-0.469-0.469-0.469-1.229,0-1.697l2.758-3.15L5.651,6.849c-0.469-0.469-0.469-1.228,0-1.697c0.469-0.469,1.228-0.469,1.697,0 L10,8.183l2.651-3.031c0.469-0.469,1.228-0.469,1.697,0c0.469,0.469,0.469,1.229,0,1.697l-2.758,3.152l2.758,3.15 C14.817,13.62,14.817,14.38,14.348,14.849z"><title>Close popup</title></path></svg></div>
</div>
<hr />
<div class="row">
<div class="button three columns" id="sayIt" title="Reads the quote to you. Click again to silence.">Say It</div>
<div class="button three columns" id="showIt" title="Show the quote. Click again to hide.">Show It</div>
<div class="button three columns" id="boldIt" title="Bold the next word you need to click. Click again to turn off.">Bold It</div>
<div class="button three columns" id="punctuateIt" title="Turn on punctuation and capitalization. Click again to turn off.">Punctuate It</div>
</div>
<div class="row">
<div id="setsMessage" class="twelve columns"></div>
</div>
</div>
</div>
</body>
<script src="//d3js.org/d3.v4.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="colors.js"></script>
<script>
$(function () {
// ********** EVENTS **********
$("#selectQuote").change(function () {
citation = $("#selectQuote").val();
gameManager('restart');
});
$("#settings").click(function () { $('#setsPop').show(); $('#setsPop').focus(); }); // focus allows close
$("#restart").click(function () { gameManager('restart'); });
$("#sayIt").click(function () { gameManager('sayIt'); });
$("#showIt").click(function () { gameManager('showIt'); });
$("#boldIt").click(function () { gameManager('boldIt'); });
$("#punctuateIt").click(function () { gameManager('punctuateIt'); });
$(".close").click(function () { $('.popup:visible').hide(); });
$(".popup").on('blur', function () { $('.popup:visible').hide(); }); // close when clicking outside popup
// ********** D3 HAPPENS HERE **********
// General variablees
var wordData = [], // All words (data) from text from json
wordLinks = [], // Links (data) in the simulation
svgWidth = 700, // Width of the svg palette
svgHeight = 400, // Height of the svg palette
initCount = 7, // Number of words to show at beginning of game
foci = [{ x: svgWidth * 0.25, y: svgHeight * 0.45 }, { x: svgWidth * 0.75, y: svgHeight * 0.45 }], // Sets 2 foci on page
citation = "Williams Shedd",
quote, // The quote to be memorized
boldIt = false, // Is the boltIt hint be active?
punctuateIt = false; // Is the punctuateIt on?
// Get svg handle, set up color and radius scale (use word length to set size)
$("#container").css("width", svgWidth).css("height", svgHeight);
var svg = d3.select("#gameboard");
var radScale = d3.scaleLinear().domain([1, 15]).range([10, 50]);
// Set forces in the simulation
var simulation = d3.forceSimulation()
//.force("link", d3.forceLink().id(function (d) { return d.id; }))
.force("link", d3.forceLink().distance(20).strength(0.6))
.force("charge", d3.forceManyBody().strength(-50))
.force("collide", d3.forceCollide(function (d) { return d.rad; }));
// Get quote from json, then start simulation
function build() {
d3.json("quotes.json", function (error, quotesData) {
// Populate dropdown if it's empty
if ($('#selectQuote').children('option').length == 0) {
$.each(quotesData.nodes, function (key, value) {
option = "<option value='" + value.cit + "' " + (value.cit == citation ? "selected" : "") + ">" + value.cit + "</option>";
$('#selectQuote').append(option);
});
}
// Get the selected quote (filter returns an array, length = 1); then split.
quote = quotesData.nodes.filter(function (obj) { return (obj.cit == citation ? true : false); })[0].text;
quote = punctuateIt ? quote : quote.toLowerCase().replace(/([^a-z ])/g, '');
var quoteSplit = quote.replace("-", "- ").split(" ");
var colors = d3.scaleOrdinal().domain(0, 4).range(colorPalette(quote.length)); // function in colors.js
// Format data
for (i = 0; i < quoteSplit.length; i++) {
wordData.push({
word: quoteSplit[i], // the actual word
id: i, // the original index
x: 0,
y: ~~(Math.random() * svgHeight),
rad: radScale(quoteSplit[i].length), // the radius
clickOrder: 0, // what order was it clicked on by user?
focus: (i < initCount ? 0 : -1), // is is part of the answer set; controls focii
color: colors(i)
})
}
gameManager("start");
start();
});
}
build();
// Build the force simulation
function start() {
// Create links
var glinks = svg.selectAll(".link")
.data(wordLinks, function (d) { return d.source.id + "-" + d.target.id; })
.enter().insert("line", ".gnode").attr("class", "link");
var glinks = svg.selectAll(".link")
.data(wordLinks, function (d) { return d.source.id + "-" + d.target.id; })
.exit().remove()
// Create g-elements (for nodes) based on a subset of wordData
var gnodes = svg.selectAll("g")
.data(words("showing", -1), function id(d, i) { return d.id; })
.enter().append("g").attr("class", function (d) { return "gnode g" + d.id; })
.classed("next", function (d) { return (boldIt && d.id == 0) ? true : false ; });
var circles = gnodes.append("circle").attr("class", function (d) { return "c" + d.id; })
.attr("r", function (d) { return d.rad; }).style("fill", function (d, i) { return d.color; })
.attr("opacity", 0.7);
var texts = gnodes.append("text")
.attr("class", function (d) { return "text w" + d.id; }) // Tie into CSS
.text(function (d) { return (d.word); });
var actions = gnodes.on("click", nodeClicked);
// Link data to simulation and set it in motion.
simulation.nodes(words("showing", -1)).force("link").links(wordLinks);
simulation.on("tick", ticked).alpha(0.5).restart();
}
// Manage node & link movement
function ticked(e) {
var k = .2 * simulation.alpha();
svg.selectAll(".gnode").attr("transform", function (d) {
// Set node location, multi-foci
d.y += (foci[d.focus].y - d.y) * k;
d.x += (foci[d.focus].x - d.x) * k;
// But be sure that nodes don't go out-of-bounds
d.y = Math.max(d.rad, Math.min(svgHeight - d.rad, d.y));
d.x = Math.max(d.rad, Math.min(svgWidth - d.rad, d.x));
return 'translate(' + [d.x, d.y] + ')';
});
// Set link locations
svg.selectAll(".link")
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
}
// User clicked a node
function nodeClicked(d) {
if (d.focus == 0) { // A node from the unused side
d.focus = 1; // Move gnode to other focii.
var answerCount = words("focus", 1).length; // answerCount includes just-clicked node; equals clickOrder
var showCount = words("showing", -1).length; //
d.clickOrder = answerCount; // Set clickOrder
if (wordData.length > showCount && words("focus", 0).length < initCount) { wordData[showCount].focus = 0; } // Show another node
if (answerCount > 1) { wordLinks.push({ "source": words("clickOrder", d.clickOrder - 1)[0].id, "target": d.id }); } // Add link
start();
svg.selectAll(".text").classed("next", function (d) { return (boldIt && d.index == answerCount) ? true : false; }); // Bold next word
// Check accuracy
var accurate = true;
for (i = 0; i < answerCount; i++) { // cycle thru wordData by clickOrder (base 1); compare with wordData (correct answers)
if (words("clickOrder", i + 1)[0].word != wordData[i].word) {
accurate = false;
$('.c' + words("clickOrder", i + 1)[0].id).css("fill", "red");
}
}
if (accurate && timerLength >= 0 && wordData.length == answerCount) { gameManager("win"); }
} else { // d.focus == 1 (a node from the focus)
d.focus = 0;
$('.c' + d.id).css("fill", d.color); // reset color
d.clickOrder = 0;
removeLinks(d.id)
start();
}
}
// Return portion of wordData needed.
function words(key, val) {
switch (key) {
case "showing": // returns an array of the shown [focus > -1]
return wordData.filter(function (value, index) { return value.focus > -1 ? true : false; });
break;
case "focus": // returns an array of the focus nodes [val = 1] or un-focus [val = 0]
return wordData.filter(function (value, index) { return value.focus == val ? true : false; });
break;
case "clickOrder": // returns an array with the single requested clickOrder item
return wordData.filter(function (value, index) { return value.clickOrder == val ? true : false; });
break;
}
}
// Remove links that attach to specific node id's
function removeLinks(id) {
for (var i = wordLinks.length - 1; i >= 0; i--) { // reverse order since splicing changes indexes
if (id == wordLinks[i].source.id || id == wordLinks[i].target.id) {
wordLinks.splice(i, 1);
}
}
}
// ********** GAME MECHANICS / MANAGEMENT **********
var timer, // Allows timer to be managed from multiple places
timerLength = 20; // Allows clean management of timer length
function gameManager(event) {
switch (event) {
case "timerBar":
$("#timerBar").attr("width", timerLength > 0 ? timerLength * 2 : 0).attr("opacity", (timerLength > 10 ? 0.5 : 1));
break;
case "start":
timerLength = ~~(wordData.length * 1.5);
clearInterval(timer);
timer = setInterval(gameTimer, 1000);
$("#timerBar").attr("width", timerLength * 2);
$("#timerBarBox").attr("width", timerLength * 2);
$('#message').html("<b>Memorizinator</b>. Click the words in the correct order.");
break;
case "win":
$('#message').html("<b>You won!</b> Score = " + timerLength);
$("#timerBar").attr("width", 0);
clearInterval(timer);
break;
case "loss":
$('#message').text("I'm sorry for your loss.");
clearInterval(timer);
break;
case "restart":
wordData = []; // All words (data) from quote from json
wordLinks = []; // Links (data) in the simulation
svg.selectAll("g, .link").remove();
build();
break;
case "sayIt":
$('#setsMessage').text("'Say It' only works in Chrome/Safari. Click again to silence.");
if (window.speechSynthesis.speaking) {
window.speechSynthesis.cancel();
} else {
window.speechSynthesis.speak(new SpeechSynthesisUtterance(quote));
}
break;
case "boldIt":
boldIt = !boldIt;
$('#setsMessage').text("Bolding: " + (boldIt ? "On" : "Off"));
gameManager("restart");
break;
case "punctuateIt":
punctuateIt = !punctuateIt;
$('#setsMessage').text("Punctuation: " + (punctuateIt ? "On" : "Off"));
gameManager("restart");
break;
case "showIt":
$('#setsMessage').text("Click again to hide.");
$('#message').text($('#message').text() == quote ? "" : quote);
break; // t.replace(/(\B[a-z])/g, "-")
}
}
function gameTimer() {
if (--timerLength >= 0) {
gameManager("timerBar");
} else {
gameManager("loss")
clearInterval(timer);
}
}
});
</script>
{
"nodes": [
{ "cit": "Williams Shedd", "text": "A ship in harbour is safe, but that is not what ships are built for." },
{ "cit": "John 3:16", "text": "For God so loved the world that he gave his one and only Son, that whoever believes in him shall not perish but have eternal life." },
{ "cit": "2 Timothy 4:7", "text": "I have fought the good fight, I have finished the race, I have kept the faith." },
{ "cit": "Acts 2:38", "text": "Peter replied, Repent and be baptized, every one of you, in the name of Jesus Christ for the forgiveness of your sins. And you will receive the gift of the Holy Spirit."},
{ "cit": "Walt Disney", "text": "When you're curious, you find lots of interesting things to do."},
{ "cit": "Gettysburg Address", "text": "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal." },
{ "cit": "Constitution Preamble", "text": "We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defense, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America." }
],
"links": [
]
}
@import url('https://fonts.googleapis.com/css?family=Raleway');
/* Based on Skeleton V2.0.4 Dave Gamache (www.getskeleton.com) */
/* Grid */
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.columns {
margin-left: 4%; }
.columns:first-child {
margin-left: 0; }
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.columns { width: 30.6666666667%; }
.two-thirds.columns { width: 65.3333333333%; }
.one-half.columns { width: 48%; }
/* Offsets */
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles */
/* NOTE: html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.4em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #840000; }
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 1rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.2; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.2; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.2; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Buttons */
.button {
display: inline-block;
height: 22px;
padding: 0 10px;
color: #840000;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 20px;
letter-spacing: .1rem;
text-decoration: none;
white-space: nowrap;
border-radius: 4px;
border: 1px solid #c8bfc7;
cursor: pointer;
box-sizing: border-box; }
.button:hover {
border-color: #840000;
outline: 0;
}
.iconButton {
position: absolute;
fill: #c8bfc7;
width: 20px;
height: 20px;
}
.iconButton:hover {
fill: #840000;
}
.popup {
display: none;
position: absolute;
line-height: 200%;
padding: 2%;
border: 1px solid #840000;
background-color: white;
z-index: 1002;
overflow: auto;
outline: none;
box-shadow: -10px 0 0 100em rgba(209, 200, 208, .6);
}
/* Forms */
select {
color: #840000;
height: 22px;
padding: 1px 5px; /* The 6px vertically centers text on FF, ignored by Webkit */
border: 1px solid #c8bfc7;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
select:hover {
border-color: #840000;
}
/* Spacing */
button, .button {
margin-bottom: 1rem; }
input, textarea, select, fieldset {
margin-bottom: 1.5rem; }
pre, blockquote, dl, figure, table, p, ul, ol, form {
margin-bottom: 2.5rem; }
/* Misc */
hr {
margin-top: 1rem;
margin-bottom: 1.5rem;
border-width: 0;
border-top: 1px solid #c8bfc7; }
/* Clearing */
/* Self Clearing Goodness */
.container:after, .row:after, .u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries */
/* Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there. */
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment