Mah Jong

I used to play this, when the Internet was not invented. So it’s about time I created my own version. It’s the most complex example of MaxCanvas todate, and I’ll readily admit to not having tidied the code up (or commenting it) too much. The data structures are a bit clunky and hard to follow, but … it works, is playable and I’m happy.

Code

// MaxCanvas Bases Maj-Jong Tile match implementation
// Started March 2020 - whenever 8-)
//
// Required to drag MC.js into Intellisense
// thanks to https://visualstudiomagazine.com/blogs/tool-tracker/2018/07/use-javascript-code.aspx (Dec 2018)
/// <reference path="MC.js" />

// Gamecode is wrapped in .. window.onload = (function () { -- ALL CODE HERE --}); .. so game will start AFTER everything else is loaded

window.onload = (function() {

    // AQUIRE HTML DOM CANVAS REFERENCE
    //     HTML has to have a HTML5 canvas element tagged as "canvas"
    var c=document.getElementById("canvas");

    //////////////////////////
    // INITIALISE MC Engine //
    //////////////////////////
    // CHOOSE a canvas size option // 

    // CHOICE 1: Canvas Size fixed by HTML
    MC.init(c);

    // CHOICE 2: Full Window Option (Canvas will be resized to fit window)
    //MC.init(c,"fullWindow");

    // Configure .game
    MC.game.gravity.set(0,100);        // gravity this game
    MC.game.physicsUpdates = 1;       // Simple example, no need for multiple physics Updates

    ///////////////////////////
    // END OF Initialisation //
    ///////////////////////////
    //-----------------------------------------------------------------------------
    //////////////////////////
    // VARIABLE DECLARATION //
    //////////////////////////

    var tilesheet = new MC.Picture("Images/MahjongTiles.png");
    var effectSheet = new MC.Picture("Images/Freeze.png");
    var backdrop = new MC.Picture("Images/blue-grunge-starburst.jpg")
    var shadow = new MC.Color(121, 121, 121, 0.7);

    var Effects = new MC.SpriteBin();
    EffectsFactory = function (position, duration) {
        var Effect = new MC.Sprite({ type: "anim", picture: effectSheet, scale: 0.75, pos: position, angle: MC.maths.randBetween(0,360), angleVel: MC.maths.randBetween(-360,360) });
        Effect.setAnimation(10, 10, 0, 0, 5, 8, 20);
        Effect.kill(duration);
        Effects.push(Effect);
    }

    var Scales = new Object({ dx: 48, dy: 60, dyy: 6 });

    function TileData( x, y, match, number, name) {
        this.x = x;
        this.y = y;
        this.match = match;
        this.number = number;
        this.name = name;  };

    var AllTileData = new Array (
        new TileData(0,0,1,1,"Flower-Plum"),
        new TileData(1,0,1,1,"Flower-Orchid"),
        new TileData(2,0,1,1,"Flower-Chrys"),
        new TileData(3,0,1,1,"Flower-Bamboo"),
        new TileData(4,0,2,1,"Flower-Summer"),
        new TileData(5,0,2,1,"Flower-Winter"),
        new TileData(6,0,2,1,"Flower-Fall"),
        new TileData(7,0,2,1,"Flower-Spring"),

        new TileData(0,1,3,4,"Dragon-Red"),
        new TileData(1,1,4,4,"Dragon-Green"),
        new TileData(2,1,5,4,"Dragon-White"),
        new TileData(3,1,6,4,"Wind-North"),
        new TileData(4,1,7,4,"Wind-South"),
        new TileData(5,1,8,4,"Wind-East"),
        new TileData(6,1,9,4,"Wind-West"),
        new TileData(7,1,10,4,"Crak-One"),

        new TileData(0, 2, 11, 4, "Crak-Two"),
        new TileData(1, 2, 12, 4, "Crak-Three"),
        new TileData(2, 2, 13, 4, "Crak-Four"),
        new TileData(3, 2, 14, 4, "Crak-Five"),
        new TileData(4, 2, 15, 4, "Crak-Six"),
        new TileData(5, 2, 16, 4, "Crak-Seven"),
        new TileData(6, 2, 17, 4, "Crak-Eight"),
        new TileData(7, 2, 18, 4, "Crak-Nine"),

        new TileData(0,3,19,4,"Bamboo-One"),
        new TileData(1,3,20,4,"Bamboo-Two"),
        new TileData(2,3,21,4,"Bamboo-Three"),
        new TileData(3,3,22,4,"Bamboo-Four"),
        new TileData(4,3,23,4,"Bamboo-Five"),
        new TileData(5,3,24,4,"Bamboo-Six"),
        new TileData(6,3,25,4,"Bamboo-Seven"),
        new TileData(7,3,26,4,"Bamboo-Eight"),

        new TileData(0,4,27,4,"Bamboo-Nine"),
        new TileData(1,4,28,4,"Coin-One"),
        new TileData(2,4,29,4,"Coin-Two"),
        new TileData(3,4,30,4,"Coin-Three"),
        new TileData(4,4,31,4,"Coin-Four"),
        new TileData(5,4,32,4,"Coin-Five"),
        new TileData(6,4,33,4,"Coin-Six"),
        new TileData(7,4,34,4,"Coin-Seven"),

        new TileData(0,5,35,4,"Coin-Eight"),
        new TileData(1,5,36,4,"Coin-Nine"),
        new TileData(2,5,37,0,"Blank"),
        new TileData(3,5,38,0,"Red"),
        new TileData(4,5,38,0,"Green"),
        new TileData(5,5,39,0,"Blue"),
        new TileData(6,5,40,0,"Yellow"),
        new TileData(7,5,41,0,"Mask")   );


    var Deck = new Array();
    for (var i = 0; i < AllTileData.length; i++) {
        for (var j = 0; j < AllTileData[i].number; j++) {
            Deck.push(i);
        }
    }

    ShuffleDeck = function () {
        Deck = MC.utils.shuffle(Deck);
    }

    //    |-0-|-1-|-2-|-3-|-4-|-5-|-6-|-7-|-8-|-9-|10-|11-|
    //            |12-|13-|14-|15-|16-|17-|18-|19-|
    //        |20-|21-|22-|23-|24-|25-|26-|27-|28-|29-|
    //    |30-|31-|32-|33-|34-|35-|36-|37-|38-|39-|40-|41-|
    //|42-|                                               |43-|44-|
    //    |45-|46-|47-|48-|49-|50-|51-|52-|53-|54-|55-|56-|
    //        |57-|58-|59-|60-|61-|62-|63-|64-|65-|66-|
    //            |67-|68-|69-|70-|71-|72-|73-|74-|
    //    |75-|76-|77-|78-|79-|80-|81-|82-|83-|84-|85-|86-|
    //
    //                |87-|88-|89-|90-|91-|92-|
    //                |93-|94-|95-|96-|97-|98-|
    //                |99-|100|101|102|103|104|
    //                |105|106|107|108|109|110|
    //                |111|112|113|114|115|116|
    //                |117|118|119|120|121|122|
    //
    //                    |123|124|125|126|
    //                    |127|128|129|130|
    //                    |131|132|133|134|
    //                    |135|136|137|138|
    //
    //                        |139|140|
    //                        |141|142|
    //
    //                          |143|

    // we need to draw these in ordr of tiers (in all shadow then all tiles) order
    var DrawOrder = [
        [0,86],
        [87,122],
        [123,138],
        [139,142],
        [143,143] ];


    var grid1 = new Array(
        // TIER ONE
        [-5.5, 3.5, 1, -1, 1, -1], [-4.5, 3.5, 1, 0, 2, -1], [-3.5, 3.5, 1, 1, 3, -1], [-2.5, 3.5, 1, 2, 4, -1], [-1.5, 3.5, 1, 3, 5, -1], [-0.5, 3.5, 1, 4, 6, -1],
        [ 0.5, 3.5, 1, 5, 7, -1], [1.5, 3.5, 1, 6, 8, -1], [2.5, 3.5, 1, 7, 9, -1], [3.5, 3.5, 1, 8, 10, -1], [4.5, 3.5, 1, 9, 11, -1], [5.5, 3.5, 1, 10, -1, -1],
    
        [-3.5,2.5,1,-1,12,-1],[-2.5,2.5,1,12,14,87],[-1.5,2.5,1,13,15,88],[-0.5,2.5,1,14,16,89],
        [0.5,2.5,1,15,17,90],[1.5,2.5,1,16,18,91],[2.5,2.5,1,17,19,92],[3.5,2.5,1,18,-1,-1],

        [-4.5,1.5,1,-1,21,-1],[-3.5,1.5,1,20,22,-1],[-2.5,1.5,1,21,23,93],[-1.5,1.5,1,22,24,94],[-0.5,1.5,1,23,25,95],
        [0.5,1.5,1,24,26,96],[1.5,1.5,1,25,27,97],[2.5,1.5,1,26,28,98],[3.5,1.5,1,27,29,-1],[4.5,1.5,1,28,-1,-1],

        [-5.5,0.5,1,42,31,-1],[-4.5,0.5,1,30,32,-1],[-3.5,0.5,1,31,33,-1],[-2.5,0.5,1,32,34,99],[-1.5,0.5,1,33,35,100],[-0.5,0.5,1,34,36,101],
        [0.5,0.5,1,35,37,102],[1.5,0.5,1,36,38,103],[2.5,0.5,1,37,39,104],[3.5,0.5,1,38,40,-1],[4.5,0.5,1,39,41,-1],[5.5,0.5,1,40,43,-1],

        [-6.5,0,1,-1,30,45],[6.5,0,1,41,44,56],[7.5,0,1,43,-1,-1],

        [-5.5,-0.5,1,42,46,-1],[-4.5,-0.5,1,45,47,-1],[-3.5,-0.5,1,46,48,-1],[-2.5,-0.5,1,47,49,105],[-1.5,-0.5,1,48,50,106],[-0.5,-0.5,1,49,51,107],
        [0.5,-0.5,1,50,52,108],[1.5,-0.5,1,51,53,109],[2.5,-0.5,1,52,54,110],[3.5,-0.5,1,53,55,-1],[4.5,-0.5,1,54,56,-1],[5.5,-0.5,1,55,43,-1],

        [-4.5,-1.5,1,-1,58,-1],[-3.5,-1.5,1,57,59,-1],[-2.5,-1.5,1,58,60,111],[-1.5,-1.5,1,59,61,112],[-0.5,-1.5,1,60,62,113],
        [0.5,-1.5,1,61,63,114],[1.5,-1.5,1,62,64,115],[2.5,-1.5,1,63,65,116],[3.5,-1.5,1,64,66,-1],[4.5,-1.5,1,65,-1,-1],

        [-3.5,-2.5,1,-1,68,-1],[-2.5,-2.5,1,67,69,117],[-1.5,-2.5,1,68,70,118],[-0.5,-2.5,1,69,71,119],
        [0.5,-2.5,1,70,72,120],[1.5,-2.5,1,71,73,121],[2.5,-2.5,1,72,74,122],[3.5,-2.5,1,73,-1,-1],

        [-5.5, -3.5, 1, -1, 76, -1], [-4.5, -3.5, 1, 75, 77, -1], [-3.5, -3.5, 1, 76, 78, -1], [-2.5, -3.5, 1, 77, 79, -1], [-1.5, -3.5, 1, 78, 80, -1], [-0.5, -3.5, 1, 79, 81, -1],
        [ 0.5, -3.5, 1, 80, 82, -1], [1.5, -3.5, 1, 81, 83, -1], [2.5, -3.5, 1, 82, 84, -1], [3.5, -3.5, 1, 83, 85, -1], [4.5, -3.5, 1, 84, 86, -1], [5.5, -3.5, 1, 85, -1, -1],
       //  TIER TWO
        [-2.5, 2.5, 2, -1, 88, -1], [-1.5, 2.5, 2, 87, 89, -1],[-0.5, 2.5, 2, 88, 90, -1], [0.5, 2.5, 2, 89, 91, -1], [1.5, 2.5, 2, 90, 92, -1],[2.5, 2.5, 2, 91, -1, -1],
        [-2.5, 1.5, 2, -1, 94, -1], [-1.5, 1.5, 2, 93, 95, 123],[-0.5, 1.5, 2, 94, 96, 124], [0.5, 1.5, 2, 95, 97, 125], [1.5, 1.5, 2, 96, 98, 126],[2.5, 1.5, 2, 97, -1, -1],
        [-2.5, 0.5, 2, -1, 100, -1], [-1.5, 0.5, 2, 99, 101, 127],[-0.5, 0.5, 2, 100, 102, 128], [0.5, 0.5, 2, 101, 103, 129], [1.5, 0.5, 2, 102, 104, 130],[2.5, 0.5, 2, 103, -1, -1],
        [-2.5, -0.5, 2, -1, 106, -1], [-1.5, -0.5, 2, 105, 107, 131],[-0.5, -0.5, 2, 106, 108, 132], [0.5, -0.5, 2, 107, 109, 133], [1.5, -0.5, 2, 108, 110, 134],[2.5, -0.5, 2, 109, -1, -1],
        [-2.5, -1.5, 2, -1, 112, -1], [-1.5, -1.5, 2, 111, 113, 135],[-0.5, -1.5, 2, 112, 114, 136], [0.5, -1.5, 2, 113, 115, 137], [1.5, -1.5, 2, 114, 116, 138],[2.5, -1.5, 2, 115, -1, -1],
        [-2.5, -2.5, 2, -1, 118, -1], [-1.5, -2.5, 2, 117, 119, -1],[-0.5, -2.5, 2, 118, 120, -1], [0.5, -2.5, 2, 119, 121, -1], [1.5, -2.5, 2, 120, 122, -1],[2.5, -2.5, 2, 121, -1, -1],
        //  TIER THREE
        [-1.5,1.5,3,-1,124,-1],[-0.5,1.5,3,123,125,-1],[0.5,1.5,3,124,126,-1],[1.5,1.5,3,125,-1,-1],
        [-1.5,0.5,3,-1,128,-1],[-0.5,0.5,3,127,129,139],[0.5,0.5,3,128,130,140],[1.5,0.5,3,129,-1,-1],
        [-1.5,-0.5,3,-1,132,-1],[-0.5,-0.5,3,131,133,141],[0.5,-0.5,3,132,134,142],[1.5,-0.5,3,133,-1,-1],
        [-1.5,-1.5,3,-1,136,-1],[-0.5,-1.5,3,135,137,-1],[0.5,-1.5,3,136,138,-1],[1.5,-1.5,3,137,-1,-1],
        // TIER FOUR
        [-0.5,0.5,4,-1,140,143],[0.5,0.5,4,139,-1,143],
        [-0.5,-0.5,4,-1,142,143],[0.5,-0.5,4,141,-1,143],
        //  TIER FIVE
        [0,0,5,-1,-1,-1]
        );

    function GridData(x,y, tier,left,right,upper) {
        this.offX = x;
        this.offY = y;
        this.tier = tier;
        this.left = left;
        this.right = right;
        this.upper = upper;
    };



    TileFactory = function (TileDataIndex) {
        T = new MC.Sprite({
            type: "anim",
            picture: tilesheet,
            pos: new MC.Point(MC.canvas.midX, MC.canvas.midY),
            scale: 0.75
        });
        T.data = AllTileData[TileDataIndex];
        T.setAnimation(8, 6, T.data.x, T.data.y, T.data.x, T.data.y, 99999);
        return T;
    };

    var GameGrid = new Array();

    GameGrid.update = function () {
        for (var i = 0; i < 144; i++) {
            GameGrid[i].Tile.update();
        }
    };

    GameGrid.render = function () {
        for (var Tier = 0; Tier < DrawOrder.length; Tier++) {
            // draw shadows
            for (var i = DrawOrder[Tier][0]; i <= DrawOrder[Tier][1]; i++) {
                if(GameGrid[i].alive)
                    MC.draw.rectBasic(GameGrid[i].Tile.pos.clone().add(-7, 7), GameGrid[i].Tile.size1 / 8 * GameGrid[i].Tile.scale, GameGrid[i].Tile.size2 / 6 * GameGrid[i].Tile.scale, Tier > 0 ? shadow : "black");
            }
            // draw tiles
            for (var i = DrawOrder[Tier][0]; i <= DrawOrder[Tier][1]; i++) {
                if (GameGrid[i].alive)
                    GameGrid[i].Tile.render();
            }
        }

        if (GameGrid.Selected != null) {
            GameGrid[GameGrid.Selected].Tile.drawBB("orange");
            /*
            // CHEAT FUNCTION.  Saved for dev purposes (highlights tile matches to the currently selected one
            for (var i = 0; i < 144; i++) {
                if (GameGrid.Selected != i && GameGrid[i].alive && GameGrid[i].free() && GameGrid[i].type == GameGrid[GameGrid.Selected].type) {
                    GameGrid[i].Tile.drawBB("red");
                }
            }
            */
        }
    };

    GameGrid.clear = function () {
        GameGrid.length = 0;
    }

    GameGrid.Selected = null;

    GameGrid.OnClick = function () {
        var m = MC.mouse.pos.clone();
        var onGrid = false;
        for (var i = 0; i < 144; i++) {
            if (GameGrid[i].Tile.hit(m)) {
                if (GameGrid[i].alive) onGrid = true;
                var free = GameGrid[i].free();

                // Null, select if legal
                if (GameGrid.Selected == null && free == true) {
                    GameGrid.Selected = i;
                }
                    // Deselect if required
                else if (GameGrid.Selected == i) {
                    GameGrid.Selected = null;
                }
                    // maka a match
                else if (i != GameGrid.Selected && free && GameGrid[i].type == GameGrid[GameGrid.Selected].type) {
                    EffectsFactory(GameGrid[i].Tile.pos, 0.3);
                    EffectsFactory(GameGrid[GameGrid.Selected].Tile.pos, 0.3);
                    GameGrid[i].alive = GameGrid[GameGrid.Selected].alive = false;
                    Moves.addToHistory( i, GameGrid.Selected );
                    GameGrid.Selected = null;
                    Moves.Calculate();
                }
                    // Deselect AND Reselect
                else if (i != GameGrid.Selected && free == true) {
                    GameGrid.Selected = i;
                }
            }
        }
        if (!onGrid) GameGrid.Selected = null;
    };

    GameGrid.Shuffle = function () {
        var positions = [];
        var tileNumbers = [];
        for (var i = 0; i < 144; i++) {
            if (GameGrid[i].alive) {
                positions.push(i);
                tileNumbers.push(GameGrid[i].tileNumber);
            }
        }
        tileNumbers = MC.utils.shuffle(tileNumbers);
        var len = positions.length;
        for (var i = 0; i < len; i++) {
            GameGrid[positions[i]] = ConfiguredTileFactory( positions[i], tileNumbers[i] );
        }
        Moves.Calculate();
        Moves.newGame();
        GOCon.reset();
    };
    
    ConfiguredTileFactory = function ( GridPosition, TileNumber ) {
        var g = new Object();
        g.Tile = TileFactory(TileNumber);
        g.tileNumber = TileNumber;
        g.gridPosition = GridPosition;
        g.type = g.Tile.data.match;
        g.name = g.Tile.data.name;
        g.offX = grid1[GridPosition][0];
        g.offY = grid1[GridPosition][1];
        g.tier = grid1[GridPosition][2];
        g.Tile.pos.x = MC.canvas.midX + (g.offX * Scales.dx);
        g.Tile.pos.y = MC.canvas.midY + (g.offY * Scales.dy * -1) - (g.tier * Scales.dyy);
        g.leftTile = grid1[GridPosition][3];
        g.rightTile = grid1[GridPosition][4];
        g.upperTile = grid1[GridPosition][5];
        g.selected = false;
        g.alive = true;
        g.free = function () {
            if (!this.alive) return false;
            switch (this.gridPosition) {
                // offX,offY, tier, left, right, upper
                //  42 - [-6.5,0,1,-1,30,45],  43 -  [6.5,0,1,41,44,56],   44 -  [7.5,0,1,43,-1,-1],
                case 42:
                    return true;

                case 43:
                    if (GameGrid[44].alive == false || (GameGrid[41].alive == false && GameGrid[56].alive == false)) {
                        return true;
                    }
                    else {
                        return false;
                    }

                case 44:
                    return true;

                default:
                    if (this.upperTile != -1) {
                        if (GameGrid[this.upperTile].alive == true) return false;
                    }
                    if (this.rightTile == -1 || this.leftTile == -1) return true;
                    if (this.rightTile != -1 && GameGrid[this.rightTile].alive == false) return true;
                    if (this.leftTile != -1 && GameGrid[this.leftTile].alive == false) return true;

                    return false;
            }
        };
        return g;
    }

    Reset = function () {
        ShuffleDeck();
        GameGrid.clear();

        for (var i = 0; i < 144; i++) {
            GameGrid.push(ConfiguredTileFactory(i, Deck[i]));
            GameGrid[i].alive = true;
        }
        GameGrid.Selected = null;
        Moves.newGame();
        Moves.Calculate();
        Timer.start();
        EndGameController.stop();
    }

    var Moves = {
        Available: 0,
        OptionsArray: null, // Tile.Type ranges from 1-36 .. using a 37 length Array, to prevent head bending later
        Clear: function () {
            this.OptionsArray = new Array(37);
            for (var i = 0; i < 37; i++) this.OptionsArray[i] = new Array(0);
        },
        Calculate: function() {
            this.Clear();
            this.Available = 0;
            var aliveTiles = 0;
            for (var i = 0; i < 144; i++) {
                if (GameGrid[i].alive == true) aliveTiles++;
                if ( GameGrid[i].free() ) { 
                    this.OptionsArray[GameGrid[i].type].push( i );
                    switch ( this.OptionsArray[GameGrid[i].type].length ) {
                        case 0:
                        case 1:
                            break;
                        case 2:
                            this.Available += 1;
                            break;
                        case 3:
                            this.Available += 2;
                            break;
                        case 4:
                            this.Available += 3;
                            break;
                        default:
                            break;
                    }
                }
            }
            MoveDisplay.setValues({text: " Moves: "+ this.Available + " "});
            // Check for end Game
            if (this.Available == 0) {
                var message = aliveTiles == 0 ? "Well Done !!  New Game?" : "No Moves ... New Game?";
                GOCon.start(message);
                Timer.pause();
                if (aliveTiles == 0) EndGameController.start();
            }
        },
        history: [],
        addToHistory: function ( position1, position2 ) {
            this.history.push(new MC.Point(position1, position2));
        },
        newGame: function() {
            this.history = [];
        },
        undo: function() {
            if (this.history.length > 0) {
                var last = this.history.pop();
                GameGrid[last.x].alive = true;
                GameGrid[last.y].alive = true;
            }
        },
        restart: function() {
            for (var i = 0; i < 144; i++) GameGrid[i].alive = true;
        },
        hint: function () {
            var moves = [];
            for (var i = 0; i < this.OptionsArray.length; i++) {
                if( this.OptionsArray[i].length >=2){
                    moves.push(i);
                }
            }
            if (moves.length > 0) {
                moves = MC.utils.shuffle(moves);
                var index = moves[0];
                this.OptionsArray[index] = MC.utils.shuffle(this.OptionsArray[index]);
                var t1 = this.OptionsArray[index][0];
                var t2 = this.OptionsArray[index][1];
                EffectsFactory(GameGrid[t1].Tile.pos, 1);
                EffectsFactory(GameGrid[t2].Tile.pos, 1);
            }
        }
    };

    // of course, we need a piece of fun for the end game
    var EndGame = new MC.SpriteBin();
    var EndGameController = {
        running: false,
        time: 0,
        spriteCount: 80,
        update: function () {
            if (this.running == true) {
                if (EndGame.bin.length < this.spriteCount) {
                    this.time += MC.game.deltaTime;
                    if (this.time > 0.2) {
                        this.time = 0;
                        var tile = TileFactory(MC.maths.randBetween(0, AllTileData.length - 7));
                        tile.setValues({
                            edgeBounce: true,
                            gravity: true,
                            pos: new MC.Point(MC.maths.randBetween(40, MC.canvas.width - 40), 40),
                            vel: new MC.Point(MC.maths.randBetween(-80, 80), 0),
                            angleVel: MC.maths.randBetween( -360,360 )
                        });
                        EndGame.push(tile);
                    }
                }
            EndGame.update();
            }
        },
        stop: function () {
            EndGame.empty();
            this.running = false;
        },
        start: function () {
            EndGame.empty();
            this.running = true;
        },
        render: function () {
            EndGame.render();
        }
    };
   

    var leftGUI = new MC.GUI();      // GUI = holder for Control Boxes (MC.ConBox)
    var rightGUI = new MC.GUI();
    var ResetButton = new MC.ConBox({ width: 120,
                                height: 40,
                                text: "New Game (n)",
                                onClick: function () { Reset(); GOCon.reset(); }
    });
    var Shuffle = new MC.ConBox({
        width: 120,
        height: 40,
        text: " Shuffle (s) ",
        onClick: function () { GameGrid.Shuffle(); EndGameController.stop(); }

    });
    var Undo = new MC.ConBox({
        width: 120,
        height: 40,
        text: "  Undo (u)  ",
        onClick: function () { Moves.undo(); GOCon.reset(); EndGameController.stop(); }

    });
    var Restart = new MC.ConBox({
        width: 120,
        height: 40,
        text: " Restart (r) ",
        onClick: function () { Moves.restart(); GOCon.reset(); EndGameController.stop(); Timer.start(); }

    });
    var Hint = new MC.ConBox({
        width: 120,
        height: 40,
        text: "  Hint (h)  ",
        onClick: function () { Moves.hint(); }

    });
    // using this an an inert Con Box, for display purposes
    var MoveDisplay = new MC.ConBox({
        width: 120,
        height: 40,
        text: " Moves:  "
    });

    var TimeDisplay = MoveDisplay.clone(); // n.b. text will be set by Timer

    leftGUI.push(ResetButton, Restart, Shuffle, Undo, Hint);  // Add the ConBoxes to the GUI
    rightGUI.push(MoveDisplay, TimeDisplay);

    // special ConBox, modified to bounce in at end game, user Timer and the NEW MC.Utils.tween methods
 
    var GameOver = new MC.ConBox({
        width: 300,
        height: 70,
        text: "Well Done!!  New Game?",
        pos: new MC.Point(MC.canvas.midX, 0 - this.height),
        onClick: function () { GOCon.onClick(); } // defer click method to the Controller
    });

    GOCon = { // GameOver Controller
        box: GameOver,
        size: new MC.Point(GameOver.width, GameOver.height),
        positions: [ new MC.Point(MC.canvas.midX, 0 - (2.5 * GameOver.height)),
                     new MC.Point(MC.canvas.midX, MC.canvas.height - GameOver.height),
                     new MC.Point(MC.canvas.midX, MC.canvas.height + GameOver.height)],
        times: [ 2, 1 ],
        animationTime: 0,
        running: false,
        comingIn: true,
        onClick: function () {
            if (!this.running) {
                this.running = true;
                Reset();
            }
        },
        reset: function () {
            this.box.setValues({
                width: this.size.x,
                height: this.size.y,
                pos: this.positions[0]
            });
            this.running = false;
            this.comingIn = true;
        },
        start: function (message) {
            this.reset();
            if (message != undefined) this.box.setValues({ text: message });
            this.running = true;
            animationTime = 0; },
        update: function () {
            if (this.running == true) {
                this.animationTime += MC.game.deltaTime;
                var mod = this.comingIn == true ? 0 : 1;
                var finished = this.animationTime >= this.times[mod];
                var tween = this.animationTime / this.times[mod];
                this.box.setValues({
                    pos: MC.maths.lerp(this.positions[mod], this.positions[mod + 1], tween),
                });
                if (this.comingIn == true) {
                    this.box.setValues({
                        width: this.size.x * (tween + 0.1),
                        height: this.size.y * (tween + 0.1)
                    });
                }
                if (finished == true) {
                    if (this.comingIn == false) this.reset();
                    else {
                        this.running = false;
                        this.comingIn = false;
                        this.animationTime = 0;
                    }
                }
            }
            else { this.box.update(); }
        }
    }

    var Timer = {
        running: false,
        elapsed: 0,
        start: function () {
            this.running = true;
            this.elapsed = 0;
            this.updateLabel();
        },
        pause: function () {
            this.running = false;
        },
        update: function () {
            if (this.running == true) {
                this.elapsed += MC.game.deltaTime;
                this.updateLabel();
            }
        },
        updateLabel: function () {
            // credit to https://stackoverflow.com/questions/6312993/javascript-seconds-to-time-string-with-format-hhmmss (Apr 2020)
            var sec_num = parseInt(this.elapsed, 10);
            var hours = Math.floor(sec_num / 3600);
            var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
            var seconds = sec_num - (hours * 3600) - (minutes * 60);

            if (hours < 10) { hours = "0" + hours; }
            if (minutes < 10) { minutes = "0" + minutes; }
            if (seconds < 10) { seconds = "0" + seconds; }
            TimeDisplay.setValues({ text: hours + ':' + minutes + ':' + seconds });
        }
    };

    MC.keys.n.onUp = function () { Reset(); GOCon.reset(); };
    MC.keys.s.onUp = function () { GameGrid.Shuffle(); };
    MC.keys.r.onUp = function () { Restart.onClick(); };
    MC.keys.u.onUp = function () { Undo.onClick(); };
    MC.keys.h.onUp = function () { Hint.onClick(); };

    MC.mouse.onClickL = function () { // configure MC.game so it checks the GUI upon Left click down 
        GameGrid.OnClick();
        leftGUI.click();
        GameOver.checkClick();

        // Feel free to add further code for Left Clicks here 
    };    
    


//////////////////////////
// END OF declarations  //
//////////////////////////


//////////////////////////
// Game Initialisation  //
//////////////////////////

    Reset();
    GOCon.reset();

//////////////////////////
//      GAME LOOP       //
//////////////////////////

// first call, request subsequently made within
gameLoop();

function gameLoop() {
    // ESSENTIAL MAINTAINANCE
    requestAnimationFrame(gameLoop);    // Required so the PC will automatically call the gameLoop again when (1) the screen is ready and (2) the CPU has finished any previous frame calculations
    MC.game.update();                   // Essential to update MC.game to ensure MC.game.deltaTime is available
    
    // PHYSICS UPDATE LOOP .. 

    for (var i = 0; i < MC.game.physicsUpdates; i++) {
        EndGameController.update();
        // Add Further Sprite and SpriteBin updates here
    }


    // NON-PHYSICS UPDATES
    GameGrid.update();
    leftGUI.update();
    GOCon.update();
    Effects.update();
    Timer.update();

    // RENDER
    // Stage 1.  Clear the canvas
    MC.draw.clearCanvas("Black");

    // Stage 2.  Render Sprites
    backdrop.render(new MC.Point(MC.canvas.midX, MC.canvas.midY), 0, MC.canvas.width, MC.canvas.height);
    GameGrid.render();
    Effects.render();
    EndGameController.render();
    
    // Stage 3.  Render GUIs (and moves)
    leftGUI.render(0, 0);
    rightGUI.render(MC.canvas.right - 140, 0);
    GameOver.render();

}

}); // EoF