(???) onst primaryColor = '#CE5127'
 (???) onst primaryColorLight = ' #E98A70'
 (???) onst secondaryColor = '#27A2CC'
 (???) onst secondaryColorLight = '#86CFE4'
 (???) onst textColor = '#4B4B4B'
 (???) onst backgroundColor = '#F9F4F2'
 (???) onst WIDTH = 10
 (???) onst HEIGHT = 20           // TODO 24
 (???) onst CELLSIZE = 16
 (???) / colors are material 700
 (???) onst blocks = [
 (???)    {
 (???)        color: '#0097A7',           // cyan
 (???)        coords: [
 (???)            [[0, 1], [1, 1], [2, 1], [3, 1]],
 (???)            [[1, 0], [1, 1], [1, 2], [1, 3]],
 (???)            [[0, 1], [1, 1], [2, 1], [3, 1]],
 (???)            [[1, 0], [1, 1], [1, 2], [1, 3]]],
 (???)        shape: 'I'
 (???)    }, {
 (???)        color: '#7B1FA2',           // purple
 (???)        coords: [
 (???)            [[0, 1], [1, 1], [2, 1], [2, 2]],
 (???)            [[1, 0], [1, 1], [1, 2], [2, 0]],
 (???)            [[0, 1], [0, 2], [1, 2], [2, 2]],
 (???)            [[1, 2], [2, 0], [2, 1], [2, 2]]],
 (???)        shape: 'J'
 (???)    }, {
 (???)        color: '#F57C00',           // orange
 (???)        coords: [
 (???)            [[0, 1], [0, 2], [1, 1], [2, 1]],
 (???)            [[1, 0], [1, 1], [1, 2], [2, 2]],
 (???)            [[0, 2], [1, 2], [2, 1], [2, 2]],
 (???)            [[1, 0], [2, 0], [2, 1], [2, 2]]],
 (???)        shape: 'L'
 (???)    }, {
 (???)        color: '#FBC02D',           // yellow
 (???)        coords: [
 (???)            [[1, 1], [1, 2], [2, 1], [2, 2]],
 (???)            [[1, 1], [1, 2], [2, 1], [2, 2]],
 (???)            [[1, 1], [1, 2], [2, 1], [2, 2]],
 (???)            [[1, 1], [1, 2], [2, 1], [2, 2]]],
 (???)        shape: 'O'
 (???)    }, {
 (???)        color: '#388E3C',           // green
 (???)        coords: [
 (???)            [[0, 2], [1, 1], [1, 2], [2, 1]],
 (???)            [[1, 0], [1, 1], [2, 1], [2, 2]],
 (???)            [[0, 2], [1, 1], [1, 2], [2, 1]],
 (???)            [[1, 0], [1, 1], [2, 1], [2, 2]]],
 (???)        shape: 'S'
 (???)    }, {
 (???)        color: '#1976D2',           // blue
 (???)        coords: [
 (???)            [[0, 1], [1, 1], [1, 2], [2, 1]],
 (???)            [[1, 0], [1, 1], [1, 2], [2, 1]],
 (???)            [[0, 2], [1, 1], [1, 2], [2, 2]],
 (???)            [[1, 1], [2, 0], [2, 1], [2, 2]]],
 (???)        shape: 'T'
 (???)    }, {
 (???)        color: '#D32F2F',           // red
 (???)        coords: [
 (???)            [[0, 1], [1, 1], [1, 2], [2, 2]],
 (???)            [[1, 1], [1, 2], [2, 0], [2, 1]],
 (???)            [[0, 1], [1, 1], [1, 2], [2, 2]],
 (???)            [[1, 1], [1, 2], [2, 0], [2, 1]]],
 (???)        shape: 'Z'
 (???)    }
 (???) 
 (???) / IRenderer provides primitives to render blocks and score statistics
       nterface IRenderer {
 (???)    draw(x: number, y: number, color: string): void
 (???)    score(score: number, lines: number): void
 (???) 
 (???) lass CanvasRenderer implements IRenderer {
 (???)    private context: CanvasRenderingContext2D | null
 (???)    constructor(canvas: HTMLCanvasElement) {
 (???)        canvas.setAttribute('width', '' + (WIDTH + 6) * CELLSIZE)
 (???)        canvas.setAttribute('height', '' + HEIGHT * CELLSIZE)
 (???)        this.context = canvas.getContext('2d')
 (???)        this.context!.font = '18px arial'
 (???)        this.context!.textAlign = 'center'
 (???)    }
 (???)    public draw(x: number, y: number, color: string): void {
 (???)        this.context!.fillStyle = color
 (???)        this.context!.fillRect(x * CELLSIZE, y * CELLSIZE, CELLSIZE, CELLSIZE)
 (???)    }
 (???)    public score(score: number, lines: number): void {
 (???)        // clear background
 (???)        this.context!.fillStyle = secondaryColor
 (???)        this.context!.fillRect(WIDTH * CELLSIZE, 0, WIDTH * CELLSIZE, HEIGHT * CELLSIZE)
 (???)        // draw text
 (???)        this.context!.save()
 (???)        this.context!.fillStyle = 'white'
 (???)        this.context!.fillText('Score', CELLSIZE * 13, CELLSIZE * 11)
 (???)        this.context!.fillText(score.toString(), CELLSIZE * 13, CELLSIZE * 13)
 (???)        this.context!.fillText('Lines', CELLSIZE * 13, CELLSIZE * 17)
 (???)        this.context!.fillText(lines.toString(), CELLSIZE * 13, CELLSIZE * 19)
 (???)    }
 (???) 
 (???) lass Block {
 (???)    public coords: number[][][] = [[[]]]
 (???)    public color: string = ''
 (???)    public shape: string = ''
 (???) 
 (???) lass Tetris {
 (???)    private bucket: number[][]
 (???)    private block: Block
 (???)    private nextBlock: Block
 (???)    private timer = 0
 (???)    private pause = false
 (???)    // player variables
 (???)    private rotation = 0
 (???)    private column: number = 0
 (???)    private row: number = 0
 (???)    private score = 0
 (???)    private lines = 0
 (???)    constructor(private renderer: IRenderer) {
 (???)        this.block = this.randomBlock()
 (???)        this.nextBlock = this.randomBlock()
 (???)        this.bucket = this.initBucket()
 (???)    }
 (???)    public newGame(): void {
 (???)        this.score = 0
 (???)        this.lines = 0
 (???)        this.bucket = this.initBucket()
 (???)        this.newBlock()
 (???)        window.addEventListener('keydown', (event: KeyboardEvent) => {
 (???)            if (event.defaultPrevented) {
 (???)                return
 (???)            }
 (???)            if (event.code == 'KeyP') {
 (???)                this.pause = !this.pause
 (???)                if (!this.pause) {
 (???)                    this.fall()     // anti-cheat
 (???)                }
 (???)                event.preventDefault()
 (???)            }
 (???)            if(this.pause) {
 (???)                return
 (???)            }
 (???)            switch (event.code) {
 (???)                case 'ArrowLeft': case 'KeyJ':
 (???)                    this.move(-1, 0, this.rotation)
 (???)                    event.preventDefault()
 (???)                    break
 (???)                case 'ArrowUp': case 'KeyK':
 (???)                    this.move(0, 0, (this.rotation + 1) % 4)
 (???)                    event.preventDefault()
 (???)                    break
 (???)                case 'ArrowRight': case 'KeyL':
 (???)                    this.move(1, 0, this.rotation)
 (???)                    event.preventDefault()
 (???)                    break
 (???)                case 'Space':
 (???)                    this.drop()
 (???)                    event.preventDefault()
 (???)                    break
 (???)                case 'ArrowDown': case 'KeyI': case 'KeyM':
 (???)                    this.fall()
 (???)                    event.preventDefault()
 (???)                    break
 (???)            }
 (???)        }, true)
 (???)    }
 (???)    public newBlock(): void {
 (???)        // clear preview
 (???)        this.draw(WIDTH + 1, 0, this.nextBlock, 0, secondaryColorLight)
 (???)        const clearedLines = this.sweepBucket()
 (???)        this.redrawBucket()
 (???)        // stats
 (???)        const bonus = [0, 100, 300, 700, 1500]
 (???)        this.lines += clearedLines
 (???)        this.score += bonus[clearedLines]
 (???)        this.renderer.score(this.score, this.lines)
 (???)        // show preview
 (???)        this.block = this.nextBlock
 (???)        this.nextBlock = this.randomBlock()
 (???)        this.draw(WIDTH + 1, 0, this.nextBlock, 0)
 (???)        clearInterval(this.timer)
 (???)        this.timer = setInterval(() => this.fall(), 650 - this.lines)
 (???)        this.column = 3
 (???)        this.row = 0
 (???)        this.rotation = 0
 (???)        if (!this.move(0, 0, this.rotation)) {
 (???)            clearInterval(this.timer)
 (???)            this.gameOver()
 (???)        }
 (???)    }
 (???)    public move(deltaColumn: number, deltaRow: number, newRotation: number) {
 (???)        const canMove = this.testBlock(deltaColumn, deltaRow, this.block, newRotation)
 (???)        if (canMove) {
 (???)            // clear previous block
 (???)            this.draw(this.column, this.row, this.block, this.rotation, backgroundColor)
 (???)            this.column += deltaColumn
 (???)            this.row += deltaRow
 (???)            this.rotation = newRotation
 (???)            // draw block
 (???)            this.draw(this.column, this.row, this.block, this.rotation)
 (???)        }
 (???)        return canMove
 (???)    }
 (???)    public fall() {
 (???)        if (this.pause) {
 (???)            return
 (???)        }
 (???)        if (!this.move(0, 1, this.rotation)) {
 (???)            // block lands
 (???)            this.setBuffer(blocks.indexOf(this.block), this.column, this.row, this.block, this.rotation)
 (???)            this.newBlock()
 (???)        }
 (???)    }
 (???)    public drop() {
 (???)        clearInterval(this.timer)
 (???)        this.timer = setInterval(() => this.fall(), 9)
 (???)        this.score += (HEIGHT - this.row)
 (???)        this.renderer.score(this.score, this.lines)
 (???)    }
 (???)    private draw(column: number, row: number, block: Block, rotation: number, color: string = block.color) {
 (???)        for (const { x, y } of this.coords(block, rotation, column, row)) {
 (???)            this.renderer.draw(x, y, color)
 (???)        }
 (???)    }
 (???)    private initBucket(): number[][] {
 (???)        return Array<number[]>(HEIGHT).fill([])
 (???)            .map(() => Array<number>(WIDTH).fill(-1))
 (???)    }
 (???)    private setBuffer(set: number, column: number, row: number, block: Block, rotation: number) {
 (???)        for (const { x, y } of this.coords(block, rotation, column, row)) {
 (???)            this.bucket[y][x] = set
 (???)        }
 (???)    }
 (???)    // testBlocks tests if a block can be placed and returns true if so, false otherwise
 (???)    private testBlock(deltaColumn: number, deltaRow: number, block: Block, rotation: number): boolean {
 (???)        const predicate = ({ x, y }: { x: number, y: number }) =>
 (???)            x >= 0 && x < WIDTH && y < HEIGHT && this.bucket[y][x] < 0
 (???)        return [...this.coords(block, rotation, this.column + deltaColumn, this.row + deltaRow)]
 (???)            .every(predicate)
 (???)    }
 (???)    private *coords(block: Block, rotation: number, column: number, row: number): IterableIterator<{ x: number, y: number }> {
 (???)        const coords = block.coords[rotation]
 (???)        for (const coord of coords) {
 (???)            const [x, y] = [column + coord[0], row + coord[1]]
 (???)            yield { x, y }
 (???)        }
 (???)    }
 (???)    private randomBlock(): Block {
 (???)        return blocks[Math.floor(Math.random() * blocks.length)]
 (???)    }
 (???)    private sweepBucket(): number {
 (???)        let linesCleared = 0
 (???)        for (const [rowIndex, row] of this.bucket.entries()) {
 (???)            const full = row.every(cell => cell >= 0)
 (???)            if (full) {
 (???)                this.bucket.splice(rowIndex, 1)
 (???)                this.bucket.unshift(new Array(WIDTH).fill(-1))
 (???)                linesCleared++
 (???)            }
 (???)        }
 (???)        return linesCleared
 (???)    }
 (???)    private redrawBucket(): void {
 (???)        this.bucket.forEach((row, y) => {
 (???)            row.forEach((cell, x) => {
 (???)                const color = cell >= 0 ? blocks[cell].color : backgroundColor
 (???)                this.renderer.draw(x, y, color)
 (???)            })
 (???)        })
 (???)    }
 (???)    private gameOver() {
 (???)        alert('Game over :-(')
 (???)        this.newGame()
 (???)    }
 (???) 
 (???) lass Widget {
 (???)    constructor() {
 (???)        const template = `<canvas class="bucket"></canvas>`
 (???)        document!.querySelector('.typetris')!.innerHTML = template
 (???)        const canvas = <HTMLCanvasElement>document.querySelector('.typetris canvas')
 (???)        const renderer = new CanvasRenderer(canvas)
 (???)        const tetris = new Tetris(renderer)
 (???)        tetris.newGame()
 (???)    }
 (???) 
 (???) / tslint:disable-next-line: no-unused-expression
 (???) ew Widget()