Here's a little game written in HTML5 using bleeding edge audio. Since I don't do much HTML5 or Javascript, I'm particularly interested in:
- Structure Is this a reasonable way to structure the Javascript? Should I encapsulate everything in an object?
- Compatibility I believe the older versions of Internet Explorer won't work with this. Are there other issues with modern browsers?
The in-game instructions are terse, but the keys j, k, d and f correspond to upper left, upper right, lower left and lower right quadrants respectively. Alternatively, you can use the mouse. (削除) It may also work with a touchscreen -- I'd be interested in learning if it does. (削除ここまで) It works with at least some touchscreens (Windows 8 and iPhone), but I'd be interested in learning of touchscreens where it does not seem to work.
var litcolors = ['red', 'yellow', 'lightblue', 'lightgreen'];
var colors = ['darkred', 'darkorange', 'darkblue', 'darkgreen'];
var ctx, innerRadius, outerRadius;
var states = [ false, false, false, false ];
var keys = [ 74, 75, 70, 68 ];
var notes = [ 261.626, 329.628, 391.995, 523.251 ];
var sequence = [];
var seqitem = 0;
var c;
var turn = 0;
var timeout;
function instructions() {
alert("After the computer shows a sequence,\ntry to reproduce it.\nYou can use the keys kldf or the mouse");
}
function init() {
c = document.getElementById('simon');
ctx = c.getContext("2d");
paintToy(-1);
instructions();
c.addEventListener('mousedown', function(evt) {
var mousePos = getMousePos(c, evt);
var quadrant = getQuadrant(mousePos);
if (turn == 1) {
tryMove(quadrant);
}
}, false);
c.addEventListener('mouseup', function(evt) {
paintToy(-1);
}, false);
document.addEventListener('keyup', function(evt) {
paintToy(-1);
}, false);
document.addEventListener('keydown', function(evt) {
var quadrant = getQuadrantKey(evt.keyCode);
if (turn == 1) {
tryMove(quadrant);
}
}, false);
computerTurn();
}
// Temporary workaround until AudioContext is standardized
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
var osc = audioCtx.createOscillator();
osc.type = 'triangle';
osc.start(0);
var score = 0;
function beep(frequency, duration, after) {
if (typeof after != "function") {
after = function () {};
}
osc.frequency.value = +frequency;
osc.connect(audioCtx.destination);
setTimeout(function () {
osc.disconnect();
after();
}, +duration);
}
function paintToy(litquadrant) {
writeMessage(c,10,25, ""+score);
writeMessage(c,400,490, "Simon");
outerRadius = c.width * 0.45;
innerRadius = c.width * 0.05;
ctx.translate(c.width / 2, c.height / 2);
var i;
for (i = 0; i < 4; i++) {
states[i] = (litquadrant == i);
slice(i);
}
ctx.translate(-c.width / 2, -c.height / 2);
}
function slice(quadrant) {
ctx.rotate(quadrant * Math.PI/2);
ctx.beginPath();
ctx.arc(-5,-5,innerRadius,Math.PI, 3*Math.PI/2 );
ctx.lineTo(-5,-outerRadius-5);
ctx.arc(-5,-5,outerRadius, 3*Math.PI/2,Math.PI, true);
ctx.closePath();
if (states[quadrant]) {
ctx.fillStyle = litcolors[quadrant];
} else {
ctx.fillStyle = colors[quadrant];
}
ctx.fill();
ctx.strokeStyle = 'black';
ctx.stroke();
ctx.rotate(-quadrant * Math.PI/2);
}
function getQuadrant(mousePos) {
var quadrant = 0;
if (mousePos.y > c.width/2) {
quadrant = 3;
}
if (mousePos.x > c.width/2) {
if (quadrant == 3) {
quadrant = 2;
} else {
quadrant++;
}
}
return quadrant;
}
function writeMessage(canvas, x, y, message) {
var context = canvas.getContext('2d');
context.clearRect(x, y-30, canvas.width, 200);
context.font = '18pt Calibri';
context.fillStyle = 'black';
context.fillText(message, x, y);
}
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: Math.round((evt.clientX-rect.left)/(rect.right-rect.left)*canvas.width),
y: Math.round((evt.clientY-rect.top)/(rect.bottom-rect.top)*canvas.height)
};
}
function getQuadrantKey(keycode) {
var i;
for (i = 0; i < 4; i++) {
if (keys[i] == keycode) {
return i;
}
}
return -1;
}
function sound(quadrant) {
if ((quadrant < 0) || (quadrant > 3))
return;
beep(notes[quadrant],200, false);
}
function tryMove(quadrant) {
paintToy(quadrant);
if (sequence[seqitem++] == quadrant) {
sound(quadrant);
clearTimeout(timeout);
if (seqitem == sequence.length) {
score++;
turn = 0;
setTimeout(computerTurn, 1000);
} else {
timeout = setTimeout(lose, 5000);
}
} else {
lose();
}
}
function computerPlay() {
sound(sequence[seqitem]);
paintToy(sequence[seqitem++]);
setTimeout(clear, 200);
if (seqitem < sequence.length) {
setTimeout(computerPlay, 1000);
} else {
turn = 1;
humanTurn();
}
}
function computerTurn() {
sequence.push(Math.floor((Math.random() * 4)));
seqitem = 0;
computerPlay();
}
function clear() {
beep(0, 200, false);
paintToy(-1);
}
function humanTurn() {
seqitem = 0;
timeout = setTimeout(lose, 5000);
}
function lose() {
turn = -1;
clearTimeout(timeout);
beep(155.563, 200, function(){alert("Sorry, you lose!")});
}
function losemsg() {
alert("Sorry, you lose!");
}
init();
<!DOCTYPE html>
<html>
<head>
<title>Simon</title>
</head>
<body>
<canvas id="simon" width="500" height="500"
style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag. How sad.</canvas>
</body>
</html>
-
\$\begingroup\$ I didn't really expect it to, but it doesn't work on mobile chrome. Looking forward to checking it out on my PC. \$\endgroup\$RubberDuck– RubberDuck2014年12月06日 05:14:27 +00:00Commented Dec 6, 2014 at 5:14
-
\$\begingroup\$ @RubberDuck: should be supported as of mobile Chrome 28 via webkit according to this but there may be incompatibilities in the API. The latest draft is only a few days old. \$\endgroup\$Edward– Edward2014年12月06日 11:15:17 +00:00Commented Dec 6, 2014 at 11:15
-
\$\begingroup\$ Simon says: ask this question on codereview \$\endgroup\$Jack– Jack2014年12月07日 07:36:21 +00:00Commented Dec 7, 2014 at 7:36
-
\$\begingroup\$ Rest in peace, Mr. Ralph H. Baer \$\endgroup\$Edward– Edward2014年12月08日 15:12:03 +00:00Commented Dec 8, 2014 at 15:12
2 Answers 2
First, bravo! It's very fun and highly nostalgic. I may or may not have (削除) forced (削除ここまで) encouraged my children to play it well past their bedtime.
As to the structure, in 2014, it is absolutely recommended that you encapsulate your game inside a module. This ensures that it's portable, that it doesn't cause conflicts in the global namespace, and (from a self-discipline perspective) it will benefit from a bit more code organization and thinking about things like DRY, scoping, subclasses, etc.
So some general pseudocode from a more OO-style would be:
SimonGame {
Oscillator {
Type
Frequency
Start()
Disconnect()
Beep()
}
Canvas {
WriteMessage()
Paint()
Quadrants
Quadrant
Color
LitColor
State
}
Guess()
Init(options)
}
That's just a rough start, but the general idea would be to call things like Canvas.Paint() and Oscillator.Beep(), etc. rather than your global functions.
You might also want take a document element or ID in your Init() method arguments so the user can pass in the canvas they want to "simonize".
-
-
\$\begingroup\$ Yep, that covers the gist of it, and certainly enough for the purposes of this game. \$\endgroup\$Kyle Hale– Kyle Hale2014年12月06日 18:26:50 +00:00Commented Dec 6, 2014 at 18:26
Based on @KyleHale's useful answer, I've improved the original code. Still not great but definitely better. I have another idea to implement a new feature, but if I do that it will be in a new question.
var Simon = (function () {
var simon = {};
var Oscillator = (function () {
var mysound = {};
var notes = [ 261.626, 329.628, 391.995, 523.251 ];
// Temporary workaround until AudioContext is standardized
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
var osc = audioCtx.createOscillator();
osc.type = 'triangle';
osc.start(0);
mysound.sound = function (quadrant) {
if ((quadrant < 0) || (quadrant > 3))
return;
beep(notes[quadrant],200, false);
}
mysound.silence = function () {
beep(0, 200, false);
}
mysound.losebeep = function (after) {
beep(155.563, 200, after);
}
var beep = function (frequency, duration, after) {
if (typeof after != "function") {
after = function () {};
}
osc.frequency.value = +frequency;
osc.connect(audioCtx.destination);
setTimeout(function () {
osc.disconnect();
after();
}, +duration);
}
return mysound;
})();
var GameBoard = (function () {
var game = {};
var litcolors = ['red', 'yellow', 'lightblue', 'lightgreen'];
var colors = ['darkred', 'darkorange', 'darkblue', 'darkgreen'];
var innerRadius, outerRadius;
var states = [ false, false, false, false ];
var keys = [ 74, 75, 70, 68 ];
var sequence = [];
var seqitem = 0;
var turn = 0;
var timeout;
var score = 0;
var ctx;
var c;
game.setCanvas = function (canvas) {
c = canvas;
ctx = c.getContext("2d");
return c;
}
game.paintToy = function (litquadrant) {
writeMessage(c,10,25, ""+score);
writeMessage(c,400,490, "Simon");
outerRadius = c.width * 0.45;
innerRadius = c.width * 0.05;
ctx.translate(c.width / 2, c.height / 2);
var i;
for (i = 0; i < 4; i++) {
states[i] = (litquadrant == i);
slice(i);
}
ctx.translate(-c.width / 2, -c.height / 2);
}
var slice = function (quadrant) {
ctx.rotate(quadrant * Math.PI/2);
ctx.beginPath();
ctx.arc(-5,-5,innerRadius,Math.PI, 3*Math.PI/2 );
ctx.lineTo(-5,-outerRadius-5);
ctx.arc(-5,-5,outerRadius, 3*Math.PI/2,Math.PI, true);
ctx.closePath();
if (states[quadrant]) {
ctx.fillStyle = litcolors[quadrant];
} else {
ctx.fillStyle = colors[quadrant];
}
ctx.fill();
ctx.strokeStyle = 'black';
ctx.stroke();
ctx.rotate(-quadrant * Math.PI/2);
}
game.getQuadrant = function (mousePos) {
var quadrant = 0;
if (mousePos.y > c.width/2) {
quadrant = 3;
}
if (mousePos.x > c.width/2) {
if (quadrant == 3) {
quadrant = 2;
} else {
quadrant++;
}
}
return quadrant;
}
var writeMessage = function (canvas, x, y, message) {
var context = canvas.getContext('2d');
context.clearRect(x, y-30, canvas.width, 200);
context.font = '18pt Calibri';
context.fillStyle = 'black';
context.fillText(message, x, y);
}
game.getMousePos = function (canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: Math.round((evt.clientX-rect.left)/(rect.right-rect.left)*canvas.width),
y: Math.round((evt.clientY-rect.top)/(rect.bottom-rect.top)*canvas.height)
};
}
game.getQuadrantKey = function (keycode) {
var i;
for (i = 0; i < 4; i++) {
if (keys[i] == keycode) {
return i;
}
}
return -1;
}
game.tryMove = function (quadrant) {
game.paintToy(quadrant);
if (sequence[seqitem++] == quadrant) {
Oscillator.sound(quadrant);
clearTimeout(timeout);
if (seqitem == sequence.length) {
score++;
turn = 0;
setTimeout(game.computerTurn, 1000);
} else {
timeout = setTimeout(lose, 5000);
}
} else {
lose();
}
}
game.isHumanTurn = function () {
return turn == 1;
}
var computerPlay = function () {
Oscillator.sound(sequence[seqitem]);
game.paintToy(sequence[seqitem++]);
setTimeout(clear, 200);
if (seqitem < sequence.length) {
setTimeout(computerPlay, 1000);
} else {
turn = 1;
humanTurn();
}
}
game.computerTurn = function () {
sequence.push(Math.floor((Math.random() * 4)));
seqitem = 0;
computerPlay();
}
var clear = function () {
Oscillator.silence();
game.paintToy(-1);
}
var humanTurn = function () {
seqitem = 0;
timeout = setTimeout(lose, 5000);
}
var lose = function () {
turn = -1;
clearTimeout(timeout);
Oscillator.losebeep(function(){alert("Sorry, you lose!")});
}
return game;
})();
var instructions = function () {
alert("After the computer shows a sequence,\ntry to reproduce it.\nYou can use the keys dfjk or the mouse");
}
simon.run = function(canvas) {
c = GameBoard.setCanvas(canvas);
GameBoard.paintToy(-1);
instructions();
c.addEventListener('mousedown', function(evt) {
var mousePos = GameBoard.getMousePos(c, evt);
var quadrant = GameBoard.getQuadrant(mousePos);
if (GameBoard.isHumanTurn()) {
GameBoard.tryMove(quadrant);
}
}, false);
c.addEventListener('mouseup', function(evt) {
GameBoard.paintToy(-1);
}, false);
document.addEventListener('keyup', function(evt) {
GameBoard.paintToy(-1);
}, false);
document.addEventListener('keydown', function(evt) {
var quadrant = GameBoard.getQuadrantKey(evt.keyCode);
if (GameBoard.isHumanTurn()) {
GameBoard.tryMove(quadrant);
}
}, false);
GameBoard.computerTurn();
}
return simon;
})();
<!DOCTYPE html>
<html>
<head>
<title>Simon</title>
</head>
<body onload="Simon.run(simon)">
<canvas id="simon" width="500" height="500"
style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag. How sad.</canvas>
</body>
</html>
Explore related questions
See similar questions with these tags.