├── src
├── assets
│ ├── .gitkeep
│ ├── share-image.png
│ └── share-image-large.png
├── favicon.ico
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── app
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── board.component.html
│ ├── game.service.ts
│ ├── constants.ts
│ ├── piece.component.ts
│ └── board.component.ts
├── main.ts
├── styles.scss
├── index.html
└── polyfills.ts
├── tsconfig.app.json
├── browserslist
├── tsconfig.json
├── README.md
├── .gitignore
├── package.json
└── angular.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melcor76/ng-tetris/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/assets/share-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melcor76/ng-tetris/HEAD/src/assets/share-image.png
--------------------------------------------------------------------------------
/src/assets/share-image-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/melcor76/ng-tetris/HEAD/src/assets/share-image-large.png
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | template: `
6 |
7 | `,
8 | })
9 | export class AppComponent {
10 | title = 'ng-tetris';
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ],
14 | "exclude": [
15 | "src/test.ts",
16 | "src/**/*.spec.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AppComponent } from './app.component';
5 | import { BoardComponent } from './board.component';
6 |
7 | @NgModule({
8 | declarations: [AppComponent, BoardComponent],
9 | imports: [BrowserModule],
10 | providers: [],
11 | bootstrap: [AppComponent]
12 | })
13 | export class AppModule {}
14 |
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2015",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2018",
19 | "dom"
20 | ]
21 | },
22 | "angularCompilerOptions": {
23 | "fullTemplateTypeCheck": true,
24 | "strictInjectionParameters": true
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ng-tetris
2 |
3 | Tetris game in Angular. [Play it now!](https://focused-mestorf-930f82.netlify.com/)
4 |
5 | Read the [blog about making the game](https://medium.com/angular-in-depth/game-development-tetris-in-angular-64ef96ce56f7?sk=66ab4b5774919de28eecd3a2662557a4)
6 |
7 | 
8 |
9 | ## Development server
10 |
11 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | * {
3 | font-family: 'Press Start 2P', cursive;
4 | }
5 |
6 | .grid {
7 | display: grid;
8 | grid-template-columns: 320px 200px;
9 | }
10 |
11 | .right-column {
12 | display: flex;
13 | flex-direction: column;
14 | justify-content: space-between;
15 | width: 300px;
16 | }
17 |
18 | .game-board {
19 | border: solid 2px;
20 | }
21 |
22 | .button-container {
23 | display: flex;
24 | flex-direction: row;
25 | justify-content: space-between;
26 |
27 | .play-button {
28 | background-color: #4caf50;
29 | }
30 |
31 | .reset-button {
32 | background-color: red;
33 | }
34 |
35 | .pause-button {
36 | background-color: #4caf50;
37 |
38 | &.button-disabled {
39 | background-color: #cccccc;
40 | }
41 | }
42 |
43 | .button {
44 | font-size: 16px;
45 | padding: 15px 30px;
46 | cursor: pointer;
47 | width: 140px;
48 | border: 1px solid black;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ng-tetris
6 |
7 |
8 |
9 |
10 |
12 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-tetris",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve -o",
7 | "build": "ng build",
8 | "build:prod": "ng build --prod",
9 | "test": "ng test",
10 | "lint": "ng lint"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "~9.1.9",
15 | "@angular/common": "~9.1.9",
16 | "@angular/compiler": "~9.1.9",
17 | "@angular/core": "~9.1.9",
18 | "@angular/forms": "~9.1.9",
19 | "@angular/platform-browser": "~9.1.9",
20 | "@angular/platform-browser-dynamic": "~9.1.9",
21 | "@angular/router": "~9.1.9",
22 | "ng-zzfx": "0.0.6",
23 | "rxjs": "~6.5.5",
24 | "tslib": "^1.10.0",
25 | "zone.js": "~0.10.2"
26 | },
27 | "devDependencies": {
28 | "@angular-devkit/build-angular": "~0.901.7",
29 | "@angular/cli": "~9.1.7",
30 | "@angular/compiler-cli": "~9.1.9",
31 | "@angular/language-service": "~9.1.9",
32 | "@types/node": "^12.11.1",
33 | "ts-node": "~7.0.0",
34 | "tslint": "~5.15.0",
35 | "typescript": "~3.8.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/board.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
TETRIS
6 |
Score: {{ points }}
7 |
Lines: {{ lines }}
8 |
Level: {{ level }}
9 |
High Score:
10 |
{{ highScore }}
11 |
Next Block:
12 |
13 |
14 |
15 |
22 |
29 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/app/game.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { IPiece } from './piece.component';
3 | import { COLS, ROWS, POINTS } from './constants';
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class GameService {
9 | valid(p: IPiece, board: number[][]): boolean {
10 | return p.shape.every((row, dy) => {
11 | return row.every((value, dx) => {
12 | let x = p.x + dx;
13 | let y = p.y + dy;
14 | return (
15 | this.isEmpty(value) ||
16 | (this.insideWalls(x) &&
17 | this.aboveFloor(y) &&
18 | this.notOccupied(board, x, y))
19 | );
20 | });
21 | });
22 | }
23 |
24 | isEmpty(value: number): boolean {
25 | return value === 0;
26 | }
27 |
28 | insideWalls(x: number): boolean {
29 | return x >= 0 && x < COLS;
30 | }
31 |
32 | aboveFloor(y: number): boolean {
33 | return y <= ROWS;
34 | }
35 |
36 | notOccupied(board: number[][], x: number, y: number): boolean {
37 | return board[y] && board[y][x] === 0;
38 | }
39 |
40 | rotate(piece: IPiece): IPiece {
41 | let p: IPiece = JSON.parse(JSON.stringify(piece));
42 | for (let y = 0; y < p.shape.length; ++y) {
43 | for (let x = 0; x < y; ++x) {
44 | [p.shape[x][y], p.shape[y][x]] = [p.shape[y][x], p.shape[x][y]];
45 | }
46 | }
47 | p.shape.forEach(row => row.reverse());
48 | return p;
49 | }
50 |
51 | getLinesClearedPoints(lines: number, level: number): number {
52 | const lineClearPoints =
53 | lines === 1
54 | ? POINTS.SINGLE
55 | : lines === 2
56 | ? POINTS.DOUBLE
57 | : lines === 3
58 | ? POINTS.TRIPLE
59 | : lines === 4
60 | ? POINTS.TETRIS
61 | : 0;
62 |
63 | return (level + 1) * lineClearPoints;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/constants.ts:
--------------------------------------------------------------------------------
1 | export const COLS = 10;
2 | export const ROWS = 20;
3 | export const BLOCK_SIZE = 30;
4 | export const LINES_PER_LEVEL = 10;
5 | export const COLORS = [
6 | 'none',
7 | 'rgba(0, 255, 255)',
8 | 'rgba(0, 0, 255)',
9 | 'rgba(255, 132, 0)',
10 | 'rgba(255, 255, 0)',
11 | 'rgba(0, 255, 0)',
12 | 'rgba(255, 0, 255)',
13 | 'rgba(255, 0, 0)',
14 | ];
15 | export const COLORSLIGHTER= [
16 | 'none',
17 | 'rgba(132, 255, 255)',
18 | 'rgba(132, 132, 255)',
19 | 'rgba(255, 195, 132)',
20 | 'rgba(255, 255, 132)',
21 | 'rgba(132, 255, 132)',
22 | 'rgba(255, 132, 255)',
23 | 'rgba(255, 132, 132)',
24 | ];
25 | export const COLORSDARKER= [
26 | 'none',
27 | 'rgba(0, 132, 132)',
28 | 'rgba(0, 0, 132)',
29 | 'rgba(132, 65, 0)',
30 | 'rgba(132, 132, 0)',
31 | 'rgba(0, 132, 0)',
32 | 'rgba(132, 0, 132)',
33 | 'rgba(132, 0, 0)',
34 | ];
35 | export const SHAPES = [
36 | [],
37 | [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
38 | [[2, 0, 0], [2, 2, 2], [0, 0, 0]],
39 | [[0, 0, 3], [3, 3, 3], [0, 0, 0]],
40 | [[4, 4], [4, 4]],
41 | [[0, 5, 5], [5, 5, 0], [0, 0, 0]],
42 | [[0, 6, 0], [6, 6, 6], [0, 0, 0]],
43 | [[7, 7, 0], [0, 7, 7], [0, 0, 0]]
44 | ];
45 |
46 | export class KEY {
47 | static readonly ESC = 27;
48 | static readonly SPACE = 32;
49 | static readonly LEFT = 37;
50 | static readonly UP = 38;
51 | static readonly RIGHT = 39;
52 | static readonly DOWN = 40;
53 | }
54 |
55 | export class POINTS {
56 | static readonly SINGLE = 100;
57 | static readonly DOUBLE = 300;
58 | static readonly TRIPLE = 500;
59 | static readonly TETRIS = 800;
60 | static readonly SOFT_DROP = 1;
61 | static readonly HARD_DROP = 2;
62 | }
63 |
64 | export class LEVEL {
65 | static readonly 0 = 800;
66 | static readonly 1 = 720;
67 | static readonly 2 = 630;
68 | static readonly 3 = 550;
69 | static readonly 4 = 470;
70 | static readonly 5 = 380;
71 | static readonly 6 = 300;
72 | static readonly 7 = 220;
73 | static readonly 8 = 130;
74 | static readonly 9 = 100;
75 | static readonly 10 = 80;
76 | static readonly 11 = 80;
77 | static readonly 12 = 80;
78 | static readonly 13 = 70;
79 | static readonly 14 = 70;
80 | static readonly 15 = 70;
81 | static readonly 16 = 50;
82 | static readonly 17 = 50;
83 | static readonly 18 = 50;
84 | static readonly 19 = 30;
85 | static readonly 20 = 30;
86 | // 29+ is 20ms
87 | }
88 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ng-tetris": {
7 | "projectType": "application",
8 | "schematics": {},
9 | "root": "",
10 | "sourceRoot": "src",
11 | "prefix": "app",
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/ng-tetris",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "tsconfig.app.json",
21 | "aot": true,
22 | "assets": [
23 | "src/favicon.ico",
24 | "src/assets"
25 | ],
26 | "styles": [
27 | "src/styles.scss"
28 | ],
29 | "scripts": []
30 | },
31 | "configurations": {
32 | "production": {
33 | "fileReplacements": [
34 | {
35 | "replace": "src/environments/environment.ts",
36 | "with": "src/environments/environment.prod.ts"
37 | }
38 | ],
39 | "optimization": true,
40 | "outputHashing": "all",
41 | "sourceMap": false,
42 | "extractCss": true,
43 | "namedChunks": false,
44 | "extractLicenses": true,
45 | "vendorChunk": false,
46 | "buildOptimizer": true,
47 | "budgets": [
48 | {
49 | "type": "initial",
50 | "maximumWarning": "2mb",
51 | "maximumError": "5mb"
52 | },
53 | {
54 | "type": "anyComponentStyle",
55 | "maximumWarning": "6kb",
56 | "maximumError": "10kb"
57 | }
58 | ]
59 | }
60 | }
61 | },
62 | "serve": {
63 | "builder": "@angular-devkit/build-angular:dev-server",
64 | "options": {
65 | "browserTarget": "ng-tetris:build"
66 | },
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "ng-tetris:build:production"
70 | }
71 | }
72 | },
73 | "extract-i18n": {
74 | "builder": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "browserTarget": "ng-tetris:build"
77 | }
78 | }
79 | }
80 | }
81 | },
82 | "defaultProject": "ng-tetris",
83 | "cli": {
84 | "analytics": false
85 | }
86 | }
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags.ts';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 |
61 | /***************************************************************************************************
62 | * APPLICATION IMPORTS
63 | */
64 |
--------------------------------------------------------------------------------
/src/app/piece.component.ts:
--------------------------------------------------------------------------------
1 | import { COLORS, SHAPES, COLORSLIGHTER, COLORSDARKER } from './constants';
2 |
3 | export interface IPiece {
4 | x: number;
5 | y: number;
6 | color: string;
7 | shape: number[][];
8 | }
9 |
10 | export class Piece implements IPiece {
11 | x: number;
12 | y: number;
13 | color: string;
14 | colorLighter: string;
15 | colorDarker: string;
16 | shape: number[][];
17 |
18 | constructor(private ctx: CanvasRenderingContext2D) {
19 | this.spawn();
20 | }
21 |
22 | spawn() {
23 | const typeId = this.randomizeTetrominoType(COLORS.length - 1);
24 | this.shape = SHAPES[typeId];
25 | this.color = COLORS[typeId];
26 | this.colorLighter = COLORSLIGHTER[typeId];
27 | this.colorDarker = COLORSDARKER[typeId];
28 | this.x = typeId === 4 ? 4 : 3;
29 | this.y = 0;
30 | }
31 |
32 | private add3D(ctx: CanvasRenderingContext2D, x: number, y: number): void {
33 | //Darker Color
34 | ctx.fillStyle = this.colorDarker;
35 | // Vertical
36 | ctx.fillRect(x + .9, y, .1, 1);
37 | // Horizontal
38 | ctx.fillRect(x, y + .9, 1, .1);
39 |
40 | //Darker Color - Inner
41 | // Vertical
42 | ctx.fillRect(x + .65, y + .3, .05, .3);
43 | // Horizontal
44 | ctx.fillRect(x + .3, y + .6, .4, .05);
45 |
46 | // Lighter Color - Outer
47 | ctx.fillStyle = this.colorLighter;
48 |
49 | // Lighter Color - Inner
50 | // Vertical
51 | ctx.fillRect(x + .3, y + .3, .05, .3);
52 | // Horizontal
53 | ctx.fillRect(x + .3, y + .3, .4, .05);
54 |
55 | // Lighter Color - Outer
56 | // Vertical
57 | ctx.fillRect(x, y, .05, 1);
58 | ctx.fillRect(x, y, .1, .95);
59 | // Horizontal
60 | ctx.fillRect(x, y, 1 , .05);
61 | ctx.fillRect(x, y, .95, .1);
62 | }
63 |
64 | private addNextShadow(ctx: CanvasRenderingContext2D, x: number, y: number): void {
65 | ctx.fillStyle = 'black';
66 | ctx.fillRect(x, y, 1.025, 1.025);
67 | }
68 |
69 | draw() {
70 | this.shape.forEach((row, y) => {
71 | row.forEach((value, x) => {
72 | if (value > 0) {
73 | this.ctx.fillStyle = this.color;
74 | const currentX = this.x + x;
75 | const currentY = this.y + y;
76 | this.ctx.fillRect(currentX, currentY, 1, 1);
77 | this.add3D(this.ctx, currentX, currentY);
78 | }
79 | });
80 | });
81 | }
82 |
83 | drawNext(ctxNext: CanvasRenderingContext2D) {
84 | ctxNext.clearRect(0, 0, ctxNext.canvas.width, ctxNext.canvas.height);
85 | this.shape.forEach((row, y) => {
86 | row.forEach((value, x) => {
87 | if (value > 0) {
88 | this.addNextShadow(ctxNext, x, y);
89 | }
90 | });
91 | });
92 |
93 | ctxNext.fillStyle = this.color;
94 | this.shape.forEach((row, y) => {
95 | row.forEach((value, x) => {
96 | if (value > 0) {
97 | ctxNext.fillStyle = this.color;
98 | const currentX = x + .025;
99 | const currentY = y + .025;
100 | ctxNext.fillRect(currentX, currentY, 1-.025, 1 -.025);
101 | this.add3D(ctxNext, currentX, currentY);
102 | }
103 | });
104 | });
105 | }
106 |
107 | move(p: IPiece) {
108 | this.x = p.x;
109 | this.y = p.y;
110 | this.shape = p.shape;
111 | }
112 |
113 | randomizeTetrominoType(noOfTypes: number): number {
114 | return Math.floor(Math.random() * noOfTypes + 1);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/board.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ViewChild,
4 | ElementRef,
5 | OnInit,
6 | HostListener
7 | } from '@angular/core';
8 | import {
9 | COLS,
10 | BLOCK_SIZE,
11 | ROWS,
12 | COLORS,
13 | COLORSLIGHTER,
14 | LINES_PER_LEVEL,
15 | LEVEL,
16 | POINTS,
17 | KEY,
18 | COLORSDARKER
19 | } from './constants';
20 | import { Piece, IPiece } from './piece.component';
21 | import { GameService } from './game.service';
22 | import { Zoundfx } from 'ng-zzfx';
23 |
24 | @Component({
25 | selector: 'game-board',
26 | templateUrl: 'board.component.html'
27 | })
28 | export class BoardComponent implements OnInit {
29 | @ViewChild('board', { static: true })
30 | canvas: ElementRef;
31 | @ViewChild('next', { static: true })
32 | canvasNext: ElementRef;
33 | ctx: CanvasRenderingContext2D;
34 | ctxNext: CanvasRenderingContext2D;
35 | board: number[][];
36 | piece: Piece;
37 | next: Piece;
38 | requestId: number;
39 | paused: boolean;
40 | gameStarted: boolean;
41 | time: { start: number; elapsed: number; level: number };
42 | points: number;
43 | highScore: number;
44 | lines: number;
45 | level: number;
46 | moves = {
47 | [KEY.LEFT]: (p: IPiece): IPiece => ({ ...p, x: p.x - 1 }),
48 | [KEY.RIGHT]: (p: IPiece): IPiece => ({ ...p, x: p.x + 1 }),
49 | [KEY.DOWN]: (p: IPiece): IPiece => ({ ...p, y: p.y + 1 }),
50 | [KEY.SPACE]: (p: IPiece): IPiece => ({ ...p, y: p.y + 1 }),
51 | [KEY.UP]: (p: IPiece): IPiece => this.service.rotate(p)
52 | };
53 | playSoundFn: Function;
54 |
55 | @HostListener('window:keydown', ['$event'])
56 | keyEvent(event: KeyboardEvent) {
57 | if (event.keyCode === KEY.ESC) {
58 | this.gameOver();
59 | } else if (this.moves[event.keyCode]) {
60 | event.preventDefault();
61 | // Get new state
62 | let p = this.moves[event.keyCode](this.piece);
63 | if (event.keyCode === KEY.SPACE) {
64 | // Hard drop
65 | while (this.service.valid(p, this.board)) {
66 | this.points += POINTS.HARD_DROP;
67 | this.piece.move(p);
68 | p = this.moves[KEY.DOWN](this.piece);
69 | }
70 | } else if (this.service.valid(p, this.board)) {
71 | this.piece.move(p);
72 | if (event.keyCode === KEY.DOWN) {
73 | this.points += POINTS.SOFT_DROP;
74 | }
75 | }
76 | }
77 | }
78 |
79 | constructor(private service: GameService) {}
80 |
81 | ngOnInit() {
82 | this.initBoard();
83 | this.initSound();
84 | this.initNext();
85 | this.resetGame();
86 | this.highScore = 0;
87 | }
88 |
89 | initSound() {
90 | this.playSoundFn = Zoundfx.start(0.2);
91 | }
92 |
93 | initBoard() {
94 | this.ctx = this.canvas.nativeElement.getContext('2d');
95 |
96 | // Calculate size of canvas from constants.
97 | this.ctx.canvas.width = COLS * BLOCK_SIZE;
98 | this.ctx.canvas.height = ROWS * BLOCK_SIZE;
99 |
100 | // Scale so we don't need to give size on every draw.
101 | this.ctx.scale(BLOCK_SIZE, BLOCK_SIZE);
102 | }
103 |
104 | initNext() {
105 | this.ctxNext = this.canvasNext.nativeElement.getContext('2d');
106 |
107 | // Calculate size of canvas from constants.
108 | // The + 2 is to allow for space to add the drop shadow to
109 | // the "next piece"
110 | this.ctxNext.canvas.width = 4 * BLOCK_SIZE + 2;
111 | this.ctxNext.canvas.height = 4 * BLOCK_SIZE;
112 |
113 | this.ctxNext.scale(BLOCK_SIZE, BLOCK_SIZE);
114 | }
115 |
116 | play() {
117 | this.gameStarted = true;
118 | this.resetGame();
119 | this.next = new Piece(this.ctx);
120 | this.piece = new Piece(this.ctx);
121 | this.next.drawNext(this.ctxNext);
122 | this.time.start = performance.now();
123 |
124 | // If we have an old game running a game then cancel the old
125 | if (this.requestId) {
126 | cancelAnimationFrame(this.requestId);
127 | }
128 |
129 | this.animate();
130 | }
131 |
132 | resetGame() {
133 | this.points = 0;
134 | this.lines = 0;
135 | this.level = 0;
136 | this.board = this.getEmptyBoard();
137 | this.time = { start: 0, elapsed: 0, level: LEVEL[this.level] };
138 | this.paused = false;
139 | this.addOutlines();
140 | }
141 |
142 | animate(now = 0) {
143 | this.time.elapsed = now - this.time.start;
144 | if (this.time.elapsed > this.time.level) {
145 | this.time.start = now;
146 | if (!this.drop()) {
147 | this.gameOver();
148 | return;
149 | }
150 | }
151 | this.draw();
152 | this.requestId = requestAnimationFrame(this.animate.bind(this));
153 | }
154 |
155 | draw() {
156 | this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
157 | this.piece.draw();
158 | this.drawBoard();
159 | }
160 |
161 | drop(): boolean {
162 | let p = this.moves[KEY.DOWN](this.piece);
163 | if (this.service.valid(p, this.board)) {
164 | this.piece.move(p);
165 | } else {
166 | this.freeze();
167 | this.clearLines();
168 | if (this.piece.y === 0) {
169 | // Game over
170 | return false;
171 | }
172 | this.playSoundFn([ , , 224,.02,.02,.08,1,1.7,-13.9 , , , , , ,6.7]);
173 | this.piece = this.next;
174 | this.next = new Piece(this.ctx);
175 | this.next.drawNext(this.ctxNext);
176 | }
177 | return true;
178 | }
179 |
180 | clearLines() {
181 | let lines = 0;
182 | this.board.forEach((row, y) => {
183 | if (row.every(value => value !== 0)) {
184 | lines++;
185 | this.board.splice(y, 1);
186 | this.board.unshift(Array(COLS).fill(0));
187 | }
188 | });
189 | if (lines > 0) {
190 | this.points += this.service.getLinesClearedPoints(lines, this.level);
191 | this.lines += lines;
192 | if (this.lines >= LINES_PER_LEVEL) {
193 | this.level++;
194 | this.lines -= LINES_PER_LEVEL;
195 | this.time.level = LEVEL[this.level];
196 | }
197 | }
198 | }
199 |
200 | freeze() {
201 | this.piece.shape.forEach((row, y) => {
202 | row.forEach((value, x) => {
203 | if (value > 0) {
204 | this.board[y + this.piece.y][x + this.piece.x] = value;
205 | }
206 | });
207 | });
208 | }
209 |
210 | private add3D(x: number, y: number, color: number): void {
211 | //Darker Color
212 | this.ctx.fillStyle = COLORSDARKER[color];
213 | // Vertical
214 | this.ctx.fillRect(x + .9, y, .1, 1);
215 | // Horizontal
216 | this.ctx.fillRect(x, y + .9, 1, .1);
217 |
218 | //Darker Color - Inner
219 | // Vertical
220 | this.ctx.fillRect(x + .65, y + .3, .05, .3);
221 | // Horizontal
222 | this.ctx.fillRect(x + .3, y + .6, .4, .05);
223 |
224 | // Lighter Color - Outer
225 | this.ctx.fillStyle = COLORSLIGHTER[color];
226 |
227 | // Lighter Color - Inner
228 | // Vertical
229 | this.ctx.fillRect(x + .3, y + .3, .05, .3);
230 | // Horizontal
231 | this.ctx.fillRect(x + .3, y + .3, .4, .05);
232 |
233 | // Lighter Color - Outer
234 | // Vertical
235 | this.ctx.fillRect(x, y, .05, 1);
236 | this.ctx.fillRect(x, y, .1, .95);
237 | // Horizontal
238 | this.ctx.fillRect(x, y, 1 , .05);
239 | this.ctx.fillRect(x, y, .95, .1);
240 | }
241 |
242 | private addOutlines() {
243 | for(let index = 1; index < COLS; index++) {
244 | this.ctx.fillStyle = 'black';
245 | this.ctx.fillRect(index, 0, .025, this.ctx.canvas.height);
246 | }
247 |
248 | for(let index = 1; index < ROWS; index++) {
249 | this.ctx.fillStyle = 'black';
250 | this.ctx.fillRect(0, index, this.ctx.canvas.width, .025);
251 | }
252 | }
253 |
254 | drawBoard() {
255 | this.board.forEach((row, y) => {
256 | row.forEach((value, x) => {
257 | if (value > 0) {
258 | this.ctx.fillStyle = COLORS[value];
259 | this.ctx.fillRect(x, y, 1, 1);
260 | this.add3D(x, y, value);
261 | }
262 | });
263 | });
264 | this.addOutlines();
265 | }
266 |
267 | pause() {
268 | if (this.gameStarted) {
269 | if (this.paused) {
270 | this.animate();
271 | } else {
272 | this.ctx.font = '1px Arial';
273 | this.ctx.fillStyle = 'black';
274 | this.ctx.fillText('GAME PAUSED', 1.4, 4);
275 | cancelAnimationFrame(this.requestId);
276 | }
277 |
278 | this.paused = !this.paused;
279 | }
280 | }
281 |
282 | gameOver() {
283 | this.gameStarted = false;
284 | cancelAnimationFrame(this.requestId);
285 | this.highScore = this.points > this.highScore ? this.points : this.highScore;
286 | this.ctx.fillStyle = 'black';
287 | this.ctx.fillRect(1, 3, 8, 1.2);
288 | this.ctx.font = '1px Arial';
289 | this.ctx.fillStyle = 'red';
290 | this.ctx.fillText('GAME OVER', 1.8, 4);
291 | }
292 |
293 | getEmptyBoard(): number[][] {
294 | return Array.from({ length: ROWS }, () => Array(COLS).fill(0));
295 | }
296 | }
297 |
--------------------------------------------------------------------------------