/*
 * NAME
 *   Polydrop
 *
 * DESCRIPTION
 *   Polydrop is a game in the falling blocks family. The blocks in
 *   the game are the twelve pentominos. 
 *
 * VERSION
 *   1.21 -- 2009-01-06
 *
 * AUTHOR
 *   Jonas Bergsten <web@jonasbergsten.com>
 *
 * COPYRIGHT
 *   Polydrop - A simple web based computer game.
 *   Copyright (C) 2008  Jonas Bergsten
 * 
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public License
 *   as published by the Free Software Foundation; either version 2
 *   of the License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  
 *   02111-1307, USA.
 *
 */

/*jslint bitwise:  false,
         browser:  true,
         cap:      false,
         debug:    false,
         eqeqeq:   true,
         evil:     false,
         forin:    false,
         fragment: false,
         laxbreak: false,
         nomen:    true,
         on:       false,
         plusplus: true,
         regexp:   true,
         sub:      false,
         undef:    true,
         white:    false,
 */



/** polydrop
 *
 * polydrop returns an DOM HTML object that contains the GUI and
 * internal logic of the game.
 *
 */
function polydrop(into, squareWidth)
{
    var self = {};

    /** pentomino
     *
     * pentomino returns an object that represents the pentomino that
     * corresponds to the given id. If the given area exist, the 
     * returned object is placed on it.
     *
     * Public interface:
     *   Methods:
     *     transform -- transforms the object
     *     translate -- translates the object
     *     rows      -- rows occupied by the object in current orientation
     *     x         -- horizontal orientation
     *
     *   Fields
     *     fail      -- object successfully placed on given area
     *
     */
    self.pentomino = function (area, id, x, y)
    {
        var i, self = {};

        self.all = 
        {
            F :
            {
                color : '#e2f600',
                pos   : [[0, 0], [-1, 0],  [0, 1],  [1, 1],  [0, -1]]
            },
            I : 
            {
                color : '#1d1ad8',
                pos   : [[0, 2], [0, 1],  [0, 0], [0, -1],  [0, -2]]
            },
            L : 
            {
                color : '#ff5ddf',
                pos   : [[0, 0], [-1, 0],  [-1, -1], [1, 0],  [2, 0]]
            },
            N : 
            {
                color : '#0300ff',
                pos   : [[0, 0], [0, 1],  [-1, 1], [1, 0],  [2, 0]]
            },
            P : 
            {
                color : '#00ff00',
                pos   : [[0, 0], [-1, 0],  [1, 0], [0, 1],  [1, 1]]
            },
            T : 
            {
                color : '#00ffec',
                pos   : [[0, 0], [0, -1],  [0, 1], [-1, 1],  [1, 1]]
            },
            U : 
            {
                color : '#ff00cc',
                pos   : [[0, 0], [-1, 0],  [1, 0], [-1, 1],  [1, 1]]
            },
            V : 
            {
                color : '#00b4a5',
                pos   : [[-1, 1], [-1, 0],  [-1, -1], [0, -1],  [1, -1]]
            },
            W : 
            {
                color : '#90ee90',
                pos   : [[-1, 1], [-1, 0],  [0, 0], [0, -1],  [1, -1]]
            },
            X : 
            {
                color : '#ff0003',
                pos   : [[-1, 0], [0, 0],  [1, 0], [0, 1],  [0, -1]]
            },
            Y : 
            {
                color : '#3406b3',
                pos   : [[-2, 0], [-1, 0],  [0, 0], [1, 0],  [0, 1]]
            },
            Z : 
            {
                color : '#b3b206',
                pos   : [[-1, -1], [-1, 0],  [0, 0], [1, 0],  [1, 1]]
            }
        };

        /** Private: set
         *
         * set changes the status of the area for the squares that corresponds
         * to this pentomino. It does not render the change in the GUI.
         *
         */
        self.set = function (to)
        {
            var pos = self.pos;
            var x   = self.x;
            var y   = self.y;


            for (i = 0; i < pos.length; i += 1)
            {
                area.field[y + pos[i][1]][x + pos[i][0]].taken = to;
            }

            return self;
        };

        /** Private: update
         *
         * update renders the current state of this pentomino in the GUI.
         *
         */
        self.update = function ()
        {
            var cell;
            var x   = self.x;
            var y   = self.y;
            var pos = self.pos;

            for (i = 0; i < pos.length; i += 1)
            {
                cell = area.field[y + pos[i][1]][x + pos[i][0]];
                if (cell.taken)
                {
                    cell.html.style.background = cell.taken;
                    cell.html.style.display    = 'block';
                }
                else
                {
                    cell.html.style.display = 'none';
                }
            }

            return self;
        };

        /** Public: translate
         *
         * translate will try to translate this pentomino in its area as
         * dictated by given values dx and dy. If the requested change is
         * valid it is rendered in the GUI and true is returned. Otherwise
         * false is returned.
         *
         */
        self.translate = function (dx, dy)
        {
            var i;
            var pos = self.pos;
            var x   = self.x + dx;
            var y   = self.y + dy;
            var ok  = true;

            self.set(false);
            
            for (i = 0; i < pos.length && ok; i += 1)
            {
                ok = (area.field[pos[i][1] + y]                       &&
                      area.field[pos[i][1] + y][pos[i][0] + x]        &&
                      !area.field[pos[i][1] + y][pos[i][0] + x].taken);
            }

            if (ok)
            {
                self.update();

                self.x = x;
                self.y = y;

                self.set(self.color).update();
            }
            else
            {
                self.set(self.color);
            }

            return !!ok;
        };

        /** Public: transform
         *
         * transform will try to transform this pentomino in its area as
         * dictated by the given transformation matrix. If the requested 
         * change is valid it is rendered in the GUI and true is returned.
         * Otherwise false is returned.
         *
         */
        self.transform = function (t11, t12, t21, t22)
        {
            var i;
            var res = {pos : []};
            var pos = self.pos;
            var x   = self.x;
            var y   = self.y;
            var ok  = true;

            self.set(false);

            for (i = 0; i < pos.length && ok; i += 1)
            {
                res.pos[i] = [];
                res.pos[i][0] = t11 * pos[i][0] + t12 * pos[i][1];
                res.pos[i][1] = t21 * pos[i][0] + t22 * pos[i][1];
                ok = (area.field[res.pos[i][1] + y]                           &&
                      area.field[res.pos[i][1] + y][res.pos[i][0] + x]        &&
                      !area.field[res.pos[i][1] + y][res.pos[i][0] + x].taken);
            }

            if (ok)
            {
                self.update();

                self.pos = res.pos;

                self.set(self.color).update();
            }
            else
            {
                self.set(self.color);
            }

            return !!ok;
        };

        /** Private: above
         *
         * above returns the number rows occupied by this pentomino above
         * the origin of its representation.
         *
         */
        self.above = function ()
        {
            var max, i;

            for (i = 0; i < self.pos.length; i += 1)
            {
                if (max === undefined || self.pos[i][1] > max)
                {
                    max = self.pos[i][1];
                }
            }

            return max;
        };

        /** Public: rows
         *
         * rows returns the number rows occupied by this pentomino.
         *
         */
        self.rows = function ()
        {
            var max, min, i;

            for (i = 0; i < self.pos.length; i += 1)
            {
                if (min === undefined || self.pos[i][1] < min)
                {
                    min = self.pos[i][1];
                }

                if (max === undefined || self.pos[i][1] > max)
                {
                    max = self.pos[i][1];
                }
            }

            return max - min + 1;
        };

        (function init()
        {
            var pos;

            id = id || 'I';

            pos = self.pos = self.all[id].pos;
            self.color = self.all[id].color;

            x = self.x = x || Math.floor(area.cols / 2);
            y = self.y = y || area.rows - 1 - self.above();

            for (i = 0; i < self.pos.length; i += 1)
            {
                if (area.field[y + pos[i][1]][x + pos[i][0]].taken)
                {
                    self.fail = true;
                }
            }

            self.set(self.color).update();
        })();

        return {transform : self.transform, 
                translate : self.translate,
                rows      : self.rows,
                x         : function () { return self.x; },
                fail      : self.fail};
    };

    /** dropArea
     *
     * dropArea returns an object that represents the an area on which
     * pentomino objects can be placed.
     *
     * Public interface:
     *   Methods:
     *     width   -- width of area
     *     height  -- height of area
     *     clear   -- clears area
     *     isClear -- tells if area is clear
     *     update  -- render state to GUI
     *     check   -- removes full rows, returns a count
     *
     *   Fields:
     *     html    -- HTML representation 
     *     field   -- state of area
     *     rows    -- number of rows
     *     cols    -- number of columns
     *
     */
    self.dropArea = function (side, rows, cols)
    {
        var self = {};

        /** Private: forEach
         *
         * forEach invokes the given function f on each square of this area.
         *
         */
        self.forEach = function (f)
        {
            var i, j;
            
            for (i = 0; i < self.rows; i += 1)
            {
                for (j = 0; j < self.cols; j += 1)
                {
                    f(self.field[i][j]);
                }
            }
            
            return self;
        };
        
        /** Public: isClear
         *
         * isClear tells if the bottom row (and there by all rows) of this 
         * area is empty.
         *
         */
        self.isClear = function ()
        {
            var i;

            for (i = 0; i < self.cols; i += 1)
            {
                if (self.field[0][i].taken)
                {
                    return false;
                }
            }

            return true;
        };

        /** Public: clear
         *
         * clear sets all squares of this area to be empty.
         *
         */
        self.clear =  function ()
        {
            self.forEach(function (cell)
            {
                cell.taken = false;
            });

            return self;
        };

        /** Public: update
         *
         * update renders the state of this area to the GUI.
         *
         */
        self.update = function ()
        {
            self.forEach(function (cell)
            {
                if (!cell.taken)
                {
                    cell.html.style.display = 'none';
                }
                else
                {
                    cell.html.style.background = cell.taken;
                    cell.html.style.display = 'block';
                }
            });

            return self;
        };

        /** Private: shift
         *
         * shift removes all rows of this area that contains no empty squares.
         * Remaining rows are shifted down as far as possible.
         *
         */
        self.shift = function (n)
        {
            var i, j;

            if (n >= self.rows || n < 0)
            {
                return false;
            }

            for (i = n; i < self.rows - 1; i += 1)
            {
                for (j = 0; j < self.cols; j += 1)
                {
                    self.field[i][j].taken = self.field[i + 1][j].taken;
                }
            }

            for (j = 0; j < self.cols; j += 1)
            {
                self.field[self.rows - 1][j].taken = false;
            }

            return true;
        };

        /** Public: check
         *
         * check removes all rows of this area that contains no empty squares.
         * Remaining rows are shifted down as far as possible. The number of
         * removed rows are returned.
         *
         */
        self.check = function ()
        {
            var i, j, ok, res;

            res = 0;
            i = 0;
            while (i < self.rows)
            {
                ok = true;
                for (j = 0; j < self.cols; j += 1)
                {
                    ok = ok && self.field[i][j].taken;
                }

                if (ok)
                {
                    self.shift(i);
                    res += 1;
                }
                else
                {
                    i += 1;
                }
            }

            return res;
        };

        (function init()
        {
            var i, j;

            self.side = side || 20;
            self.rows = rows || 25;
            self.cols = cols || 15;

            self.height = self.rows * (self.side - 1) + 1;
            self.width  = self.cols * (self.side - 1) + 1;

            self.html = function ()
            {
                var res = document.createElement('div');

                res.style.position  = 'absolute';
                res.style.top       = '0px';
                res.style.left      = '0px';
                res.style.height    = self.height + 'px';
                res.style.width     = self.width + 'px';

                return res;
            }();
            
            self.field = [];

            for (i = self.rows - 1; i >= 0 ; i -= 1)
            {
                self.field[i] = [];
                for (j = 0; j < self.cols; j += 1)
                {
                    self.field[i][j] = function ()
                    {
                        var html = document.createElement('div');

                        html.style.width  = (self.side - 2) + 'px';
                        html.style.height = (self.side - 2) + 'px';
                        html.style.position = 'absolute';
                        html.style.left = j * (self.side - 1) + 'px';
                        html.style.top  = 
                            (self.rows - 1 - i) * (self.side - 1) + 'px';
                        html.style.border = '1px solid #dddddd';
                        html.style.display = 'none';
                        self.html.appendChild(html);

                        return { html : html, taken : false };
                    }();
                }
            }
        })();
        
        return {html    : self.html, 
                rows    : self.rows,
                cols    : self.cols,
                height  : function () { return self.height; },
                width   : function () { return self.width; },
                field   : self.field,
                clear   : self.clear,
                isClear : self.isClear,
                update  : self.update,
                check   : self.check};
    };

    /** Private: clear
     *
     * clear resets this polydrop object to a state ready for a new game.
     *
     */
    self.clear =  function ()
    {
        self.delay     = 700;
        self.score     = 0;
        self.removed   = 0;
        self.placed    = 0;
        self.nextPiece = undefined;
        self.drop.clear();

        return self;
    };
    
    /** Private: update
     *
     * update renders the state of this polydrop game to the GUI.
     *
     */
    self.update = function ()
    {
        self.drop.update();

        self.setScore();

        return self;
    };

    /** Private: next
     *
     * next controls this polydrop game by translating pentominos down,
     * introducing new polyminos, removing full rows. When introduction
     * of a new polymino fails the game is terminated.
     *
     */
    self.next = function ()
    {
        var rem;

        var randomChar = function (s)
        {
            return s.charAt(Math.floor(Math.random() * s.length));
        };

        var scoreTab = 
        {
            0     : 0,
            1     : 1,
            2     : 4,
            3     : 10,
            4     : 20,
            5     : 40,
            
            clear : 50
        };

        clearTimeout(self.timeoutId);

        if (!self.falling)
        {
            self.nextPiece = self.nextPiece || randomChar('FILNPTUVWXYZ');
            self.falling = self.pentomino(self.drop, self.nextPiece);
            self.nextPiece = randomChar('FILNPTUVWXYZ');
            self.setNext(self.nextPiece);
            self.placed += 1;

            if (self.falling.fail)
            {
                self.end();
            }
        }
        else
        {
            if (!self.falling.translate(0, -1))
            {
                delete self.falling;

                if ((rem = self.drop.check()))
                {
                    self.score += scoreTab[rem] || 0;
                    if (self.drop.isClear())
                    {
                        self.score += scoreTab.clear;
                    }

                    self.removed += rem;
                    self.update();
                }

                if (self.delay > 100)
                {
                    self.delay -= 2;
                }

                return self.next();
            }
        }

        if (self.falling)
        {
            self.timeoutId = setTimeout(self.next, self.delay);
        }

        return self;
    };

    /** Private: restartTimeout
     *
     * restartTimeout resets the time out schedule of this polydrop game by
     * canceling any pending calls to next and introducing a new call scheduled
     * a full delay interval from current time.
     *
     */
    self.restartTimeout = function ()
    {
        clearTimeout(self.timeoutId);
        self.timeoutId = setTimeout(self.next, self.delay);
    };

    /** Private: start
     *
     * starts starts a new game by setting up appropriate keyboard callbacks
     * for gameplay, resetting this polydrop object and making the initial
     * call to next.
     *
     */
    self.start = function ()
    {
        self.keyIsDown     = {};
        self.oldOnKeyDown  = document.onkeydown;
        self.oldOnKeyUp    = document.onkeyup;
        self.oldOnKeyPress = document.onkeypress;

        document.onkeydown = self.keyDown;
        document.onkeyup   = self.keyUp;

        document.onkeypress  = function () 
        {
            return false;
        };

        if (self.helpMenu)
        {
            self.helpMenu.style.display = 'none';
        }

        self.clear().update().next();
    };

    /** Private: end
     *
     * end ends the current game in this polydrop object by canceling
     * any pending calls to next, and resetting keyboard callbacks to
     * the state they where in before the game started.
     *
     */
    self.end = function ()
    {
        delete self.falling;
        clearTimeout(self.timeoutId);
        self.msg.showText('Game over!', self.start);

        document.onkeydown  = self.oldOnKeyDown;
        document.onkeyup    = self.oldOnKeyUp;
        document.onkeypress = self.oldOnKeyPress;
    };

    /** Private: action
     *
     * action is an object that maps key names to corresponding actions.
     *
     */
    self.action =
    {
        ENTER : function ()
        {
            if (self.falling && !self.pause)
            {
                while (self.falling.translate(0, -1)) {}
                        
                self.next();
            }
            else
            {
                self.msg.onclick({});
            }
        },

        LEFT  : function () { self.falling.translate(-1,  0); },
        UP    : function () {},
        RIGHT : function () { self.falling.translate(1,  0); },
        DOWN  : function ()
        {
            if (self.falling.translate(0, -1))
            {
                self.restartTimeout();
            }
        },
        
        ROT_RIGHT  : function () { self.falling.transform(0,   1, -1,  0); },
        ROT_LEFT   : function () { self.falling.transform(0,  -1,  1,  0); },
        ROT_UP     : function () { self.falling.transform(-1,  0,  0, -1); },
        ROT_DOWN   : function () { self.falling.transform(-1,  0,  0, -1); },
        
        FLIP_RIGHT : function () { self.falling.transform(-1,  0,  0,  1); },
        FLIP_LEFT  : function () { self.falling.transform(-1,  0,  0,  1); },
        FLIP_UP    : function () { self.falling.transform(1,   0,  0, -1); },
        FLIP_DOWN  : function () { self.falling.transform(1,   0,  0, -1); },
        
        FULL_LEFT : function ()
        {
            while (self.falling.translate(-1, 0)) {}
        },
        FULL_RIGHT : function ()
        {
            while (self.falling.translate(1, 0)) {}
        },
        FULL_DOWN : function ()
        {
            while (self.falling.translate(0, -1)) {}
                        
            self.next();
        },
        FULL_UP : function ()
        {
            var middle = self.drop.cols / 2;
            
            if (self.falling.x() < middle)
            {
                while (self.falling.x() < middle - 0.5 &&
                       self.falling.translate(1, 0)) {}
            }
            else
            {
                while (self.falling.x() > middle &&
                       self.falling.translate(-1, 0)) {}
            }
        },
        NEXT : function ()
        { 
            self.showNext = !self.showNext;
            self.setNext(self.nextPiece);
        },
        PAUSE : function ()
        { 
            if (self.pause)
            {
                self.msg.onclick({});
            }
            else
            {
                clearTimeout(self.timeoutId);
                self.pause = true;
                
                self.msg.showText('PAUSE', function ()
                {
                    self.pause = false;
                    
                    self.restartTimeout();
                });
            }
        },
        END : self.end
    };
    
    /** Private: keyMap
     *
     * keyMap is an object that maps key codes to key names.
     *
     */
    self.keyMap = 
    {
        13 : 'ENTER',
        32 : 'ENTER',
        
        37 : 'LEFT',
        38 : 'UP',
        39 : 'RIGHT',
        40 : 'DOWN',

        65 : 'FULL',
        83 : 'FLIP',
        67 : 'ROT',
        68 : 'ROT',
        
        78 : 'NEXT',
        
        19 : 'PAUSE',
        80 : 'PAUSE',
        
        27 : 'END'
    };

    /** Private: keyDown
     *
     * keyDown is the callback function used for the keydown event
     * during gameplay.
     *
     */
    self.keyDown = function (e)
    {
        e = e || window.event;
        var code = e.keyCode || e.which;
        code = self.keyMap[code] || '';

        code = e.ctrlKey  || self.keyIsDown.ROT  ? 'ROT_'  + code : code;
        code = e.shiftKey || self.keyIsDown.FLIP ? 'FLIP_' + code : code;
        code = e.altKey   || self.keyIsDown.FULL ? 'FULL_' + code : code;

        if (self.action[code])
        {
            self.action[code]();
        }
        else
        {
            self.keyIsDown[code] = true;
        }

        return false;
    };

    /** Private: keyUp
     *
     * keyUp is the callback function used for the keyup event
     * during gameplay.
     *
     */
    self.keyUp = function (e)
    {
        e = e || window.event;
        var code = e.keyCode || e.which;

        code = self.keyIsDown[self.keyMap[code]] = false;

        return false;
    };
    
    /* Construction of a polydrop object */
    (function ()
    {
        var side   = squareWidth || 20;
        var bw     = Math.ceil(side / 5);
        var border = bw + 'px solid #646464';
        var offset = 1.7 * side + bw;
        var width;
        var height;

        self.cols = 15;
        self.rows = 25;

        self.drop = self.dropArea(side);

        width  = self.drop.width() + 1;
        height = self.drop.height() + offset;

        self.html = document.createElement('div');

        self.html.appendChild(self.canvas = function ()
        {
            var res = document.createElement('div');

            res.style.position   = 'relative';
            res.style.top        = '0px';
            res.style.left       = '0px';
            res.style.width      = width + 'px';
            res.style.height     = height + 'px';
            res.style.border     = border;
            res.style.background = 'black';

            res.appendChild(self.drop.html);
            self.drop.html.style.top = offset + 'px';

            return res;
        }());

        /* Construction of the canvas part of the GUI */
        self.canvas.appendChild (function ()
        {
            var next = self.dropArea(1.7 * side / 5, 10, 10);

            next.html.style.top  = '0px';
            next.html.style.left = self.drop.width() - next.width() + 'px';

            self.setNext = function (id)
            {
                var p, rows;

                next.clear();
                if (self.showNext)
                {
                    p = self.pentomino(next, id);
                
                    rows = p.rows();
                    if (rows === 5)
                    {
                        p.transform(0, -1,  1,  0);
                    }
                    else if (rows === 3)
                    {
                        p.translate(0, -1);         
                    }
                    else if (rows === 2)
                    {
                        p.translate(0, -2);         
                    }
                }

                next.update();
            };

            return next.html;
        }());

        /* Construction of score display */
        self.canvas.appendChild (function ()
        {
            var res = document.createElement('div');
            res.style.position     = 'absolute';
            res.style.top          = '0px';
            res.style.left         = '0px';
            res.style.width        = width - 2 * bw + 'px';
            res.style.height       = offset - 3 * bw + 'px';
            res.style.color        = 'white';
            res.style.font         = (Math.floor(offset * 0.5) +
                                        'px sans-serif');
            res.style.textAlign    = 'left';
            res.style.padding      = bw + 'px';
            res.style.borderBottom = border;

            self.setScore = function ()
            {
                var node = document.createElement('div');
                var text = function (s) { return document.createTextNode (s); };

                while (res.firstChild)
                {
                    res.removeChild(res.firstChild);
                }
        
                node.appendChild(text('Score: ' + self.score));
                node.style.position = 'absolute';
                res.appendChild(node);
                
                node = document.createElement('div');
                node.appendChild(text('Rows: ' + self.removed));
                node.style.position = 'absolute';
                node.style.textAlign = 'right';
                node.style.width = '78%';
                res.appendChild(node);
            };

            return res;
        }());

        /* Construction of the message button */
        self.canvas.appendChild(self.msg = function ()
        {
            var res   = document.createElement('div');
            var msgbw = Math.floor(side / 8);

            res.style.display    = 'none';
            res.style.position   = 'absolute';
            res.style.width      = Math.floor(0.8 * width) + 'px';
            res.style.left       = Math.floor(0.1 * width) - msgbw - bw + 'px';
            res.style.border     = msgbw + 'px solid white';
            res.style.padding    = Math.floor(side / 5) + 'px';
            res.style.marginTop  = Math.floor(height / 2 - 1.5 * side) + 'px';
            res.style.textAlign  = 'center';
            res.style.color      = 'white';
            res.style.font       = Math.floor(1.5 * side) + 'px sans-serif';
            res.style.background = 'black';
            res.showText = function (text, callback)
            {
                res.onclick = function ()
                {
                    res.style.display = 'none';
                    callback();
                };

                while (res.firstChild)
                {
                    res.removeChild(res.firstChild);
                }

                res.appendChild(document.createTextNode(text + ''));
                res.style.display = 'block';
            };

            return res;
        }());

        /* Construction of the help menu */
        self.canvas.appendChild(self.helpMenu = function ()
        {
            var res, table, tbody;

            var row = function ()
            {
                var tr, td;
                
                tr = document.createElement('tr');
                
                for (var i = 0; i < arguments.length; i += 1)
                {
                    td = document.createElement('td');
                    td.appendChild(document.createTextNode(arguments[i]));
                    td.style.textAlign  = i ? 'right' : 'left';
                    tr.appendChild(td);
                }

                return tr;
            };

            res = document.createElement('div');
            res.style.position   = 'absolute';
            res.style.width      = Math.floor(0.8 * width) + 'px';
            res.style.left       = Math.floor(0.09 * width) - bw + 'px';
            res.style.padding    = Math.floor(side / 5) + bw + 'px';
            res.style.marginTop  = Math.floor(height / 2 + 1.5 * side) + 'px';
            res.style.textAlign  = 'left';
            res.style.color      = 'white';
            res.style.font       = Math.floor(0.7 * side) + 'px sans-serif';

            tbody = document.createElement('tbody');
            tbody.appendChild(row('arrow key',     'move'));
            tbody.appendChild(row('arrow key + d', 'rotate'));
            tbody.appendChild(row('arrow key + s', 'flip'));
            tbody.appendChild(row('arrow key + a', 'full move'));
            tbody.appendChild(row('p',             'pause'));
            tbody.appendChild(row('n',             'show next'));
            tbody.appendChild(row('Esc',           'end game'));
            table = document.createElement('table');
            table.width = '100%';
            table.appendChild(tbody);
            res.appendChild(table);

            return res;
        }());

        if (into)
        {
            into.appendChild(self.html);
        }

        self.clear().update().msg.showText('Start game!', self.start);
    })();

    return self.html;
}

