Exit Full View

Locked-down Games / server.js

"use strict";

var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io').listen(server);
const { exec } = require("child_process");

/**
 * Instances of GameType.
 */
var gameTypes = {};


/**
 * Instance of Game that are currently being played on this server. Keyed on
 * the prototype game's id.
 */
var games = {};

class GameType {
    
    constructor(id, name, variations) {
        this.id = id;
        this.name = name;
        this.variations = variations;
        this.instances = {}; // Running instances of type Game keyed on Game.id.
        this.nextGameId = 1;
    }
    
    newGame(variationId, name) {
        for ( var variationIndex = 0; variationIndex < this.variations.length; variationIndex ++ ) {
            var variation = this.variations[variationIndex];
            if ( variation.id == variationId ) {
                var game = variation.copy(`${variation.id}${this.nextGameId ++}`);
                game.name = name;
                games[game.id] = game;
                this.instances[game.id] = game;
                return game;
            }
        }
        return null;
    }
    
    listGames(includePrivate) {
        var instances = {};
        for (var id in this.instances) {
            var game = this.instances[id];
            if (includePrivate || game.isPublic) {
                instances[id] = {
                    gameId: game.id,
                    name: game.name,
                    playerCount: Object.keys(game.players).length,
                    maxPlayers: game.maxPlayers,
                    lastActive : game.lastActive.getTime()
                };
            }
        }
        return instances;
    }
}

function addGameTypeVariations( id, name, variations ) {
    gameTypes[id] = new GameType( id, name, variations );
}

function addGameType(variation) {
    gameTypes[variation.id] = new GameType(variation.id, variation.name, [variation] );
}


function resetAction(game) {
    io.to(game.id).emit( 'movePieces', game.reset() );
}
 
function sendCharade(game, socket, type, filename, size) {
    try {
        const nth = Math.floor( Math.random() * size );
        const command = `head -n ${nth} charades/${filename} | tail -1`;

        exec(command, (error, stdout, stderr) => {        
            const chatInfo = {
                name : '',
                message : `(${type}) ${stdout}`,
                playerNumber : -1,
                isSpectator : false
            };
            socket.emit( 'chat', chatInfo );
        });
    } catch (e) {
        console.log( `sendCharade failed : ${e}` );
    }
}

function takeRandomCharadeAction(game, socket) {
    const type = Math.floor( Math.random() * 4 );
    if ( type == 1 ) {
        takeSongCharadeAction(game,socket);
    } else if (type == 2) {
        takeTVCharadeAction(game,socket);
    } else if (type == 3) {
        takeBookCharadeAction(game,socket);
    } else {
        takeFilmCharadeAction(game,socket);
    }
}

function takeFilmCharadeAction(game, socket) {
    sendCharade(game,socket, "Film", "top1000Films.txt", 1000 );
}
function takeSongCharadeAction(game, socket) {
    sendCharade(game,socket, "Song", "top1000Songs.txt", 1000 );
}
function takeTVCharadeAction(game, socket) {
    sendCharade(game,socket, "TV Show", "top250TVShows.txt", 250 );
}
function takeBookCharadeAction(game, socket) {
    sendCharade(game,socket, "Book", "top1000Books.txt", 998 );
}


function shuffleAction(game) {
    io.to(game.id).emit( 'movePieces', game.shuffle() );
}

function throwAction(game) {
    io.to(game.id).emit( 'throwDice', game.throwDice('dice') );
}

function createNameTagsAction(game) {
    game.createNameTags();
}

function createXmasDecorationsAction(game) {
    game.createXmasDecorations();
}

function createBirthdayDecorationsAction(game) {
    game.createBirthdayDecorations();
}

function throwBlueAction(game) {
    io.to(game.id).emit( 'throwDice', game.throwDice('diceBlue') );
}

function createMoveGroupAction( fromAreaName, toAreaName, shuffle ) {
    return function(game) {
        io.to(game.id).emit( 'movePieces', game.moveGroup( fromAreaName, toAreaName, shuffle ) );
    };
}

class Game {
    
    constructor( id, name ) {
        this.id = id;
        this.name = name;
        this.isPublic = true; // Spectators can join the game.
        this.maxPlayers = 2; // Maximum number of players (player with higher indices will be SPECTATORS).
        this.instructions = ''; // HTML, not plain text
        this.players = {}; // Keyed on the socket's id.
        this.assets = {}; // Keyed on name
        this.pieces = {}; // Keyed on name.
        this.backgrounds = {}; // Keyed on name.
        this.buttons = {}; // Keyed on name.
        this.specialAreas = {}; // keyed by a string (the area does not have a name).
        this.glue = {};

        this.topMost = 0; // The maximum zIndex of all this.pieces.zIndex
        this.minSnap2 = 36; // Snap by 6 pixels (36 is the square of the distance).
        
        this.lastActive = new Date(); // The last action that the server received from a player.      
        
        this.assets['nameTag'] = {
            type : 'ninepatch',
            name : 'nameTag',
            url : 'assets/nameTag.png',
            sizeX : 99,
            sizeY : 27,
            columns: [5,89,5],
            rows: [5,17,5],
            margin: [2,10,2,10]
        };

        this.buttons['nametags'] = {
            name: 'nametags',
            text: 'Add Tags',
            instructions: 'Adds a movable name tag for each player',
            action: createNameTagsAction,
            assetName: 'button',
            x: -1100,
            y: 100
        };
        
        this.buttons['xmas'] = {
            name: 'xmas',
            text: 'Add Christmas Decorations',
            instructions: 'Adds Christmas decoration pieces that can be moved about',
            action: createXmasDecorationsAction,
            assetName: 'button',
            x: -1100,
            y: 100
        };
        
        
        this.buttons['birthday'] = {
            name: 'birthday',
            text: 'Add Birthday Decorations',
            instructions: 'Adds Birthday decoration pieces that can be moved about',
            action: createBirthdayDecorationsAction,
            assetName: 'button',
            x: -1100,
            y: 100
        };
    }
    
    active() {
        this.lastActive = new Date();
    }
    
    copy(id) {
        
        var result = new Game(id);
        result.instructions = this.instructions;
        
        shallowCopyObjectMap( this.assets, result.assets );
        shallowCopyObjectMap( this.backgrounds, result.backgrounds );
        shallowCopyObjectMap( this.buttons, result.buttons );
        shallowCopyObjectMap( this.specialAreas, result.specialAreas );
        shallowCopyObjectMap( this.pieces, result.pieces );
 
        for (var glueId in this.glue) {
            result.glue[glueId] = this.glue[glueId];
        }
        
        result.topMost = this.topMost;
        result.minSnap2 = this.minSnap2;
        
        result.prototype = this; // Used by shuffle()
        result.shuffle();
        
        return result;
    }
    
    
    initCribbage() {
        console.log( `Initialising cribbage` );

        this.deckX = 450;
        this.deckY = 480;

        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/cribbage.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['cards'] = {
            type : 'spritesheet',
            name : 'cards',
            url : 'assets/pieces/cards.png',
            sizeX : 79,
            sizeY : 123
        };
        this.assets['pinBlue'] = {
            type : 'image',
            name : 'pinBlue',
            url : 'assets/pieces/pinBlue.png',
            sizeX : 40,
            sizeY : 40
        };
        this.assets['pinRed'] = {
            type : 'image',
            name : 'pinRed',
            url : 'assets/pieces/pinRed.png',
            sizeX : 40,
            sizeY : 40
        };
        this.assets['dealer'] = {
            type : 'image',
            name : 'dealer',
            url : 'assets/pieces/dealer.png',
            sizeX : 46,
            sizeY : 46
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['player0'] = {
            type: 'reveal',
            x : 0,
            y : 385,
            width : 290,
            height : 212,
            playerNumber : 0
        };
        this.specialAreas['player1'] = {
            type: 'reveal',
            x : 504,
            y : 385,
            width : 290,
            height : 212,
            playerNumber : 1
        };
        this.specialAreas['showdown'] = {
            type: 'reveal',
            x : 0,
            y : 0,
            width : 800,
            height : 390,
            playerNumber : null
        };
        this.specialAreas['holes'] = {
            type: 'snap',
            x: 100,
            y: 34,
            width: 575,
            height: 140,
            snapX: 20,
            snapY: 20
        };
        this.specialAreas['pack'] = {
            type: 'shuffle',
            x: this.deckX - 40,
            y: this.deckY - 62,
            width: 80,
            height: 124
        }
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX-8,
            textY: this.deckY - 80
        }
        
        this.buttons['shuffle'] = {
            name: 'shuffle',
            text: 'Shuffle',
            action: shuffleAction,
            confirm: true,
            assetName: 'button',
            x: 450,
            y: 560,
        };
        
        for ( var suit = 0; suit < 4; suit ++ ) {
            for ( var value = 0; value < 13; value ++ ) {
                var name = `card_${suit}_${value}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'cards',
                    assetNumber : 52+2,
                    hiddenAssetNumber : 52+2,
                    visibleAssetNumber : suit * 13 + value,
                    x : this.deckX,
                    y : this.deckY,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
            }
        }
    
        name = 'pinRed';
        this.pieces[name] = {
            name : name,
            assetName : 'pinRed',
            x : 83,
            y : 45,
            zIndex : this.topMost++,
            gluedTo : null,
            isPermanent : true
        };
        
        name = 'pinBlue';
        this.pieces[name] = {
            name : name,
            assetName : 'pinBlue',
            x : 83,
            y : 168,
            zIndex : this.topMost++,
            gluedTo : null,
            isPermanent : true
        };
        
        this.pieces['dealer'] = {
            name: 'dealer',
            assetName: 'dealer',
            x: 400,
            y: 400,
            zIndex: this.topMost++,
            isPermanent : true
        };
        
        return this;
    }
    
    
    initTwoPlayerCards() {
        console.log( `Initialising twoPlayerCards` );

        this.deckX = 60;
        this.deckY = 300;

        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/twoPlayerCards.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['cards'] = {
            type : 'spritesheet',
            name : 'cards',
            url : 'assets/pieces/cards.png',
            sizeX : 79,
            sizeY : 123
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dealer'] = {
            type : 'image',
            name : 'dealer',
            url : 'assets/pieces/dealer.png',
            sizeX : 46,
            sizeY : 46
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['player0'] = {
            type: 'reveal',
            x : 0,
            y : 600-140,
            width : 800,
            height : 140,
            playerNumber : 1
        };
        this.specialAreas['player1'] = {
            type: 'reveal',
            x : 0,
            y : 0,
            width : 800,
            height : 140,
            playerNumber : 0
        };
        this.specialAreas['showdown'] = {
            type: 'reveal',
            x : 160,
            y : 140,
            width : 800-160,
            height : 600-140*2,
            playerNumber : null
        };
        this.specialAreas['pack'] = {
            type: 'shuffle',
            x: this.deckX - 40,
            y: this.deckY - 62,
            width: 80,
            height: 124
        }
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX-8,
            textY: this.deckY - 80
        }
        
        this.buttons['shuffle'] = {
            name: 'shuffle',
            text: 'Shuffle',
            action: shuffleAction,
            confirm: true,
            assetName : 'button',
            x: this.deckX,
            y: this.deckY + 90
        };
        
        for ( var suit = 0; suit < 4; suit ++ ) {
            for ( var value = 0; value < 13; value ++ ) {
                var name = `card_${suit}_${value}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'cards',
                    assetNumber : 52+2,
                    hiddenAssetNumber : 52+2,
                    visibleAssetNumber : suit * 13 + value,
                    x : this.deckX,
                    y : this.deckY,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
            }
        }
    
        this.pieces['dealer'] = {
            name: 'dealer',
            assetName: 'dealer',
            x: 160,
            y: this.deckY,
            zIndex: this.topMost++,
            isPermanent : true
        };
        
        return this;
    }
    
    
    
    initTwoPlayerCardsMini() {
        console.log( `Initialising twoPlayerCardsMini` );

        this.deckX = 56;
        this.deckY = 300;

        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/twoPlayerCardsMini.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['cards'] = {
            type : 'spritesheet',
            name : 'cards',
            url : 'assets/pieces/miniPlayingCards.png',
            sizeX : 41,
            sizeY : 61
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dealer'] = {
            type : 'image',
            name : 'dealer',
            url : 'assets/pieces/dealer.png',
            sizeX : 46,
            sizeY : 46
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['player0'] = {
            type: 'reveal',
            x : 0,
            y : 600-90,
            width : 800,
            height : 90,
            playerNumber : 1
        };
        this.specialAreas['player1'] = {
            type: 'reveal',
            x : 0,
            y : 0,
            width : 800,
            height : 90,
            playerNumber : 0
        };
        this.specialAreas['showdown'] = {
            type: 'reveal',
            x : 85,
            y : 90,
            width : 800-85,
            height : 600-90*2,
            playerNumber : null
        };
        this.specialAreas['deck'] = {
            type: 'bottom',
            assetName: 'cards',
            x : this.deckX - 20,
            y : this.deckY - 30,
            width : 40,
            height : 60,
        };
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX-8,
            textY: this.deckY - 60
        }
        
        this.buttons['shuffle'] = {
            name: 'shuffle',
            text: 'Shuffle',
            action: shuffleAction,
            confirm: true,
            assetName: 'button',
            x: this.deckX,
            y: this.deckY + 55
        };
        
        for ( var suit = 0; suit < 4; suit ++ ) {
            for ( var value = 0; value < 13; value ++ ) {
                var name = `card_${suit}_${value}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'cards',
                    assetNumber : 52+2,
                    hiddenAssetNumber : 52+2,
                    visibleAssetNumber : suit * 13 + value,
                    x : this.deckX,
                    y : this.deckY,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
            }
        }
    
        this.pieces['dealer'] = {
            name: 'dealer',
            assetName: 'dealer',
            x: 85,
            y: 120,
            zIndex: this.topMost++,
            isPermanent : true
        };
        
        return this;
    }
    
    initDraughts() {

        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/chess.jpg',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['white'] = {
            type : 'image',
            name : 'white',
            url : 'assets/pieces/discWhite46.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['black'] = {
            type : 'image',
            name : 'black',
            url : 'assets/pieces/discBlack46.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['crown'] = {
            type : 'image',
            name : 'crown',
            url : 'assets/pieces/crown.png',
            sizeX : 40,
            sizeY : 23
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        
        
        this.specialAreas['grid'] = {
            type: 'snap',
            x: 230-50/2,
            y: 129-50/2,
            width: 49*8,
            height: 49*8,
            snapX: 49,
            snapY: 49
        };
        
        this.glue['crown'] = ['white', 'black'];
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 720,
            y: 570,
        };
        
        var x,y,i,name;
        i = 0;
        for (x = 0; x < 4; x++) {
            for (y = 0; y < 3; y ++) {
                
                name = `white_${i}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'white',
                    x : 230 + 98 * x + ((y == 1) ? 0 : 50),
                    y : 125 + 50 * y,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
                
                name = `black_${i}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'black',
                    x : 230 + 98 * x + ((y == 1) ? 50 : 0),
                    y : 472 - 50 * y,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
                i ++;
            }
        }
        
        i = 0;
        for (x = 0; x < 9; x ++ ) {
            name = `crown_${i++}`;
            this.pieces[name] = {
                name : name,
                assetName : 'crown',
                x : x + 200 + x * 49,
                y : 50,
                zIndex : this.topMost++,
                gluedTo : null
            }
            name = `crown_${i++}`;
            this.pieces[name] = {
                name : name,
                assetName : 'crown',
                x : x + 200 + x * 49,
                y : 550,
                zIndex : this.topMost++,
                gluedTo : null
            }
        }
        
        return this;
    }
 
    initChess() {

        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/chess.jpg',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['chessPieces'] = {
            type : 'spritesheet',
            name : 'chessPieces',
            url : 'assets/pieces/chessPieces.png',
            sizeX : 280/6,
            sizeY : 50
        };
        
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        
        this.specialAreas['grid'] = {
            type: 'snap',
            x: 230-50/2,
            y: 129-50/2,
            width: 49*8,
            height: 49*8,
            snapX: 49,
            snapY: 49
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName : 'button',
            x: 720,
            y: 570,
        };
        
        for ( var x = 0; x < 8; x ++ ) {
            this.addChessPiece( 5, x, 1 );
        }
        this.addChessPiece( 2, 0, 0 );
        this.addChessPiece( 4, 1, 0 );
        this.addChessPiece( 3, 2, 0 );
        this.addChessPiece( 0, 3, 0 );
        this.addChessPiece( 1, 4, 0 );
        this.addChessPiece( 3, 5, 0 );
        this.addChessPiece( 4, 6, 0 );
        this.addChessPiece( 2, 7, 0 );
        
        return this;
    }

    addChessPiece( pieceNumber, x, y ) {
        var name = `piece_${x}_${y}`;
        this.pieces[name] = {
            name : name,
            assetName : 'chessPieces',
            assetNumber: pieceNumber + 6,
            x : 230 + 49 * x,
            y : 129 + 49 * y,
            zIndex : this.topMost++,
            gluedTo : null
        }
        y = 7-y;
        name = `piece_${x}_${y}`;
        this.pieces[name] = {
            name : name,
            assetName : 'chessPieces',
            assetNumber: pieceNumber,
            x : 230 + 49 * x,
            y : 129 + 49 * y,
            zIndex : this.topMost++,
            gluedTo : null
        }
    }
    
    initBackgammon() {
        console.log( `Initialising backgammon` );

        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/backgammon.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['white'] = {
            type : 'image',
            name : 'white',
            url : 'assets/pieces/discWhite46.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['black'] = {
            type : 'image',
            name : 'black',
            url : 'assets/pieces/discBlack46.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dice'] = {
            type : 'spritesheet',
            name : 'dice',
            url : 'assets/pieces/dice.png',
            sizeX : 42,
            sizeY : 38
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.buttons['throw'] = {
            name: 'throw',
            text: 'Throw',
            action: throwAction,
            assetName : 'button',
            x: 300,
            y: 325
        };
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 150,
            y: 300
        };
   
        this.specialAreas['gridBL'] = {
            type: 'snap',
            x: 55,
            y: 345,
            width: 310,
            height: 210,
            snapX: 330/12,
            snapY: 46/2
        };
        this.specialAreas['gridTL'] = {
            type: 'snap',
            x: 55,
            y: 48,
            width: 310,
            height: 210,
            snapX: 330/12,
            snapY: 46/2
        };
        this.specialAreas['gridBR'] = {
            type: 'snap',
            x: 55 + 390,
            y: 345,
            width: 310,
            height: 210,
            snapX: 330/12,
            snapY: 46/2
        };
        this.specialAreas['gridTR'] = {
            type: 'snap',
            x: 55 + 390,
            y: 48,
            width: 310,
            height: 210,
            snapX: 330/12,
            snapY: 46/2
        };
        
        this.addBackgammonPieces(732,  1, 2, 'white');
        this.addBackgammonPieces(732, -1, 2, 'black');
        
        this.addBackgammonPieces(458,  1, 5, 'black');
        this.addBackgammonPieces(458, -1, 5, 'white');
        
        this.addBackgammonPieces(68,  1, 5, 'white');
        this.addBackgammonPieces(68, -1, 5, 'black');

        this.addBackgammonPieces(288,  1, 3, 'black');
        this.addBackgammonPieces(288, -1, 3, 'white');
        
        this.pieces['dice_0'] = {
            name : 'dice_0',
            assetName : 'dice',
            assetNumber : 0,
            x : 280,
            y : 285
        };
        this.pieces['dice_1'] = {
            name : 'dice_1',
            assetName : 'dice',
            assetNumber : 0,
            x : 320,
            y : 285
        };
        
        return this;
    }

    addBackgammonPieces( x, dy, count, color ) {
        var y = dy == 1 ? 60 : 540;
        for (var i = 0; i < count; i ++ ) {
            var name = color + Object.keys(this.pieces).length;
            this.pieces[name] = {
                name: name,
                assetName: color,
                x: x,
                y: y + i * dy * 46
            }
        }
    }
    
    
    initCrossedWords() {
        console.log( `Initialising CrossedWords` );

        this.bagX = 620;
        this.bagY = 550;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/crossedWords.png',
            sizeX : 800,
            sizeY : 600
        };
        
        this.assets['letters'] = {
            type : 'spritesheet',
            name : 'letters',
            url : 'assets/pieces/letters.png',
            sizeX : 33,
            sizeY : 33
        };
        
        this.assets['button'] = {
            type: 'spritesheet',
            name: 'button',
            url: 'assets/button.png',
            sizeX: 104,
            sizeY: 31
        };
        
        this.backgrounds['board'] = {
            name: 'board',
            assetName: 'board',
            x: 400,
            y: 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x: 0,
            y: 520,
            width: 520,
            height:750,
            playerNumber : -1
        };
        this.specialAreas['board'] = {
            type: 'reveal',
            x: 0,
            y: 0,
            width: 520,
            height: 520,
            playerNumber: null
        };
        this.specialAreas['boardSnap'] = {
            type: 'snap',
            x: 10,
            y: 10,
            width: 515,
            height: 515,
            snapX: 34.5,
            snapY: 34.5
        };
        this.specialAreas['bag'] = {
            type: 'shuffle',
            x: this.bagX - 20,
            y: this.bagY - 20,
            width: 40,
            height: 40
        };
        this.specialAreas['tileCounter'] = {
            type: 'counter',
            x: this.bagX,
            y: this.bagY,
            textX: this.bagX+40,
            textY: this.bagY-10
        }
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 680,
            y: 20
        };
   
        this.addCrossedWordsLetter('_',2);
        this.addCrossedWordsLetter('A',9);
        this.addCrossedWordsLetter('B',2);
        this.addCrossedWordsLetter('C',2);
        this.addCrossedWordsLetter('D',4);
        this.addCrossedWordsLetter('E',12);
        this.addCrossedWordsLetter('F',2);
        this.addCrossedWordsLetter('G',3);
        this.addCrossedWordsLetter('H',2);
        this.addCrossedWordsLetter('I',9);
        this.addCrossedWordsLetter('J',1);
        this.addCrossedWordsLetter('K',1);
        this.addCrossedWordsLetter('L',4);
        this.addCrossedWordsLetter('M',2);
        this.addCrossedWordsLetter('N',6);
        this.addCrossedWordsLetter('O',8);
        this.addCrossedWordsLetter('P',2);
        this.addCrossedWordsLetter('Q',1);
        this.addCrossedWordsLetter('R',6);
        this.addCrossedWordsLetter('S',4);
        this.addCrossedWordsLetter('T',6);
        this.addCrossedWordsLetter('U',4);
        this.addCrossedWordsLetter('V',2);
        this.addCrossedWordsLetter('W',2);
        this.addCrossedWordsLetter('X',1);
        this.addCrossedWordsLetter('Y',2);
        this.addCrossedWordsLetter('Z',1);
            
        return this;
    }

    initSpeedAnagrams() {
        console.log( `Initialising Speed Anagram` );

        this.instructions = '<a href="https://bananagrams.com/blog/how-to-play-bananagrams-instructions-for-getting-started">How to play</a>';
        
        this.bagX = 400;
        this.bagY = 300;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/speedAnagrams.png',
            sizeX : 800,
            sizeY : 600
        };
        
        this.assets['letters'] = {
            type : 'spritesheet',
            name : 'letters',
            url : 'assets/pieces/letters.png',
            sizeX : 33,
            sizeY : 33
        };
        
        this.assets['button'] = {
            type: 'spritesheet',
            name: 'button',
            url: 'assets/button.png',
            sizeX: 104,
            sizeY: 31
        };
        
        this.backgrounds['board'] = {
            name: 'board',
            assetName: 'board',
            x: 400,
            y: 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x: 0,
            y: 520,
            width: 520,
            height:750,
            playerNumber : -1
        };
        this.specialAreas['board1'] = {
            type: 'reveal',
            x: 0,
            y: 0,
            width: 390,
            height: 600,
            playerNumber: null
        };
        this.specialAreas['board2'] = {
            type: 'reveal',
            x: 410,
            y: 0,
            width: 390,
            height: 600,
            playerNumber: null
        };
        this.specialAreas['boardSnap'] = {
            type: 'snap',
            x: 0,
            y: 0,
            width: 800,
            height: 600,
            snapX: 34.5,
            snapY: 34.5
        };
        this.specialAreas['bag'] = {
            type: 'shuffle',
            x: this.bagX - 20,
            y: this.bagY - 20,
            width: 40,
            height: 40
        };
        this.specialAreas['tileCounter'] = {
            type: 'counter',
            x: this.bagX,
            y: this.bagY,
            textX: this.bagX-12,
            textY: this.bagY+20
        }
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 400,
            y: 20
        };
        
        this.addCrossedWordsLetter('A',13);
        this.addCrossedWordsLetter('B',3);
        this.addCrossedWordsLetter('C',3);
        this.addCrossedWordsLetter('D',6);
        this.addCrossedWordsLetter('E',18);
        this.addCrossedWordsLetter('F',3);
        this.addCrossedWordsLetter('G',4);
        this.addCrossedWordsLetter('H',3);
        this.addCrossedWordsLetter('I',12);
        this.addCrossedWordsLetter('J',2);
        this.addCrossedWordsLetter('K',2);
        this.addCrossedWordsLetter('L',5);
        this.addCrossedWordsLetter('M',3);
        this.addCrossedWordsLetter('N',8);
        this.addCrossedWordsLetter('O',11);
        this.addCrossedWordsLetter('P',3);
        this.addCrossedWordsLetter('Q',2);
        this.addCrossedWordsLetter('R',9);
        this.addCrossedWordsLetter('S',6);
        this.addCrossedWordsLetter('T',9);
        this.addCrossedWordsLetter('U',6);
        this.addCrossedWordsLetter('V',3);
        this.addCrossedWordsLetter('W',3);
        this.addCrossedWordsLetter('X',2);
        this.addCrossedWordsLetter('Y',3);
        this.addCrossedWordsLetter('Z',2);
            
        return this;
    }

    addCrossedWordsLetter( letter, count ) {
        
        for ( var i = 0; i < count; i ++ ) {
            var name = 'letter' + letter + '_' + i;
            var assetNumber = (letter == '_') ? 0 : letter.charCodeAt(0) - 64;
            const piece = {
                name: name,
                assetName: 'letters',
                assetNumber: 0,
                hiddenAssetNumber: 0,
                visibleAssetNumber: assetNumber,
                x: this.bagX,
                y: this.bagY,
                zIndex: this.topMost ++
            };
            this.pieces[name] = piece;
        }
    }
    
    
    initCrossedNumbers() {
        console.log( `Initialising CrossedNumbers` );

        this.instructions = 'Read the rules on <a href="https://www.instructables.com/id/Number-Scrabble-The-Game-aka-Math-Scrabble/">instructables.com</a>';
        
        this.bagX = 620;
        this.bagY = 550;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/crossedWords.png',
            sizeX : 800,
            sizeY : 600
        };
        
        this.assets['tiles'] = {
            type : 'spritesheet',
            name : 'tiles',
            url : 'assets/pieces/numbers.png',
            sizeX : 33,
            sizeY : 33
        };
        
        this.assets['button'] = {
            type: 'spritesheet',
            name: 'button',
            url: 'assets/button.png',
            sizeX: 104,
            sizeY: 31
        };
        
        this.backgrounds['board'] = {
            name: 'board',
            assetName: 'board',
            x: 400,
            y: 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x: 0,
            y: 520,
            width: 520,
            height:750,
            playerNumber : -1
        };
        this.specialAreas['board'] = {
            type: 'reveal',
            x: 0,
            y: 0,
            width: 520,
            height: 520,
            playerNumber: null
        };
        this.specialAreas['boardSnap'] = {
            type: 'snap',
            x: 10,
            y: 10,
            width: 515,
            height: 515,
            snapX: 34.5,
            snapY: 34.5
        };
        this.specialAreas['bag'] = {
            type: 'shuffle',
            x: this.bagX - 20,
            y: this.bagY - 20,
            width: 40,
            height: 40
        };
        this.specialAreas['tileCounter'] = {
            type: 'counter',
            x: this.bagX,
            y: this.bagY,
            textX: this.bagX+40,
            textY: this.bagY-10
        }
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 680,
            y: 20
        };
   
        this.addCrossedNumbersTile('blank', 0, 2);
        this.addCrossedNumbersTile('0', 1, 5);
        this.addCrossedNumbersTile('1', 2, 5);
        this.addCrossedNumbersTile('2', 3, 5);
        this.addCrossedNumbersTile('3', 4, 5);
        this.addCrossedNumbersTile('4', 5, 5);
        this.addCrossedNumbersTile('5', 6, 5);
        this.addCrossedNumbersTile('6', 7, 5);
        this.addCrossedNumbersTile('7', 8, 5);
        this.addCrossedNumbersTile('8', 9, 5);
        this.addCrossedNumbersTile('9', 10, 5);
        this.addCrossedNumbersTile('+', 11, 7);
        this.addCrossedNumbersTile('-', 12, 7);
        this.addCrossedNumbersTile('x', 13, 5);
        this.addCrossedNumbersTile('/', 14, 5);
        this.addCrossedNumbersTile('sqrt', 15, 2);
        this.addCrossedNumbersTile('sqr', 16, 2);
        this.addCrossedNumbersTile('=', 17, 20);
            
        return this;
    }

    addCrossedNumbersTile( letter, assetNumber, count ) {
        
        for ( var i = 0; i < count; i ++ ) {
            var name = 'tile_' + letter + '_' + i;
            const piece = {
                name: name,
                assetName: 'tiles',
                assetNumber: 0,
                hiddenAssetNumber: 0,
                visibleAssetNumber: assetNumber,
                x: this.bagX,
                y: this.bagY,
                zIndex: this.topMost ++
            };
            this.pieces[name] = piece;
        }
    }
    
    initCards() {
        console.log( `Initialising cards` );
        this.deckX = 90;
        this.deckY = 480;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/cards.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['cards'] = {
            type : 'spritesheet',
            name : 'cards',
            url : 'assets/pieces/cards.png',
            sizeX : 79,
            sizeY : 123
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dealer'] = {
            type : 'image',
            name : 'dealer',
            url : 'assets/pieces/dealer.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['leader'] = {
            type : 'image',
            name : 'leader',
            url : 'assets/pieces/leader.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['dealerHidden'] = {
            type : 'image',
            name : 'dealerHidden',
            url : 'assets/pieces/discWhite46.png',
            sizeX : 46,
            sizeY : 46
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x : 180,
            y : 390,
            width : 620,
            height : 212,
            playerNumber : -1
        };
        this.specialAreas['table'] = {
            type: 'reveal',
            x : 0,
            y : 0,
            width : 800,
            height : 390,
            playerNumber : null
        };
        this.specialAreas['deck'] = {
            type: 'bottom',
            assetName: 'cards',
            x : this.deckX - 40,
            y : this.deckY - 60,
            width : 80,
            height : 120,
        };
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX-8,
            textY: this.deckY - 80
        }
        
        this.buttons['shuffle'] = {
            name: 'shuffle',
            text: 'Shuffle',
            action: shuffleAction,
            confirm: true,
            assetName: 'button',
            x: 90,
            y: 560,
        };
        
        for ( var suit = 0; suit < 4; suit ++ ) {
            for ( var value = 0; value < 13; value ++ ) {
                var name = `card_${suit}_${value}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'cards',
                    assetNumber : 52+2,
                    hiddenAssetNumber : 52+2,
                    visibleAssetNumber : suit * 13 + value,
                    x : this.deckX,
                    y : this.deckY,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
            }
        }
        
        this.pieces['dealer'] = {
            name: 'dealer',
            assetName: 'dealer',
            hiddenAssetName: 'dealerHidden',
            visibleAssetName: 'dealer',
            x: 170,
            y: 400,
            zIndex: this.topMost++
        };
        this.pieces['leader'] = {
            name: 'leader',
            assetName: 'leader',
            hiddenAssetName: 'dealerHidden',
            visibleAssetName: 'leader',
            x: 190,
            y: 400,
            zIndex: this.topMost++
        };
        
        return this;
    }
    
    
    initCardsMini() {
        console.log( `Initialising cards (mini)` );
        
        this.deckX = 60;
        this.deckY = 530;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/cardsMini.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['cards'] = {
            type : 'spritesheet',
            name : 'cards',
            url : 'assets/pieces/miniPlayingCards.png',
            sizeX : 41,
            sizeY : 61
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dealer'] = {
            type : 'image',
            name : 'dealer',
            url : 'assets/pieces/dealer.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['dealerHidden'] = {
            type : 'image',
            name : 'dealerHidden',
            url : 'assets/pieces/discWhite46.png',
            sizeX : 46,
            sizeY : 46
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x : 120,
            y : 490,
            width : 800-120,
            height : 600-490,
            playerNumber : -1
        };
        this.specialAreas['table'] = {
            type: 'reveal',
            x : 100,
            y : 100,
            width : 600,
            height : 290,
            playerNumber : null
        };
        this.specialAreas['deck'] = {
            type: 'bottom',
            assetName: 'cards',
            x : this.deckX - 20,
            y : this.deckY - 30,
            width : 40,
            height : 60,
        };
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX + 30,
            textY: this.deckY
        }
        
        this.buttons['shuffle'] = {
            name: 'shuffle',
            text: 'Shuffle',
            action: shuffleAction,
            assetName: 'button',
            confirm: true,
            x: this.deckX,
            y: this.deckY + 55
        };
        
        for ( var suit = 0; suit < 4; suit ++ ) {
            for ( var value = 0; value < 13; value ++ ) {
                var name = `card_${suit}_${value}`;
                this.pieces[name] = {
                    name : name,
                    assetName : 'cards',
                    assetNumber : 52+2,
                    hiddenAssetNumber : 52+2,
                    visibleAssetNumber : suit * 13 + value,
                    x : this.deckX,
                    y : this.deckY,
                    zIndex : this.topMost++,
                    gluedTo : null
                }
            }
        }
        
        this.pieces['dealer'] = {
            name: 'dealer',
            assetName: 'dealer',
            hiddenAssetName: 'dealerHidden',
            visibleAssetName: 'dealer',
            x: 170,
            y: 400,
            zIndex: this.topMost++
        };
        
        return this;
    }
    
    
    initSpiteAndMalice() {
        console.log( `Initialising spite and malice` );
        
        this.instructions = 'You can read the rules from <a href="https://en.wikipedia.org/wiki/Spite_and_Malice">Wikipedia</a>';
        
        this.deckX = 400;
        this.deckY = 455;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/spiteAndMalice.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['cards'] = {
            type : 'spritesheet',
            name : 'cards',
            url : 'assets/pieces/miniPlayingCards.png',
            sizeX : 41,
            sizeY : 61
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dealer'] = {
            type : 'image',
            name : 'dealer',
            url : 'assets/pieces/dealer.png',
            sizeX : 46,
            sizeY : 46
        };
        this.assets['dealerHidden'] = {
            type : 'image',
            name : 'dealerHidden',
            url : 'assets/pieces/discWhite46.png',
            sizeX : 46,
            sizeY : 46
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x : 476,
            y : 530,
            width : 800-476,
            height : 70,
            playerNumber : -1
        };
        this.specialAreas['table'] = {
            type: 'reveal',
            x : 0,
            y : 0,
            width : 800,
            height : 421,
            playerNumber : null
        };
        this.specialAreas['deck'] = {
            type: 'bottom',
            assetName: 'cards',
            x : this.deckX - 20,
            y : this.deckY - 30,
            width : 40,
            height : 60
        };
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX + 30,
            textY: this.deckY
        }
        
        for ( var player = 0; player < 4; player ++ ) {
            this.specialAreas[`pile${player}`] = {
                type: 'bottom',
                assetName: 'cards',
                x: 97 + player * 204 - 30,
                y: 455 - 40,
                width : 60,
                height : 80
            }
            this.specialAreas[`topOfPile${player}`] = {
                type: 'snap',
                x: 97 + player * 204 - 30,
                y: 373 - 40,
                width : 60,
                height : 80,
                snapX: 60,
                snapY: 80
            }
            this.specialAreas[`pileCounter${player}`] = {
                type: 'counter',
                x: 97 + player * 204,
                y: 455,
                textX: 130 + player * 204,
                textY: 451
            };
            this.specialAreas[`columns${player}`] = {
                type: 'snap',
                x: 10 + player * 200,
                y: 133,
                width: 180,
                height: 210,
                snapX: 45,
                snapY: 30
            };
        }
        
        
        for ( var deck = 0; deck < 4; deck ++ ) {
            for ( var suit = 0; suit < 4; suit ++ ) {
                for ( var value = 0; value < 13; value ++ ) {
                    var name = `card_${deck}_${suit}_${value}`;
                    this.pieces[name] = {
                        name : name,
                        assetName : 'cards',
                        assetNumber : 52+2,
                        hiddenAssetNumber : 52+2,
                        visibleAssetNumber : suit * 13 + value,
                        x : this.deckX,
                        y : this.deckY,
                        zIndex : this.topMost++,
                        gluedTo : null
                    }
                }
            }
        }
        
        
        this.buttons['shuffle'] = {
            name: 'shuffle',
            text: 'Shuffle',
            action: shuffleAction,
            confirm: true,
            assetName: 'button',
            x: this.deckX,
            y: this.deckY + 75
        };
        
        this.buttons['restock'] = {
            name: 'restock',
            text: 'Restock',
            action: createMoveGroupAction('discard', 'deck', true),
            assetName : 'button',
            x: 194,
            y: 590
        };
        
        this.specialAreas['discard'] = {
            type: 'insert',
            assetName: 'cards',
            x: 194 - 30,
            y: 544 - 40,
            width: 60,
            height: 80,
        };
        this.specialAreas['discardCounter'] = {
            type: 'counter',
            x: 194,
            y: 544,
            textX: 194 + 26,
            textY: 538
        };

        for ( var stack = 0; stack < 4; stack ++ ) {
            this.specialAreas[`stack${stack}`] = {
                type: 'snap',
                x: 237 + stack * 108 - 30,
                y: 71 - 40,
                width: 60,
                height: 80,
                snapX: 60,
                snapY: 80
            }
            
            this.buttons[`discard${stack}`] = {
                name: `discard${stack}`,
                text: 'Discard',
                action: createMoveGroupAction(`stack${stack}`, 'discard', true ),
                assetName: 'button',
                x: 237 + stack * 108,
                y: 15
            }
        }
        
        this.pieces['dealer'] = {
            name: 'dealer',
            assetName: 'dealer',
            hiddenAssetName: 'dealerHidden',
            visibleAssetName: 'dealer',
            x: 158,
            y: 377,
            zIndex: this.topMost++
        };
        
        return this;
    }
    
    initRisk() {

        this.deckX = 330;
        this.deckY = 520;
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/risk.jpg',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['troops1'] = {
            type : 'spritesheet',
            name : 'troops1',
            url : 'assets/pieces/troops1.png',
            sizeX : 14,
            sizeY : 177/7
        };
        this.assets['troops5'] = {
            type : 'spritesheet',
            name : 'troops5',
            url : 'assets/pieces/troops5.png',
            sizeX : 27,
            sizeY : 177/7
        };
        this.assets['troops10'] = {
            type : 'spritesheet',
            name : 'troops10',
            url : 'assets/pieces/troops10.png',
            sizeX : 26,
            sizeY : 177/7
        };
        this.assets['cards'] = {
            type: 'spritesheet',
            name: 'cards',
            url: 'assets/pieces/riskCards.png',
            sizeX : 94,
            sizeY: 150
        };
        this.assets['dice'] = {
            type: 'spritesheet',
            name: 'dice',
            url: 'assets/pieces/dice.png',
            sizeX : 42,
            sizeY: 38
        };
        this.assets['diceBlue'] = {
            type: 'spritesheet',
            name: 'diceBlue',
            url: 'assets/pieces/diceBlue.png',
            sizeX : 42,
            sizeY: 38
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.specialAreas['hand'] = {
            type: 'reveal',
            x : 0,
            y : 100,
            width : 80,
            height : 500,
            playerNumber : -1
        };
        this.specialAreas['show'] = {
            type: 'reveal',
            x : 400,
            y : 0,
            width : 500,
            height : 600,
            playerNumber : null
        };
        this.specialAreas['deck'] = {
            type: 'bottom',
            assetName: 'cards',
            x : this.deckX-40,
            y : this.deckY-60,
            width : 80,
            height : 120
        };
        this.specialAreas['deckCounter'] = {
            type: 'counter',
            color: '#000',
            x: this.deckX,
            y: this.deckY,
            textX: this.deckX-8,
            textY: this.deckY - 90
        }
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 720,
            y: 570
        };
        
        this.topMost = 0;
        
        // Cards
        for (var c = 0; c < 9*5-1; c ++) {
            var name = `card_${c}`;
            this.pieces[name] = {
                name: name,
                assetName: 'cards',
                assetNumber: c,
                hiddenAssetNumber : 9*5-1,
                visibleAssetNumber : c,
                x : this.deckX,
                y : this.deckY,
                zIndex: this.topMost++
            };
        }
        this.topMost = 200;
        
        // Troop reserves
        for ( var p = 0; p < 6; p ++ ) {
            for ( var i = 0; i < 40; i ++ ) {
                var name = `troops1_${p}_${i}`;
                this.pieces[name]={
                    name: name,
                    assetName: 'troops1',
                    assetNumber: p,
                    x: 548,
                    y: 420 + p * 30,
                    zIndex: this.topMost++
                };
            }
            
            for ( var i = 0; i < 10; i ++ ) {
                var name = `troops5_${p}_${i}`;
                this.pieces[name] = {
                    name: name,
                    assetName: 'troops5',
                    assetNumber: p,
                    x: 576,
                    y: 420 + p * 30,
                    zIndex: this.topMost++
                };
            }
            
            for ( var i = 0; i < 10; i ++ ) {
                var name = `troops10_${p}_${i}`;
                this.pieces[name]={
                    name: name,
                    assetName: 'troops10',
                    assetNumber: p,
                    x: 610,
                    y: 420 + p * 30,
                    zIndex: this.topMost++
                };
            }
            
        }
        
        
        this.pieces['dice_0'] = {
            name : 'dice_0',
            assetName : 'dice',
            assetNumber : 0,
            x : 400,
            y : 547,
            zIndex: this.topMost++
        };
        this.pieces['dice_1'] = {
            name : 'dice_1',
            assetName : 'dice',
            assetNumber : 0,
            x : 450,
            y : 547,
            zIndex: this.topMost++
        };
        this.pieces['dice_2'] = {
            name : 'dice_2',
            assetName : 'dice',
            assetNumber : 0,
            x : 500,
            y : 547,
            zIndex: this.topMost++
        };
        this.buttons['attack'] = {
            name: 'attack',
            text: 'Throw',
            action: throwAction,
            assetName: 'button',
            x: 450,
            y: 587
        };
    
        this.pieces['dice_3'] = {
            name : 'dice_3',
            assetName : 'diceBlue',
            assetNumber : 0,
            x : 150,
            y : 547,
            zIndex: this.topMost++
        };
        this.pieces['dice_4'] = {
            name : 'dice_4',
            assetName : 'diceBlue',
            assetNumber : 0,
            x : 200,
            y : 547,
            zIndex: this.topMost++
        };
        this.buttons['defend'] = {
            name: 'defend',
            text: 'Throw',
            action: throwBlueAction,
            assetName: 'button',
            x: 175,
            y: 587
        };
        this.reset();
        
        return this;
    }

   
    initChineseCheckers() {
    
       this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/chineseCheckers.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 105,
            sizeY : 31
        };
        this.assets['spheres'] = {
            type : 'spritesheet',
            name : 'spheres',
            url : 'assets/pieces/spheres.png',
            sizeX : 37.7,
            sizeY : 38
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 720,
            y: 570
        };
        
        
        this.specialAreas['grid'] = {
            type: 'snap',
            x: 12,
            y: 16,
            width: 800,
            height: 600,
            snapX: 21,
            snapY: 21
        };
        
        for ( var x = 0; x < 5; x ++ ) {
            for (var y = 0; y < x; y ++ ) {
                this.addCCPiece( x*2 - 18, y*2 - x +1, 0 );
                this.addCCPiece( -x*2, y*2 - x -8, 1 );
                this.addCCPiece( x*2, y*2 - x -8, 2 );

                this.addCCPiece( 18 - x*2, y*2 - x +1, 3 );
                this.addCCPiece( x*2, y*2 - x + 10, 4 );
                this.addCCPiece( -x*2, y*2 - x + 10, 5 );

            }
        }
        
        for ( var i = 0; i < 5; i ++ ) {
            this.addCCPiece( -15, 4, 0 ); 
            this.addCCPiece( -11, -10, 1 ); 
            this.addCCPiece( 4, -13, 2 ); 
            this.addCCPiece( 15, -4, 3 ); 
            this.addCCPiece( 11, 10, 4 ); 
            this.addCCPiece( -4, 13, 5 ); 
        }
        
        return this;
    }
    
    addCCPiece( x, y, assetNumber ) {
        var name = `piece${this.topMost}`;
        this.pieces[name] = {
            name: name,
            assetName: 'spheres',
            assetNumber: assetNumber,
            x: 400 + x * 21,
            y: 300 + y * 21,
            zIndex: this.topMost++
        };
    }
    
    initConnectFour() {
    
       this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/connectFour.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['blue'] = {
            type: 'image',
            name: 'blue',
            url : 'assets/pieces/discBlue72.png',
            sizeX: 72,
            sizeY: 72
        }
        this.assets['red'] = {
            type: 'image',
            name: 'red',
            url : 'assets/pieces/discRed72.png',
            sizeX: 72,
            sizeY: 72
        }
        
         this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName : 'button',
            x: 400,
            y: 30
        };
        
        this.specialAreas['grid'] = {
            type: 'snap',
            x: 120,
            y: 70,
            width: 80*7,
            height: 80*6,
            snapX: 80,
            snapY: 80
        };
        
        for ( var i = 0; i < 21; i ++ ) {
            name = `red${i}`;
            this.pieces[name] = {
                name: name,
                assetName: 'red',
                x: 50,
                y: 500
            };
            var name = `blue${i}`;
            this.pieces[name] = {
                name: name,
                assetName: 'blue',
                x: 800-50,
                y: 500
            };
        }
        
        return this;
    }
    
    initSuperSeven() {

        this.instructions = 'Read the <a href="instructions/superSeven.html">Rules of Super7</a>.';
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/superSeven.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['ballBigBlue'] = {
            type : 'image',
            name : 'ballBigBlue',
            url : 'assets/pieces/ballBigBlue.png',
            sizeX : 143,
            sizeY : 143
        };
        this.assets['ballBigRed'] = {
            type : 'image',
            name : 'ballBigRed',
            url : 'assets/pieces/ballBigRed.png',
            sizeX : 143,
            sizeY : 143
        };
        this.assets['ballBlue'] = {
            type : 'image',
            name : 'ballBlue',
            url : 'assets/pieces/ballBlue.png',
            sizeX : 51,
            sizeY : 51
        };
        this.assets['ballRed'] = {
            type : 'image',
            name : 'ballRed',
            url : 'assets/pieces/ballRed.png',
            sizeX : 51,
            sizeY : 51
        };
        
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        this.assets['dice'] = {
            type: 'spritesheet',
            name: 'dice',
            url: 'assets/pieces/dice.png',
            sizeX : 42,
            sizeY: 38
        };
        
        this.specialAreas['grid'] = {
            type: 'snap',
            x: 30,
            y: 30,
            width: 540,
            height: 540,
            snapX: 60,
            snapY: 60
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        for ( var i = 0; i < 50; i ++ ) {
            this.pieces[`blue_${i}`] = {
                name: `blue_${i}`,
                assetName: 'ballBlue',
                x: 730,
                y: 332,
                zIndex: this.topMost++
            };
            this.pieces[`red_${i}`] = {
                name: `red_${i}`,
                assetName: 'ballRed',
                x: 630,
                y: 332,
                zIndex: this.topMost++
            };
        }
        for ( var i = 0; i < 5; i ++ ) {
            this.pieces[`bigBlue_${i}`] = {
                name: `bigBlue_${i}`,
                assetName: 'ballBigBlue',
                x: 658,
                y: 450,
                zIndex: this.topMost++
            };
            this.pieces[`bigRed_${i}`] = {
                name: `bigRed_${i}`,
                assetName: 'ballBigRed',
                x: 715,
                y: 450,
                zIndex: this.topMost++
            };
        }
        
        this.pieces['dice_0'] = {
            name: 'dice_0',
            assetName: 'dice',
            assetNumber: 0,
            x: 652,
            y: 246,
            zIndex: this.topMost++
        };
        this.pieces['dice_1'] = {
            name: 'dice_1',
            assetName: 'dice',
            assetNumber: 0,
            x: 705,
            y: 246,
            zIndex: this.topMost++
        };
        
        this.buttons['throw'] = {
            name: 'throw',
            text: 'Throw',
            action: throwAction,
            assetName: 'button',
            x: 680,
            y: 285
        };
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 680,
            y: 570
        };
        
        return this;
    }

    initHex() {

        this.instructions = 'Read the <a href="https://en.wikipedia.org/wiki/Hex_(board_game)">Rules of Hex</a>.';
        
        this.assets['board'] = {
            type : 'image',
            name : 'board',
            url : 'assets/backgrounds/hex.png',
            sizeX : 800,
            sizeY : 600
        };
        this.assets['ballBlue'] = {
            type : 'image',
            name : 'ballBlue',
            url : 'assets/pieces/ballBlue.png',
            sizeX : 51,
            sizeY : 51
        };
        this.assets['ballRed'] = {
            type : 'image',
            name : 'ballRed',
            url : 'assets/pieces/ballRed.png',
            sizeX : 51,
            sizeY : 51
        };
        
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        
        this.specialAreas['grid'] = {
            type: 'snap',
            x: 19,
            y: 66,
            width: 800,
            height: 500,
            snapX: 24.62,
            snapY: 42.95
        };
        
        this.backgrounds['board'] = {
            name : 'board',
            assetName : 'board',
            x : 400,
            y : 300
        };
        
        for ( var i = 0; i < 30; i ++ ) {
            this.pieces[`blue1_${i}`] = {
                name: `blue1_${i}`,
                assetName: 'ballBlue',
                x: 127,
                y: 346,
                zIndex: this.topMost++
            };
            this.pieces[`blue2_${i}`] = {
                name: `blue2_${i}`,
                assetName: 'ballBlue',
                x: 675,
                y: 259,
                zIndex: this.topMost++
            };
            this.pieces[`red1_${i}`] = {
                name: `red1_${i}`,
                assetName: 'ballRed',
                x: 303,
                y: 43,
                zIndex: this.topMost++
            };
            this.pieces[`red2_${i}`] = {
                name: `red2_${i}`,
                assetName: 'ballRed',
                x: 498,
                y: 561,
                zIndex: this.topMost++
            };
        }
        
        this.buttons['reset'] = {
            name: 'reset',
            text: 'Reset',
            action: resetAction,
            confirm: true,
            assetName: 'button',
            x: 720,
            y: 567
        };
        
        return this;
    }
    
    
    initCharades() {

        this.instructions = 'Use video conferencing!';
        
        this.assets['button'] = {
            type : 'spritesheet',
            name : 'button',
            url : 'assets/button.png',
            sizeX : 104,
            sizeY : 31
        };
        
        this.buttons['random'] = {
            name: 'random',
            text: 'Random',
            action: takeRandomCharadeAction,
            confirm: false,
            assetName: 'button',
            x: 400,
            y: 300
        };
        
        
        this.buttons['film'] = {
            name: 'film',
            text: 'Film',
            action: takeFilmCharadeAction,
            confirm: false,
            assetName: 'button',
            x: 100,
            y: 400
        };
        
        this.buttons['song'] = {
            name: 'song',
            text: 'Song',
            action: takeSongCharadeAction,
            confirm: false,
            assetName: 'button',
            x: 300,
            y: 400
        };


        this.buttons['tv'] = {
            name: 'tv',
            text: 'TV Show',
            action: takeTVCharadeAction,
            confirm: false,
            assetName: 'button',
            x: 500,
            y: 400
        };

        this.buttons['book'] = {
            name: 'book',
            text: 'Book',
            action: takeBookCharadeAction,
            confirm: false,
            assetName: 'button',
            x: 700,
            y: 400
        };
        
        return this;
    }
    
    reset() {
        var prototype = this.prototype;
        if (prototype == null) return;
        
        for (var id in this.pieces) {
            var piece = this.pieces[id];
            var prototypePiece = prototype.pieces[id];
            
            piece.ownedByPlayerNumber = null;
            piece.gluedTo = null;
            piece.draggingBy = null;

            if (prototypePiece) {
                if (prototypePiece.isPermanent != true) {
                    piece.x = prototypePiece.x;
                    piece.y = prototypePiece.y;
                }
                if (prototypePiece.assetNumber !== undefined) {
                    piece.assetNumber = prototypePiece.assetNumber;
                }
            }
        }
        this.shuffle();
        
        return this.pieces;
    }
    
    shuffle() {
        var prototype = this.prototype;
        if (prototype == null) return;
        
        var pieces = {};
        var zIndices = new Array();
        
        // Find the pieces that need to be shuffled, and store all the old zIndicies in
        // the zIndices array.
        // The pieces are also moved back to their original positions.
        for ( var id in this.pieces ) {
            var piece = this.pieces[id];
            var prototypePiece = prototype.pieces[id];

            if (piece.assetName == 'cards' || piece.assetName == 'letters' || piece.assetName == 'tiles' ) {
                pieces[id] = piece;
                piece.ownedByPlayerNumber = null;
                pieces[piece.name] = piece;
                zIndices[zIndices.length] = piece.zIndex;
                piece.x = prototypePiece.x;
                piece.y = prototypePiece.y;
            }
            
        }
        
        // The second pass changes the zIndex of each of the pieces found in the first pass.
        for (id in pieces) {
            var piece = pieces[id];
            var rnd = getRandomInt(zIndices.length);
            piece.zIndex = zIndices[rnd];
            if (rnd < zIndices.length -1) {
                zIndices[rnd] = zIndices.pop();
            } else {
                zIndices.pop();
            }
        }
        return pieces;
    }

    toShuffleArea( area, piece ) {
        piece.x = area.x + area.width/2;
        piece.y = area.y + area.height/2;
        
        return this.shuffleArea( area );
    }
    
    shuffleArea( area ) {
        const pieces = {};
        const x = area.x + area.width/2;
        const y = area.y + area.height/2;

        var count = 0;
        for ( var id in this.pieces ) {
            var p = this.pieces[id];
            if (p.x == x && p.y == y) count ++
        }
        for ( var id in this.pieces ) {
            var p = this.pieces[id];
            if (p.x == x && p.y == y) {
                pieces[p.name] = p;
                p.zIndex = getRandomInt( count );
            }
        }
        return pieces;
    }
    
    toBottomArea( area, piece ) {
        piece.x = area.x + area.width/2;
        piece.y = area.y + area.height/2;
        var pieces = {};

        piece.zIndex = -1;
        for ( var id in this.pieces ) {
            var p = this.pieces[id];
            if (p.x == piece.x && p.y == piece.y) {
                pieces[p.name] = p;
                p.zIndex ++;
            }
        }
        return pieces;
    }
    
    toInsertArea( area, piece ) {

        const x = area.x + area.width/2;
        const y = area.y + area.height/2;
        var pieces = {};

        for ( var id in this.pieces ) {
            var p = this.pieces[id];
            if (p.x == x && p.y == y) {
                pieces[p.name] = p;
            }
        }
        // Find a random piece in the area
        if (Object.keys(pieces).length > 0) {
            const rnd = getRandomInt( Object.keys(pieces).length );
            const otherPiece = pieces[Object.keys(pieces)[rnd]]

            if (otherPiece) {
                // Swap the z-index with a random piece from the pile.
                const temp = piece.zIndex;
                piece.zIndex = otherPiece.zIndex;
                otherPiece.zIndex = temp;
            }
        }
        
        piece.x = x;
        piece.y = y;
        pieces[piece.name] = piece;
        return pieces;
    }
    
    throwDice(assetName) {
        if (!assetName) assetName = 'dice';
        var dice = {};
        for ( var id in this.pieces ) {
            var piece = this.pieces[id];
            if (piece.assetName == assetName) {
                piece.assetNumber = getRandomInt(6);
                dice[piece.name] = piece;
            }
        }
        return dice;
    }
    
    moveGroup(fromAreaName, toAreaName, shuffle) {
        const fromArea = this.specialAreas[fromAreaName];
        const toArea = this.specialAreas[toAreaName];

        const fromX = fromArea.x + fromArea.width/2;
        const fromY = fromArea.y + fromArea.height/2;
        const toX = toArea.x + toArea.width/2;
        const toY = toArea.y + toArea.height/2;

        const pieces = {};
        for ( var id in this.pieces ) {
            var piece = this.pieces[id];
            if (piece.x == fromX && piece.y == fromY) {
                pieces[piece.name] = piece;
                piece.x = toX;
                piece.y = toY;
            }
        }
        
        if ( shuffle ) {
            return this.shuffleArea( toArea );
        }
        
        return pieces;
    }

    isPlayerNumberUsed( playerNumber ) {
        for ( var id in this.players ) {
            if ( this.players[id].playerNumber == playerNumber ) {
                return true;
            }
        }
        return false;
    }

    findPlayerNumber() {
        for ( var i=0; i < 20; i ++ ) {
            if (this.isPlayerNumberUsed(i)) {
                continue;
            }
            return i;
        }
        return 0;
    }
    
    
    findGlueTo( piece, x, y ) {
        var gluesTo = this.glue[ piece.assetName ];
        if (gluesTo) {
            var minDist2 = 100000;
            var result = null;
            
            for (const pName in this.pieces) {
                const p = this.pieces[pName];
                if (p !== piece && gluesTo.includes(p.assetName)) {
                    const d2 = (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y);
                    if ( d2 < minDist2 && d2 < this.minSnap2 ) {
                        minDist2 = d2;
                        result = p;
                    }
                }
            }
        }
        return result;
    }

        
    findRevealArea( x, y ) {
        for ( var id in this.specialAreas ) {
            const ra = this.specialAreas[id];
            if ( ra.type == 'reveal' && x < ra.x + ra.width && x >= ra.x && y < ra.y + ra.height && y >= ra.y ) {
                return ra
            }
        }
        return null;
    }

    findShuffleArea( x, y ) {
        for ( var id in this.specialAreas ) {
            const ra = this.specialAreas[id];
            if ( ra.type == 'shuffle' && x < ra.x + ra.width && x >= ra.x && y < ra.y + ra.height && y >= ra.y ) {
                return ra
            }
        }
        return null;
    }
    
    findBottomArea( piece ) {
        for ( var id in this.specialAreas ) {
            const ra = this.specialAreas[id];
            if ( ra.type == 'bottom'
                && ra.assetName == piece.assetName
                && piece.x < ra.x + ra.width && piece.x >= ra.x
                && piece.y < ra.y + ra.height && piece.y >= ra.y ) {
                return ra
            }
        }
        return null;
    }
    findInsertArea( piece ) {
        for ( var id in this.specialAreas ) {
            const ra = this.specialAreas[id];
            if ( ra.type == 'insert'
                && ra.assetName == piece.assetName
                && piece.x < ra.x + ra.width && piece.x >= ra.x
                && piece.y < ra.y + ra.height && piece.y >= ra.y ) {
                return ra
            }
        }
        return null;
    }
    
    createNameTags() {
        const newPieces = {};
        const changedPieces = {};
        for ( var id in this.players ) {
            var player = this.players[id];
           
            if (!player.isSpectator) {
                
                var existing = this.pieces[`nameTag${player.playerNumber}`];
                if (existing) {
                    changedPieces[existing.name] = existing;
                    existing.text = player.name;
                } else {
                    var piece = {
                        name: `nameTag${player.playerNumber}`,
                        assetName: 'nameTag',
                        text: player.name,
                        textStyle: { fontFamily: 'Arial', fontSize: '16px' },
                        x: 100,
                        y: 100 + player.playerNumber * 50,
                        zIndex: this.topMost++
                    };
                    this.pieces[piece.name] = piece;
                    newPieces[piece.name] = piece;
                }
            }
        }
        io.to(this.id).emit( 'addPieces', newPieces );
        io.to(this.id).emit( 'movePieces', changedPieces );
    }
    
    createXmasDecorations() {
        
        const newAssets = {};
        const newPieces = {};
        const x = 200;
        const y = 200;
        
        
        newAssets['baubles1'] = {
            type : 'image',
            name : 'baubles1',
            url : 'assets/pieces/xmas/baubles1.png',
            sizeX : 200,
            sizeY : 78
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'baubles1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['holly1'] = {
            type : 'image',
            name : 'holly1',
            url : 'assets/pieces/xmas/holly1.png',
            sizeX : 150,
            sizeY : 100
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'holly1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['merry1'] = {
            type : 'image',
            name : 'merry1',
            url : 'assets/pieces/xmas/merry1.png',
            sizeX : 200,
            sizeY : 130
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'merry1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        
        newAssets['reindeer1'] = {
            type : 'image',
            name : 'reindeer1',
            url : 'assets/pieces/xmas/reindeer1.png',
            sizeX : 100,
            sizeY : 143
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'reindeer1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        
        newAssets['reindeer2'] = {
            type : 'image',
            name : 'reindeer2',
            url : 'assets/pieces/xmas/reindeer2.png',
            sizeX : 130,
            sizeY : 138
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'reindeer2',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['santa1'] = {
            type : 'image',
            name : 'santa1',
            url : 'assets/pieces/xmas/santa1.png',
            sizeX : 92,
            sizeY : 110
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'santa1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };

        
        newAssets['santa2'] = {
            type : 'image',
            name : 'santa2',
            url : 'assets/pieces/xmas/santa2.png',
            sizeX : 150,
            sizeY : 134
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'santa2',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['snowman1'] = {
            type : 'image',
            name : 'snowman1',
            url : 'assets/pieces/xmas/snowman1.png',
            sizeX : 90,
            sizeY : 125
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'snowman1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['tree1'] = {
            type : 'image',
            name : 'tree1',
            url : 'assets/pieces/xmas/tree1.png',
            sizeX : 150,
            sizeY : 231
        };
        newPieces[`xmas${this.topMost}`] = {
            name: `xmas${this.topMost}`,
            assetName: 'tree1',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        

        for (var name in newPieces) {
            this.pieces[name] = newPieces[name];
        }
        for (var name in newAssets) {
            this.assets[name] = newAssets[name];
        }

        io.to(this.id).emit( 'assets', newAssets );
        io.to(this.id).emit( 'addPieces', newPieces );
        io.to(this.id).emit( 'movePieces', this.pieces );

    }
    
    
    createBirthdayDecorations() {
        
        const newAssets = {};
        const newPieces = {};
        const x = 200;
        const y = 200;
                
        newAssets['balloons'] = {
            type : 'image',
            name : 'balloons',
            url : 'assets/pieces/birthday/balloons.png',
            sizeX : 194,
            sizeY : 243
        };
        newPieces[`birthday${this.topMost}`] = {
            name: `birthday${this.topMost}`,
            assetName: 'balloons',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['banner'] = {
            type : 'image',
            name : 'banner',
            url : 'assets/pieces/birthday/banner.png',
            sizeX : 400,
            sizeY : 253
        };
        newPieces[`birthday${this.topMost}`] = {
            name: `birthday${this.topMost}`,
            assetName: 'banner',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        newAssets['cake'] = {
            type : 'image',
            name : 'cake',
            url : 'assets/pieces/birthday/cake.png',
            sizeX : 250,
            sizeY : 238
        };
        newPieces[`birthday${this.topMost}`] = {
            name: `birthday${this.topMost}`,
            assetName: 'cake',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        
        newAssets['popper'] = {
            type : 'image',
            name : 'popper',
            url : 'assets/pieces/birthday/popper.png',
            sizeX : 208,
            sizeY : 189
        };
        newPieces[`birthday${this.topMost}`] = {
            name: `birthday${this.topMost}`,
            assetName: 'popper',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        
        
        newAssets['presents'] = {
            type : 'image',
            name : 'presents',
            url : 'assets/pieces/birthday/presents.png',
            sizeX : 250,
            sizeY : 213
        };
        newPieces[`birthday${this.topMost}`] = {
            name: `birthday${this.topMost}`,
            assetName: 'presents',
            x: x,
            y: y,
            zIndex: this.topMost++
        };
        

        for (var name in newPieces) {
            this.pieces[name] = newPieces[name];
        }
        for (var name in newAssets) {
            this.assets[name] = newAssets[name];
        }

        io.to(this.id).emit( 'assets', newAssets );
        io.to(this.id).emit( 'addPieces', newPieces );
        io.to(this.id).emit( 'movePieces', this.pieces );

        console.log(`Adding assets ${Object.keys(newAssets).length}`);
        console.log(`addPieces ${Object.keys(newPieces).length}`);
    }
    
};

console.log( 'Creating GameTypes' );

addGameTypeVariations( 'cards', 'Cards', [
    new Game( 'cards', 'Large Cards' ).initCards(),
    new Game( 'cardsMini', 'Small Cards' ).initCardsMini(),
    new Game( 'spiteAndMalice', 'Spite and Malice' ).initSpiteAndMalice()
]);
addGameTypeVariations( 'twoPlayerCards', 'Two Player Cards', [
    new Game( 'twoPlayerCards', 'Large Cards' ).initTwoPlayerCards(),
    new Game( 'twoPlayerCardsMini', 'Small Cards' ).initTwoPlayerCardsMini() ,
    new Game( 'cribbage', 'Cribbage' ).initCribbage()
]);

addGameType( new Game( 'draughts', 'Draughts' ).initDraughts() );
addGameType( new Game( 'chess', 'Chess' ).initChess() );
addGameTypeVariations( 'crossedWords', 'Crossed Words', [
    new Game( 'crossedWords', 'Crossed Words' ).initCrossedWords(),
    new Game( 'crossedNumbers', 'Crossed Numbers' ).initCrossedNumbers(),
    new Game( 'speedAnagrams', 'Speed Anagrams' ).initSpeedAnagrams()
]);

addGameType( new Game( 'backgammon', 'Backgammon' ).initBackgammon() );
addGameType( new Game( 'risk', 'Risk' ).initRisk() );
addGameType( new Game( 'chineseCheckers', 'Chinese Checkers' ).initChineseCheckers() );
addGameType( new Game( 'connectFour', 'Connect Four' ).initConnectFour() );
addGameType( new Game( 'superSeven', 'Super 7' ).initSuperSeven() );
addGameType( new Game( 'hex', 'Hex' ).initHex() );

addGameType( new Game( 'charades', 'Charades' ).initCharades() );

console.log( 'Created GameTypes' );

function shallowCopyObjectMap( sourceMap, destinationMap ) {
    for (var key in sourceMap) {
        var item = sourceMap[key];
        var newItem = {};
        for (var id in item) {
            newItem[id] = item[id];
        }
        destinationMap[key] = newItem;
    }
}

function findGame( socket ) {
    if (socket.gameId) {
        return games[socket.gameId];
    }
    const rooms = Object.keys(socket.rooms);
    const gameId = rooms[1];
    return games[gameId];
}

function debugGameNotFound( socket, name ) {
    console.log( `User id ${socket.id}. Failed to find game (${name}).` );
    console.log( `Finding game from socket id : ${socket.id}` );
    console.log( `Room count : ${Object.keys(socket.rooms).length}`);
    console.log( `2nd room id : ${Object.keys(socket.rooms)[1]}`);
    console.log( `Game ? ${games[Object.keys(socket.rooms)[1]]}`);
}


io.on('connection', function (socket) {
    
    socket.on('listGameTypes', function(data) {
        var gameTypesInfo = {};
        for ( var id in gameTypes ) {
            var gameType = gameTypes[id];
            gameTypesInfo[id] = {
                id: id,
                name: gameType.name,
                gameCount: Object.keys(gameType.instances).length
            };
        }
        socket.emit('gameTypes', gameTypesInfo );
    });
    
    socket.on('lobby', function(data) {
        const lobbyData = createLobbyData( data.gameTypeId );
        socket.join( data.gameTypeId );

        socket.emit('lobby', lobbyData );
    });
    
    socket.on('newGame', function(data) {
        const gameTypeId = data.gameTypeId;
        const variationId = data.variationId;
        const gameType = gameTypes[gameTypeId];
        if (gameType) {
            var game = gameType.newGame(variationId, data.name);
            if ( game ) {
                game.maxPlayers = data.playerCount;
                socket.emit( 'startGame', {gameId: game.id} );
                io.to(gameType.id).emit('lobby', createLobbyData(gameTypeId) );
            }
        }
    });
    
    socket.on( 'game' , function(data) {
        const gameId = data.gameId;
        
        const game = games[ gameId ];
        if (!game) return;
        socket.gameId = gameId;
        var playerNumber = game.findPlayerNumber();
        var playerName = '';
        if ( playerNumber >= game.maxPlayers ) {
            playerName = `Spectator${playerNumber-game.maxPlayers+1}`;
        } else {
            playerName = `Player${playerNumber + 1}`;
        }
        const player = {
            id: socket.id,
            name: playerName,
            playerNumber: playerNumber,
            isSpectator: playerNumber >= game.maxPlayers
        };
        if ( game.isPublic || !player.isSpectator ) {
            socket.join( gameId );
            game.players[socket.id] = player;
        }
    
        socket.emit('gameInfo', {
            name: game.name,
            instructions: game.instructions,
            maxPlayers: game.maxPlayers,
            playerCount: Object.keys(game.players).length
        });
        socket.emit('playerInfo', game.players[socket.id] );
        socket.emit('assets', game.assets );
        socket.emit('backgrounds', game.backgrounds);
        socket.emit('pieces', game.pieces);
        socket.emit('buttons', game.buttons);
        socket.emit('specialAreas', game.specialAreas);
        
        io.to(game.id).emit('connected', player);
    });
    

    socket.on('disconnect', function () {
        const game = findGame( socket );
        if (!game) {
            //console.log(`User ${socket.id} disconnected from game ???`);
            //debugGameNotFound(socket, 'disconnect');
            return;
        }

        const player = game.players[socket.id];
        if (! player) return;
              
        for ( var id in game.pieces ) {
            var piece = game.pieces[id];
            if (piece.draggingBy == player.playerNumber) {
                piece.draggingBy = null;
                io.to(game.id).emit('movePiece', piece);
            }
        }
        
        io.to(game.id).emit('disconnected', player);
        delete game.players[socket.id];
    });
    
    socket.on('draggingPiece', function (dragInfo) {
        const game = findGame( socket );
        if (! game) return;
              
        game.active();
        const player = game.players[socket.id];
        if (! player) return;
        const playerNumber = player.playerNumber;
        
        const piece = game.pieces[ dragInfo.name ];
        if (!piece) return;

        if (piece.draggingBy != null && piece.draggingBy != player.playerNumber) {
            // Tell the client the REAL position of the piece again!
            socket.emit( 'movePiece', piece );
            return;
        }

        if ( piece.zIndex < game.topMost-1 ) {
            piece.zIndex = game.topMost ++;
        }
        
        if (dragInfo.end==true) {
            piece.draggingBy = null;
        } else {
            piece.draggingBy = playerNumber;
        }
        
        var revealArea = game.findRevealArea( dragInfo.x, dragInfo.y );
        if (revealArea && revealArea.playerNumber == -1) {
            // Reveal it to the player dragging, but hide from all other players.
            piece.ownedByPlayerNumber = playerNumber;
        } else {
            piece.ownedByPlayerNumber = null;
        }
        
        var glueTo = game.findGlueTo( piece, dragInfo.x, dragInfo.y );
        if (glueTo) {
            piece.x = glueTo.x;
            piece.y = glueTo.y;
            if ( dragInfo.end ) {
                piece.gluedTo = glueTo.name;
            }
        } else {
            piece.x = dragInfo.x;
            piece.y = dragInfo.y;
            if ( dragInfo.end ) {
                piece.gluedTo = null;
            }
        }
        
        var shuffledPieces = null;
        if ( dragInfo.end == true ) {
            var area = game.findShuffleArea( dragInfo.x, dragInfo.y );
            if (area) {
                shuffledPieces = game.toShuffleArea( area, piece );
            } else {
                area = game.findBottomArea( piece );
                if (area) {
                    shuffledPieces = game.toBottomArea( area, piece );
                } else {
                    area = game.findInsertArea( piece );
                    if (area) {
                        shuffledPieces = game.toInsertArea( area, piece );
                    }
                }
            }
        }
        
        if ( shuffledPieces ) {
            io.to(game.id).emit('movePieces', shuffledPieces);
        } else {
            io.to(game.id).emit('movePiece', piece);
            for ( var pName in game.pieces ) {
                const p = game.pieces[pName];
                if ( p.gluedTo == piece.name ) {
                    p.draggingBy = piece.draggingBy;
                    p.x = piece.x;
                    p.y = piece.y;
                    if ( p.zIndex < game.topMost-1 ) {
                        p.zIndex = game.topMost ++;
                    }
                    io.to(game.id).emit('movePiece', p);
                }
            }
        }
    });

    socket.on('click', function (clickInfo) {
        const game = findGame(socket);
        if (!game) {
            console.log(`Can't find the game for click message ${clickInfo.name}`);
            debugGameNotFound(socket, 'click');
            return;
        }        

        game.active();
        const button = game.buttons[ clickInfo.name ];
        if (button) {
            button.action(game,socket);
        } else {
            console.log( `click button ${clickInfo.name} not found` );
        }
    });
    
    
    socket.on('highlightPointer', function (pointerInfo) {
        // Note, not safe from script kiddies as pointerInfo is not being checked in any way.
        // In particular, we are trusting their 'isSpectator' flag.
        const game = findGame(socket);
        if (!game) return;
              
        io.to(game.id).emit( 'highlightPointer', pointerInfo );
    });
    
    socket.on('chat', function (chatInfo) {
        const game = findGame( socket );
        if (!game) return;

        game.active();

        const player = game.players[socket.id];
        if (!player) return;

        const message = chatInfo.message;
        if (message == 'private' && !player.isSpectator) {
            game.isPublic = false;
            const gameSockets = io.sockets.adapter.rooms[game.id].sockets;
            io.to(game.id).emit( 'chat', {
                playerNumber: player.playerNumber,
                isSpectator: player.isSpectator,
                name: player.name,
                message: 'The game is now private.'
            });
            for ( var socketId in gameSockets ) {
                var clientSocket = io.sockets.connected[socketId];
                var otherPlayer = game.players[clientSocket.id];
                if (otherPlayer.isSpectator) {
                    // Removing the spectator from the game room, so that they see no
                    // more updates, such as piece movements and chat.
                    clientSocket.leave( game.id );
                    delete( game.players[socketId] );
                }
            }

        }
        if (message == 'public' && !player.isSpectator) {
            game.isPublic = true;
        }
        
        
        if (player.name != chatInfo.name) {
            io.to(game.id).emit('changeName', {
                playerNumber: player.playerNumber,
                isSpectator: player.isSpectator,
                from: player.name,
                to: chatInfo.name
            });
            player.name = chatInfo.name;
        }
        var reply = { 
            playerNumber: player.playerNumber,
            isSpectator: player.isSpectator,
            name : player.name,
            message : message
        };
        io.to(game.id).emit( 'chat', reply );
    });

    socket.on('changeName', function (info) {
        const game = findGame( socket );
        if (!game) return;

        game.active();

        const player = game.players[socket.id];
        if (player.name != info.name) {
            io.to(game.id).emit('changeName', {
                playerNumber: player.playerNumber,
                isSpectator: player.isSpectator,
                from: player.name,
                to: info.name
            });
            player.name = info.name;
        }
    });
    
});

function createLobbyData(gameTypeId) {
    const gameType = gameTypes[gameTypeId];
    if (!gameType) return { id: gameTypeId, name: 'Unknown', variations: [] };
    
    const variations = [];
    for ( var variationIndex = 0; variationIndex < gameType.variations.length; variationIndex ++ ) {
        var variation = gameType.variations[variationIndex];
        variations[variations.length] = { variationId: variation.id, name: variation.name }
    }
    const lobbyData = {
        id: gameTypeId,
        name: gameType.name,
        variations: variations
    };
    
    lobbyData.gameList = gameType.listGames(false);

    return lobbyData;
}

app.use(express.static(__dirname + '/public'));
 
app.get('/', function (req, res) {
    res.sendFile(__dirname + '/index.html');
});


server.listen(8081, function () {
    console.log(`Listening on ${server.address().port}`);
});


function getRandomInt(max) {
  return Math.floor(Math.random() * Math.floor(max));
}