12
\$\begingroup\$

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>

\$\endgroup\$
  • \$\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 Dec 6 '14 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 Dec 6 '14 at 11:15
  • \$\begingroup\$ Simon says: ask this question on codereview \$\endgroup\$ – Jack Dec 7 '14 at 7:36
  • \$\begingroup\$ Rest in peace, Mr. Ralph H. Baer \$\endgroup\$ – Edward Dec 8 '14 at 15:12
6
\$\begingroup\$

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".

\$\endgroup\$
  • \$\begingroup\$ That's a good hint, thanks. Is this link a good place to start to teach myself how to create modules in Javascript? \$\endgroup\$ – Edward Dec 6 '14 at 18:01
  • \$\begingroup\$ Yep, that covers the gist of it, and certainly enough for the purposes of this game. \$\endgroup\$ – Kyle Hale Dec 6 '14 at 18:26
2
\$\begingroup\$

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>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.