(???) 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()