Built a Tetris game with Angular šŸŽ®Ā 

Frontend Engineer

Angular Kenya - 05 Feb 2021

About me

Hi, My name is Trung šŸ˜Š

  • Experienced FE engineer, specialized in branding, interactive application
  • Frontend Engineer @cakedefi
  • Organizer @Angular Vietnam
  • Write, code, and talk about Angular
  • Biggest Angular group in APAC
  • Advocate and grow the Angular developer community in Vietnam
  • 14k members
  • Founded in 2017 by
  • 100 Days Of AngularĀ series

Agenda

  • What is Tetris?

  • What and why Angular Tetris?

  • Techstack

  • Development Challenge

    • Tetris Game Loop

    • Piece/Tetrominos

    • Board

    • Animation/Timer

    • Keyboard

    • Sounds

What is Tetris?

  • 1984
  • Alexey Pajitnov
  • Rotate and move falling Tetris pieces
  • Fill the horizontal rows of blocks without empty cells

What is Angular Tetris?

Why Angular Tetris?

  • My first game "console" ever
  • Costed probably $1 USD
  • $1 USD = 24 eggs
  • vue-tetris
  • My wife asked me to do the same

1996 maybe?

Development Approach

  • Look at vue-tetris source code
  • Build a minimal to do list
  • Start with the HTML skeleton
  • Build the Tetris core (end up with @chrum/ngx-tetris)
  • Replace setTimeout, setInterval with rxjs
  • Handle keyboard event
  • Handle sounds

Ā 

Development Challenge

  • Error-prone codeĀ 
  • Extensive use of setTimeout and setInterval
  • The game loop was difficult to understand

Tetris Core

It is the most important part of the game

Tetris Core

ā€œ Code is like humor. When you have to explain it, itā€™s bad.ā€Ā  - Cory House

I ended up using @chrum/ngx-tetris

  • Game loop (rewritten using rxjs)
  • How to render the piece into a board
  • Navigate the pieceĀ 
  • Handle piece clearing and game over state
  • and more

Ā 

I did write some additional functionality

Game loop

  _gameInterval: Subscription;

  auto(delay: number) {
    this._gameInterval = timer(0, delay).subscribe(() => {
      this._update();
    });
  }

Tetromino

  • A geometric shape composed of four squares
  • L and J are, S and Z are reflections of each other.

Tetromino Data Structure

export class Piece {
  x: number;
  y: number;
  rotation = PieceRotation.Deg0;
  type: PieceTypes;
  shape: Shape;
  next: Shape;

  private _shapes: Shapes;
  private _lastConfig: Partial<Piece>;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  protected setShapes(shapes: Shapes) {
    this._shapes = shapes;
    this.shape = shapes[this.rotation];
  }
}

Tetromino Data Structure

const ShapesL: Shapes = [];
ShapesL[PieceRotation.Deg0] = [
  [0, 0, 0, 0],
  [1, 0, 0, 0],
  [1, 0, 0, 0],
  [1, 1, 0, 0]
];

ShapesL[PieceRotation.Deg90] = [
  [0, 0, 0, 0],
  [0, 0, 0, 0],
  [1, 1, 1, 0],
  [1, 0, 0, 0]
];

export class PieceL extends Piece {
  constructor(x: number, y: number) {
    super(x, y);
    this.type = PieceTypes.L;
    this.next = [
      [0, 0, 1, 0],
      [1, 1, 1, 0]
    ];
    this.setShapes(ShapesL);
  }
}

Custom Tetromino

const ShapesF: Shapes = [];
ShapesF[PieceRotation.Deg0] = [
  [1, 0, 0, 0],
  [1, 1, 0, 0],
  [1, 0, 0, 0],
  [1, 1, 0, 0]
];

export class PieceF extends Piece {
  constructor(x, y) {
    super(x, y);
    this.type = PieceTypes.F;
    this.next = [
      [1, 0, 1, 0],
      [1, 1, 1, 1]
    ];
    this.setShapes(ShapesF);
  }
}

Custom Tetromino

Board

0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
0 0 0 0 1 0 0 0 0 0
0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 1 0 0 0 0

Board

0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
0 0 0 0 1 0 0 0 0 0
0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 1 0 0 0 0

Animation

I rewrote the animation with RxJS

Animation

div.b {
  transform: scale(-1, 1);
}
div.cube {
  width: 150px;
  height: 80px;
  background-color: yellow;
}

div.a {
  transform: scale(1, 1);
}

Animation

.dragon {
    width: 80px;
    height: 86px;
    margin: 0 auto;
    background-position: 0 -100px;
  
    &.l1 {
      background-position: 0 -100px;
    }
    &.l1
      transform: scale(-1, 1);
    }
  }
.dragon {
    width: 80px;
    height: 86px;
    margin: 0 auto;
    background-position: 0 -100px;
  
    &.r1 {
      background-position: 0 -100px;
    }
  }

Animation

  eyes() {
    return timer(0, 500).pipe(
      startWith(0),
      map((x) => x + 1),
      takeWhile((x) => x < 6),
      tap((x) => {
        let state = x % 2 === 0 ? 1 : 2;
        this.className = `l${ state }`;
      })
    );
  }

Animation

  run() {
    let side = 'r';
    return timer(0, 100).pipe(
      startWith(0),
      map((x) => x + 1),
      takeWhile((x) => x <= 40),
      tap((x) => {
        if (x === 10 || x === 20 || x === 30) {
          side = side === 'r' ? 'l' : 'r';
        }
        let state = x % 2 === 0 ? 3 : 4;
        this.className = `${ side }${ state }`;
      }),
      finalize(() => {
        this.className = `${ side }1`;
      })
    );
  }

r 1s -> l 1s -> r1s -> l 1s -> end with {side}1 ~ l1

Animation

The ConcatĀ operator concatenates the output of multiple ObservablesĀ so that they act like a single Observable, with all of the items emitted by the first ObservableĀ being emitted before any of the items emitted by the second Observable

  ngOnInit(): void {
    concat(this.run(), this.eyes())
      .pipe(
        delay(5000),
        repeat(1000),
        untilDestroyed(this)
      )
      .subscribe();
  }

Animation

The actual result doesn't look very identical but it is good enough in my standard.

Keyboard handling

export enum TetrisKeyboard {
  Up = 'arrowup',
  Down = 'arrowdown',
  Left = 'arrowleft',
  Right = 'arrowright',
  Space = 'space',
  P = 'p',
  R = 'r',
  S = 's'
}

@HostListener(`${KeyDown}.${TetrisKeyboard.Left}`)
keyDownLeft() {
  this._soundManager.move();
  this._keyboardService.setKeyĢ£({
    left: true
  });
  if (this.hasCurrent) {
    this._tetrisService.moveLeft();
  } else {
    this._tetrisService.decreaseLevel();
  }
}

See more āž”ļø @HostListener

Web Audio API

  • Chrome, Safari and Opera: webkitAudioContext
  • Firefox: AudioContext

Some browsers use deprecated properties and method names that are not present in standards-compliant browsers

Time Spending

Community Support

Community Support

Community Support

Thank you!

Made with Slides.com