├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── components │ │ ├── button │ │ │ ├── button.component.html │ │ │ ├── button.component.scss │ │ │ └── button.component.ts │ │ ├── clock │ │ │ ├── clock.component.html │ │ │ ├── clock.component.scss │ │ │ └── clock.component.ts │ │ ├── github │ │ │ ├── github.component.html │ │ │ ├── github.component.scss │ │ │ └── github.component.ts │ │ ├── hold │ │ │ ├── hold.component.html │ │ │ ├── hold.component.scss │ │ │ └── hold.component.ts │ │ ├── keyboard │ │ │ ├── keyboard.component.html │ │ │ ├── keyboard.component.scss │ │ │ └── keyboard.component.ts │ │ ├── level │ │ │ ├── level.component.html │ │ │ ├── level.component.scss │ │ │ └── level.component.ts │ │ ├── logo │ │ │ ├── logo.component.html │ │ │ ├── logo.component.scss │ │ │ └── logo.component.ts │ │ ├── matrix │ │ │ ├── matrix.component.html │ │ │ ├── matrix.component.scss │ │ │ └── matrix.component.ts │ │ ├── next │ │ │ ├── next.component.html │ │ │ ├── next.component.scss │ │ │ └── next.component.ts │ │ ├── number │ │ │ ├── number.component.html │ │ │ ├── number.component.scss │ │ │ └── number.component.ts │ │ ├── pause │ │ │ ├── pause.component.html │ │ │ ├── pause.component.scss │ │ │ └── pause.component.ts │ │ ├── point │ │ │ ├── point.component.html │ │ │ ├── point.component.scss │ │ │ └── point.component.ts │ │ ├── screen-decoration │ │ │ ├── screen-decoration.component.html │ │ │ ├── screen-decoration.component.scss │ │ │ └── screen-decoration.component.ts │ │ ├── shared-button │ │ │ ├── shared-button.component.html │ │ │ ├── shared-button.component.scss │ │ │ └── shared-button.component.ts │ │ ├── sound │ │ │ ├── sound.component.html │ │ │ ├── sound.component.scss │ │ │ └── sound.component.ts │ │ ├── start-line │ │ │ ├── start-line.component.html │ │ │ ├── start-line.component.scss │ │ │ └── start-line.component.ts │ │ └── tile │ │ │ ├── tile.component.scss │ │ │ └── tile.component.ts │ ├── containers │ │ └── angular-tetris │ │ │ ├── angular-tetris.component.html │ │ │ ├── angular-tetris.component.scss │ │ │ └── angular-tetris.component.ts │ ├── factory │ │ └── piece-factory.ts │ ├── interface │ │ ├── callback.ts │ │ ├── game-state.ts │ │ ├── keyboard.ts │ │ ├── piece │ │ │ ├── Dot.ts │ │ │ ├── I.ts │ │ │ ├── J.ts │ │ │ ├── L.ts │ │ │ ├── O.ts │ │ │ ├── S.ts │ │ │ ├── T.ts │ │ │ ├── Z.ts │ │ │ ├── none.ts │ │ │ ├── piece-enum.ts │ │ │ ├── piece.ts │ │ │ └── shape.ts │ │ ├── speed.ts │ │ ├── tile │ │ │ ├── animated-tile.ts │ │ │ ├── empty-tile.ts │ │ │ ├── filled-tile.ts │ │ │ └── tile.ts │ │ ├── ui-model │ │ │ └── arrow-button.ts │ │ └── utils │ │ │ └── matrix.ts │ ├── services │ │ ├── google-analytics.service.ts │ │ ├── local-storage.service.ts │ │ └── sound-manager.service.ts │ ├── state │ │ ├── keyboard │ │ │ └── keyboard.service.ts │ │ └── tetris │ │ │ ├── tetris.service.ts │ │ │ └── tetris.state.ts │ └── styles │ │ ├── _number.scss │ │ ├── _reset.scss │ │ ├── _util.scss │ │ └── tetris.scss ├── assets │ ├── .gitkeep │ ├── favicon.png │ ├── img │ │ └── bg.png │ ├── js │ │ └── AudioContextMonkeyPatch.js │ ├── readme │ │ ├── akita-devtool.gif │ │ ├── angular-tetris-demo-sound.mp4 │ │ ├── angular-tetris-demo.gif │ │ ├── angular-tetris-iphonex.gif │ │ ├── compare01.png │ │ ├── compare02-result.gif │ │ ├── compare02.png │ │ ├── piecef-demo.gif │ │ ├── retro-tetris.jpg │ │ ├── tech-stack.png │ │ ├── tetris-social-cover.png │ │ └── time-spending.png │ └── tetris-sound.mp3 ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── index.prod.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 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'. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "plugins": [ 7 | "@angular-eslint", 8 | "unused-imports" 9 | ], 10 | "overrides": [ 11 | { 12 | "files": [ 13 | "*.ts" 14 | ], 15 | "parserOptions": { 16 | "project": [ 17 | "tsconfig.json", 18 | "e2e/tsconfig.json" 19 | ], 20 | "createDefaultProgram": true 21 | }, 22 | "extends": [ 23 | "eslint:recommended", 24 | "plugin:@typescript-eslint/recommended", 25 | "plugin:@angular-eslint/recommended", 26 | "plugin:@angular-eslint/template/process-inline-templates" 27 | ], 28 | "rules": { 29 | "unused-imports/no-unused-imports": "error", 30 | "@angular-eslint/component-selector": [ 31 | "error", 32 | { 33 | "type": "element", 34 | "prefix": "t", 35 | "style": "kebab-case" 36 | } 37 | ], 38 | "@angular-eslint/directive-selector": [ 39 | "error", 40 | { 41 | "type": "attribute", 42 | "prefix": "app", 43 | "style": "camelCase" 44 | } 45 | ], 46 | "@angular-eslint/no-output-on-prefix": "off", 47 | "@typescript-eslint/naming-convention": [ 48 | "error", 49 | { 50 | "selector": "default", 51 | "format": [ 52 | "camelCase" 53 | ], 54 | "leadingUnderscore": "allow", 55 | "trailingUnderscore": "allow" 56 | }, 57 | { 58 | "selector": "variable", 59 | "format": [ 60 | "camelCase", 61 | "UPPER_CASE", 62 | "PascalCase" 63 | ], 64 | "leadingUnderscore": "allow", 65 | "trailingUnderscore": "allow" 66 | }, 67 | { 68 | "selector": "typeLike", 69 | "format": [ 70 | "PascalCase" 71 | ] 72 | }, 73 | { 74 | "selector": "enumMember", 75 | "format": [ 76 | "camelCase", 77 | "PascalCase" 78 | ] 79 | } 80 | ], 81 | "@typescript-eslint/member-ordering": "off", 82 | "@typescript-eslint/no-empty-function": "off", 83 | "@typescript-eslint/no-inferrable-types": "off", 84 | "no-prototype-builtins": "off", 85 | "typescript-eslint/quotes": "off", 86 | "no-underscore-dangle": "off", 87 | "radix": "off" 88 | } 89 | }, 90 | { 91 | "files": [ 92 | "*.html" 93 | ], 94 | "extends": [ 95 | "plugin:@angular-eslint/template/recommended" 96 | ], 97 | "rules": {} 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [trungvose] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/trungvose'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | 48 | .angular 49 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": true 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Trung Vo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Tetris 2 | 3 | A childhood memory Tetris game built with Angular and Akita. 4 | 5 | ## Working Game 6 | 7 | Check out the **working game** -> https://tetris.trungk18.com 8 | 9 | The game has sounds, wear your 🎧 or turn on your 🔊 for a better experience. 10 | 11 | ![A childhood memory Tetris game built with Angular and Akita][demo] 12 | 13 | > Please tweet and tag me @trungvose for any issues that you are currently facing! 14 | > Thanks for your understanding. Stay tuned! 15 | 16 | ![A childhood memory Tetris game built with Angular and Akita][iphonex] 17 | 18 | ## Support 19 | 20 | If you like my work, feel free to: 21 | 22 | - ⭐ this repository. And we will be happy together :) 23 | - [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)][tweet] about Angular Tetris 24 | - Buy Me A Coffee 25 | 26 | Thanks a bunch for stopping by and supporting me! 27 | 28 | [tweet]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Ftrungk18%2Fangular-tetris&text=Awesome%20Tetris%20game%20built%20with%20Angular%2010%20and%20Akita%2C%20can%20you%20get%20999999%20points%3F&hashtags=angular,angulartetris,akita,typescript 29 | 30 | ## Why? 31 | 32 | Tetris was the first game that my dad bought for me and It cost about 1$ US at that time. It didn't sound a lot today. But 20 years ago, 1$ can feed my family for at least a few days. Put it that way, with 1\$ you can buy two dozens eggs. 33 | This is the only gaming "machine" that I ever had until my first computer arrived. I have never had a SNES or PS1 at home. 34 | 35 | My Tetris was exactly in the same yellow color and it was so big, running on 2 AA battery. It is how it looks. 36 | 37 | ![Retro Tetris][tetris] 38 | 39 | After showing my wife the [Tetris game built with Vue][vue]. She asked me why I didn't build the same Tetris with Angular? And here you go. 40 | 41 | > The game can hold up to a maximum score of 999999 (one million minus one 😂) and I have never reached that very end. 42 | > 43 | > Please [tweet][tweetmax] the screenshot with your highest score, together with hashtag `#angulartetris` and my name tagged as well `@trungvose`. I will send **a free gift** to the one having the highest score of the day, from now till 1 Aug 2020. 44 | 45 | [tweetmax]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Ftrungk18%2Fangular-tetris&text=Woo-hoo!%20I%20got%20a%20999999%20points%20on%20Angular%20Tetris%20%40trungvose.%20Wanna%20join%20the%20party%3F%20&hashtags=angular,angulartetris,akita,typescript 46 | 47 | ## Who is this for? 48 | 49 | I built this game dedicated to: 50 | 51 | - For anyone that grew up with Tetris as a part of your memory. It was my childhood memory and I hope you enjoy the game as well. 52 | - For the Angular developer community, I have never really seen a game that built with Angular and that's my answer. Using Akita as the underlying state management helps me to see all of the data flow, it is great for debugging. I wanted to see more Angular game from you guys 💪💪💪 53 | 54 | ## How to play 55 | 56 | ### Before playing 57 | 58 | - You can use both keyboard and mouse to play. But prefer to use keyboard 59 | - Press arrow left and right to change the speed of the game **(1 - 6)**. The higher the number, the faster the piece will fall 60 | - Press arrow up and down to change how many of lines have been filled before starting the game **(1 - 10)** 61 | - Press `Space` to start the game 62 | - Press `P` for pause/resume game 63 | - Press `R` for resetting the game 64 | - Press `S` for the turn on/off the sounds 65 | 66 | ### Playing game 67 | 68 | - Press `Space` make the piece drop quickly 69 | - Press `Arrow left` and `right` for moving left and right 70 | - Press `Arrow up` to rotate the piece 71 | - Press `Arrow down` to move a piece faster 72 | - When clearing lines, you will receive a point - 100 points for 1 line, 300 points for 2 lines, 700 points for 3 lines, 1500 points for 4 lines 73 | - The drop speed of the pieces increases with the number of rows eliminated (one level up for every 20 lines cleared) 74 | 75 | ## Techstack 76 | 77 | I built it barely with Angular and Akita, no additional UI framework/library was required. 78 | 79 | ![Angular Tetris][techstack] 80 | 81 | ## Development Challenge 82 | 83 | I got the inspiration from the same but different [Tetris game built with Vue][vue]. To not reinvented the wheel, I started to look at Vue code and thought it would be very identical to Angular. But later on, I realized a few catches: 84 | 85 | - The Vue source code was written a few years ago with pure JS. I could find several problems that the compiler didn't tell you. Such as giving `parseInt` a number. It is still working though, but I don't like it. 86 | - There was extensive use of `setTimeout` and `setInterval` for making animations. I rewrote all of the animation logic using RxJS. You will see the detail below. 87 | - The brain of the game also used `setTimeout` for the game loop. It was not a problem, but I was having a hard time understanding the code on some essential elements: how to render the piece to the UI, how the calculation makes sense with XY axis. In the end, I changed all of the logic to a proper OOP way using TypeScript class, based on [@chrum/ngx-tetris][ngx-tetris]. 88 | 89 | ### Tetris Core 90 | 91 | It is the most important part of the game. As I am following the Vue source code, It is getting harder to understand what was the developer's intention. The Vue version inspired me but I think I have to write the core Tetris differently. 92 | 93 | Take a look at the two blocks of code below which do the same rendering piece on the screen and you will understand what I am talking about. The left side was rewritten with Angular and TypeScript and the right side was the JS version. 94 | 95 | ![Angular Tetris][compare01] 96 | 97 | I always think that your code must be written as you talk to people, without explaining a word. Otherwise, when someone comes in and reads your code and maintains it, they will be struggling. 98 | 99 | > “ Code is like humor. When you have to explain it, it’s bad.” – Cory House 100 | 101 | And let me emphasize it again, I didn't write the brain of the game from scratch. I adapted the well-written source by [@chrum/ngx-tetris][ngx-tetris] for Tetris core. I did refactor some parts to support Akita and wrote some new functionality as well. 102 | 103 | ### Akita state management + dev tool support 104 | 105 | Although you don't dispatch any action, Akita will still do it undo the hood as the Update action. And you still can see the data with [Redux DevTools][redux-devtool]. Remember to put that option into your `AppModule` 106 | 107 | ```ts 108 | imports: [environment.production ? [] : AkitaNgDevtools.forRoot()]; 109 | ``` 110 | 111 | I turn it on all the time on [tetris.trungk18.com][angular-tetris], you can open the DevTools and start seeing the data flow. 112 | 113 | ![Angular Tetris][akita-devtool] 114 | 115 | > Note: opening the DevTools could reduce the performance of the game significantly. I recommended you turn it off when you want to archive a high score 🤓 116 | 117 | ### Customizing Piece 118 | 119 | I defined a base [Piece class][piece-class] for a piece. And for each type of piece, it will extend from the same base class to inherit the same capability 120 | 121 | [piece-class]: src/app/interface/piece/piece.ts 122 | 123 | ```ts 124 | export class Piece { 125 | x: number; 126 | y: number; 127 | rotation = PieceRotation.Deg0; 128 | type: PieceTypes; 129 | shape: Shape; 130 | next: Shape; 131 | 132 | private shapes: Shapes; 133 | private lastConfig: Partial; 134 | 135 | constructor(x: number, y: number) { 136 | this.x = x; 137 | this.y = y; 138 | } 139 | 140 | store(): Piece { 141 | this.lastConfig = { 142 | x: this.x, 143 | y: this.y, 144 | rotation: this.rotation, 145 | shape: this.shape 146 | }; 147 | return this.newPiece(); 148 | } 149 | 150 | //code removed for brevity 151 | } 152 | ``` 153 | 154 | For example, I have a piece L. I create a new class name [PieceL][piecel]. I will contain the shape of L in four different rotation so that I don't have to mess up with the math to do minus plus on the XY axis. And I think defining in that way makes the code self-express better. If you see 1, it means on the matrix it will be filled, 0 mean empty tile. 155 | 156 | If my team member needs to maintain the code, I hope he will understand what I was trying to write immediately. Or maybe not 🤣 157 | 158 | One import property of the Piece is the `next` property to display the piece shape on the decoration box for the upcoming piece. 159 | 160 | [piecel]: src/app/interface/piece/L.ts 161 | 162 | ```ts 163 | const ShapesL: Shapes = []; 164 | ShapesL[PieceRotation.Deg0] = [ 165 | [0, 0, 0, 0], 166 | [1, 0, 0, 0], 167 | [1, 0, 0, 0], 168 | [1, 1, 0, 0] 169 | ]; 170 | 171 | ShapesL[PieceRotation.Deg90] = [ 172 | [0, 0, 0, 0], 173 | [0, 0, 0, 0], 174 | [1, 1, 1, 0], 175 | [1, 0, 0, 0] 176 | ]; 177 | //code removed for brevity 178 | 179 | export class PieceL extends Piece { 180 | constructor(x: number, y: number) { 181 | super(x, y); 182 | this.type = PieceTypes.L; 183 | this.next = [ 184 | [0, 0, 1, 0], 185 | [1, 1, 1, 0] 186 | ]; 187 | this.setShapes(ShapesL); 188 | } 189 | } 190 | ``` 191 | 192 | Now is the interesting part, you create a custom piece by yourself. Simply create a new class that extends from `Piece` with different rotations. 193 | 194 | For instance, I will define a new piece call F with class name [`PieceF`][piecef]. That is how it should look like. 195 | 196 | [piecef]: https://github.com/trungk18/angular-tetris/blob/feature/pieceF/src/app/interface/piece/F.ts 197 | 198 | ```ts 199 | const ShapesF: Shapes = []; 200 | ShapesF[PieceRotation.Deg0] = [ 201 | [1, 0, 0, 0], 202 | [1, 1, 0, 0], 203 | [1, 0, 0, 0], 204 | [1, 1, 0, 0] 205 | ]; 206 | 207 | export class PieceF extends Piece { 208 | constructor(x, y) { 209 | super(x, y); 210 | this.type = PieceTypes.F; 211 | this.next = [ 212 | [1, 0, 1, 0], 213 | [1, 1, 1, 1] 214 | ]; 215 | this.setShapes(ShapesF); 216 | } 217 | } 218 | ``` 219 | 220 | And the last step, go to [PieceFactory][piecefactory] to add the new PieceF into the available pieces. 221 | 222 | [piecefactory]: src/app/factory/piece-factory.ts 223 | 224 | ```ts 225 | export class PieceFactory { 226 | private available: typeof Piece[] = []; 227 | 228 | constructor() { 229 | //code removed for brevity 230 | this.available.push(PieceF); 231 | } 232 | } 233 | ``` 234 | 235 | And you're all set, this is the result. See how easy it is to understand the code and add a custom piece that you like. 236 | 237 | The source code for that custom piece F, you can see at [feature/pieceF][feature/piecef] branch. 238 | 239 | ![Angular Tetris Piece F][piecef-demo] 240 | 241 | [feature/piecef]: https://github.com/trungk18/angular-tetris/tree/feature/pieceF 242 | [piecef-demo]: src/assets/readme/piecef-demo.gif 243 | 244 | ### Animation 245 | 246 | I rewrote the animation with RxJS. See the comparison below for the simple dinosaurs running animation at the beginning of the game. 247 | 248 | You could do a lot of stuff if you know RxJS well enough :) I think I need to strengthen my RxJS knowledge soon enough as well. Super powerful. 249 | 250 | ![Angular Tetris][compare02] 251 | 252 | The actual result doesn't look very identical but it is good enough in my standard. 253 | 254 | ![Angular Tetris][compare02-result] 255 | 256 | ### Web Audio API 257 | 258 | There are many sound effects in the game such as when you press space, or left, right. In reality, all of the sounds were a reference to a single file [assets/tetris-sound.mp3][sounds]. 259 | 260 | I don't have much experience working with audio before but the Web Audio API looks very promising. You could do more with it. 261 | 262 | - See the [official documentation][webaudio] 263 | - See how I load the mp3 file and store it in [sound-manager.service.ts][sound-manager] 264 | - [Writing Web Audio API code that works in every browser][web_audio_api_cross_browser] 265 | 266 | ### Keyboard handling 267 | 268 | I planned to use [@ngneat/hotkeys][hotkeys] but I decided to use `@HostListener` instead. A simple implementation could look like: 269 | 270 | ```typescript 271 | @HostListener(`${KeyDown}.${TetrisKeyboard.Left}`) 272 | keyDownLeft() { 273 | this.soundManager.move(); 274 | this.keyboardService.setKeỵ({ 275 | left: true 276 | }); 277 | if (this.hasCurrent) { 278 | this.tetrisService.moveLeft(); 279 | } else { 280 | this.tetrisService.decreaseLevel(); 281 | } 282 | } 283 | ``` 284 | 285 | See more at [containers/angular-tetris/angular-tetris.component.ts][hotkeys-implementation] 286 | 287 | ## Features and Roadmap 288 | 289 | ### Phase 1 - Angular Tetris basic functionality 290 | 291 | > July 10 - 23, 2020 292 | 293 | - [x] Proven, scalable, and easy to understand project structure 294 | - [x] Basic Tetris functionality 295 | - [x] Six levels 296 | - [x] Local storage high score 297 | - [x] Sounds effects 298 | - [x] Limited mobile support 299 | 300 | ### Phase 2 - Firebase high score, service worker, more sounds effect, more animation 301 | 302 | > TBD 303 | 304 | - [ ] Fully mobile support 305 | - [ ] Offline mode (play without internet connection) 306 | - [ ] Firebase high score 307 | - [ ] More sound effects 308 | - [ ] More animations 309 | 310 | ## Time spending 311 | 312 | I was still working with [Chau Tran][chautran] on phase two of [Angular Jira clone][jira-clone] when I saw that Tetris game built with Vue. My wife wanted to have a version that I built so that I decided to finish the Angular Tetris first before completing Jira clone phase two. 313 | 314 | According to waka time report, I have spent about 30 hours working on this project. Which is equal to [run a marathon five times][marathon] at my current speed 😩 315 | 316 | ![Angular Tetris][timespending] 317 | 318 | The flow was easy. I designed a simple [to do list][todolist], then start reading the code in Vue. And start working on the Angular at the same time. Halfway, I start to read [@chrum/ngx-tetris][ngx-tetris] instead of the Vue source. And keep building until I have the final result. 30 hours was a good number. It would take me longer, or lesser. But I enjoyed the experience working on the first-ever game I have built. 319 | 320 | ## Setting up development environment 🛠 321 | 322 | - `git clone https://github.com/trungk18/angular-tetris.git` 323 | - `cd angular-tetris` 324 | - `npm start` 325 | - The app should run on `http://localhost:4200/` 326 | 327 | ## Author: Trung Vo ✍️ 328 | 329 | - A young and passionate front-end engineer. Working with Angular and TypeScript. Like photography, running, cooking, and reading books. 330 | - Author of Angular Jira clone -> [jira.trungk18.com][jira-clone] 331 | - Personal blog: https://trungk18.com/ 332 | - Say hello: trungk18 [et] gmail [dot] com 333 | 334 | ## Credits and references 335 | 336 | | Resource | Description | 337 | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | 338 | | [@Binaryify/vue-tetris][vue] | Vue Tetris, I reused part of HTML, CSS and static assets from that project | 339 | | [@chrum/ngx-tetris][ngx-tetris] | A comprehensive core Tetris written with Angular, I reused part of that for the brain of the game. | 340 | | [Game Development: Tetris in Angular][medium] | A detailed excellent article about how to build a complete Tetris game. I didn't check the code but I learned much more from that | 341 | | [Super Rotation System][srs] | A standard for how the piece behaves. I didn't follow everything but it is good to know as wells | 342 | 343 | ## Contributing 344 | 345 | If you have any ideas, just [open an issue][issues] and tell me what you think. 346 | 347 | If you'd like to contribute, please fork the repository and make changes as you'd like. [Pull requests][pull] are warmly welcome. 348 | 349 | ## License 350 | 351 | Feel free to use my code on your project. It would be great if you put a reference to this repository. 352 | 353 | [MIT](https://opensource.org/licenses/MIT) 354 | 355 | [issues]: https://github.com/trungk18/angular-tetris/issues/new/choose 356 | [pull]: https://github.com/trungk18/angular-tetris/pulls 357 | [angular-tetris]: https://tetris.trungk18.com 358 | [medium]: https://medium.com/angular-in-depth/game-development-tetris-in-angular-64ef96ce56f7 359 | [srs]: https://tetris.fandom.com/wiki/SRS 360 | [vue]: https://github.com/Binaryify/vue-tetris 361 | [tetris]: src/assets/readme/retro-tetris.jpg 362 | [demo]: src/assets/readme/angular-tetris-demo.gif 363 | [iphonex]: src/assets/readme/angular-tetris-iphonex.gif 364 | [ngx-tetris]: https://github.com/chrum/ngx-tetris 365 | [techstack]: src/assets/readme/tech-stack.png 366 | [compare01]: src/assets/readme/compare01.png 367 | [compare02]: src/assets/readme/compare02.png 368 | [compare02-result]: src/assets/readme/compare02-result.gif 369 | [timespending]: src/assets/readme/time-spending.png 370 | [akita-devtool]: src/assets/readme/akita-devtool.gif 371 | [sounds]: src/assets/tetris-sound.mp3 372 | [sound-manager]: src/app/services/sound-manager.service.ts 373 | [webaudio]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API 374 | [redux-devtool]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en 375 | [hotkeys]: https://github.com/ngneat/hotkeys 376 | [hotkeys-implementation]: src/app/containers/angular-tetris/angular-tetris.component.ts 377 | [chautran]: https://github.com/nartc 378 | [jira-clone]: https://github.com/trungk18/jira-clone-angular 379 | [marathon]: https://www.strava.com/activities/2902245728 380 | [todolist]: https://www.notion.so/trungk18/Phase-1-be1ae0fbbf2c4c2fb92887e2218413db 381 | [web_audio_api_cross_browser]: https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Web_Audio_API_cross_browser 382 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": "ea20bcd9-1c36-4071-913f-75da1051415d" 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "angular-tetris": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss", 14 | "skipTests": true 15 | }, 16 | "@schematics/angular:class": { 17 | "skipTests": true 18 | }, 19 | "@schematics/angular:directive": { 20 | "skipTests": true 21 | }, 22 | "@schematics/angular:guard": { 23 | "skipTests": true 24 | }, 25 | "@schematics/angular:interceptor": { 26 | "skipTests": true 27 | }, 28 | "@schematics/angular:module": { 29 | "skipTests": true 30 | }, 31 | "@schematics/angular:pipe": { 32 | "skipTests": true 33 | }, 34 | "@schematics/angular:service": { 35 | "skipTests": true 36 | } 37 | }, 38 | "root": "", 39 | "sourceRoot": "src", 40 | "prefix": "t", 41 | "architect": { 42 | "build": { 43 | "builder": "@angular-devkit/build-angular:browser", 44 | "options": { 45 | "outputPath": "dist/angular-tetris", 46 | "index": "src/index.html", 47 | "main": "src/main.ts", 48 | "polyfills": "src/polyfills.ts", 49 | "tsConfig": "tsconfig.app.json", 50 | "aot": true, 51 | "assets": [ 52 | "src/favicon.ico", 53 | "src/assets" 54 | ], 55 | "styles": [ 56 | "src/styles.scss" 57 | ], 58 | "scripts": [] 59 | }, 60 | "configurations": { 61 | "production": { 62 | "fileReplacements": [ 63 | { 64 | "replace": "src/environments/environment.ts", 65 | "with": "src/environments/environment.prod.ts" 66 | } 67 | ], 68 | "index": { 69 | "input": "src/index.prod.html", 70 | "output": "index.html" 71 | }, 72 | "optimization": true, 73 | "outputHashing": "all", 74 | "sourceMap": true, 75 | "namedChunks": false, 76 | "extractLicenses": true, 77 | "vendorChunk": false, 78 | "buildOptimizer": true, 79 | "budgets": [ 80 | { 81 | "type": "initial", 82 | "maximumWarning": "2mb", 83 | "maximumError": "5mb" 84 | }, 85 | { 86 | "type": "anyComponentStyle", 87 | "maximumWarning": "6kb", 88 | "maximumError": "10kb" 89 | } 90 | ] 91 | }, 92 | "development": { 93 | "buildOptimizer": false, 94 | "optimization": false, 95 | "vendorChunk": true, 96 | "extractLicenses": false, 97 | "sourceMap": true, 98 | "namedChunks": true 99 | } 100 | } 101 | }, 102 | "serve": { 103 | "builder": "@angular-devkit/build-angular:dev-server", 104 | "configurations": { 105 | "production": { 106 | "browserTarget": "angular-tetris:build:production" 107 | }, 108 | "development": { 109 | "browserTarget": "angular-tetris:build:development" 110 | } 111 | }, 112 | "defaultConfiguration": "development" 113 | }, 114 | "extract-i18n": { 115 | "builder": "@angular-devkit/build-angular:extract-i18n", 116 | "options": { 117 | "browserTarget": "angular-tetris:build" 118 | } 119 | }, 120 | "test": { 121 | "builder": "@angular-devkit/build-angular:karma", 122 | "options": { 123 | "main": "src/test.ts", 124 | "polyfills": "src/polyfills.ts", 125 | "tsConfig": "tsconfig.spec.json", 126 | "karmaConfig": "karma.conf.js", 127 | "assets": [ 128 | "src/favicon.ico", 129 | "src/assets" 130 | ], 131 | "styles": [ 132 | "src/styles.scss" 133 | ], 134 | "scripts": [] 135 | } 136 | }, 137 | "lint": { 138 | "builder": "@angular-eslint/builder:lint", 139 | "options": { 140 | "lintFilePatterns": [ 141 | "src/**/*.ts", 142 | "src/**/*.html" 143 | ] 144 | } 145 | }, 146 | "e2e": { 147 | "builder": "@angular-devkit/build-angular:protractor", 148 | "options": { 149 | "protractorConfig": "e2e/protractor.conf.js", 150 | "devServerTarget": "angular-tetris:serve" 151 | }, 152 | "configurations": { 153 | "production": { 154 | "devServerTarget": "angular-tetris:serve:production" 155 | } 156 | } 157 | } 158 | } 159 | } 160 | }, 161 | "schematics": { 162 | "@angular-eslint/schematics:application": { 163 | "setParserOptionsProject": true 164 | }, 165 | "@angular-eslint/schematics:library": { 166 | "setParserOptionsProject": true 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('angular-tetris app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain( 20 | jasmine.objectContaining({ 21 | level: logging.Level.SEVERE 22 | } as logging.Entry) 23 | ); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/angular-tetris'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tetris", 3 | "version": "1.0.0", 4 | "author": { 5 | "name": "Trung Vo", 6 | "email": "trungk18@gmail.com", 7 | "url": "https://github.com/trungk18/angular-tetris" 8 | }, 9 | "description": "Tetris game built with Angular and Akita 🎮", 10 | "scripts": { 11 | "ng": "ng", 12 | "start": "ng serve", 13 | "build": "ng build --configuration production", 14 | "test": "ng test", 15 | "lint": "ng lint", 16 | "lint:fix": "ng lint --fix", 17 | "e2e": "ng e2e" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "npm run lint" 22 | } 23 | }, 24 | "private": true, 25 | "dependencies": { 26 | "@angular/animations": "~16.0.3", 27 | "@angular/common": "~16.0.3", 28 | "@angular/compiler": "~16.0.3", 29 | "@angular/core": "~16.0.3", 30 | "@angular/forms": "~16.0.3", 31 | "@angular/platform-browser": "~16.0.3", 32 | "@angular/platform-browser-dynamic": "~16.0.3", 33 | "@angular/router": "~16.0.3", 34 | "@datorama/akita": "^8.0.1", 35 | "@ngneat/until-destroy": "^7.3.2", 36 | "@sentry/angular": "7.27.0", 37 | "@sentry/tracing": "7.27.0", 38 | "rxjs": "^7.4.0", 39 | "tslib": "2.4.1", 40 | "zone.js": "~0.13.0" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/architect": "^0.1600.3", 44 | "@angular-devkit/build-angular": "~16.0.3", 45 | "@angular-eslint/builder": "16.0.2", 46 | "@angular-eslint/eslint-plugin": "16.0.2", 47 | "@angular-eslint/eslint-plugin-template": "16.0.2", 48 | "@angular-eslint/schematics": "16.0.2", 49 | "@angular-eslint/template-parser": "16.0.2", 50 | "@angular/cli": "~16.0.3", 51 | "@angular/compiler-cli": "~16.0.3", 52 | "@datorama/akita-ngdevtools": "7.0.0", 53 | "@types/jasmine": "~3.6.0", 54 | "@types/jasminewd2": "~2.0.3", 55 | "@types/node": "^12.11.1", 56 | "@typescript-eslint/eslint-plugin": "5.59.2", 57 | "@typescript-eslint/parser": "5.59.2", 58 | "eslint": "^8.40.0", 59 | "eslint-plugin-import": "2.25.2", 60 | "eslint-plugin-jsdoc": "30.7.6", 61 | "eslint-plugin-prefer-arrow": "1.2.2", 62 | "eslint-plugin-unused-imports": "3.0.0", 63 | "husky": "^4.3.8", 64 | "jasmine-core": "~3.6.0", 65 | "jasmine-spec-reporter": "~5.0.0", 66 | "karma": "~6.4.1", 67 | "karma-chrome-launcher": "~3.1.0", 68 | "karma-coverage-istanbul-reporter": "~3.0.2", 69 | "karma-jasmine": "~4.0.0", 70 | "karma-jasmine-html-reporter": "^1.5.0", 71 | "protractor": "~7.0.0", 72 | "ts-node": "~8.3.0", 73 | "typescript": "~5.0.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AngularTetrisComponent } from './containers/angular-tetris/angular-tetris.component'; 3 | 4 | @Component({ 5 | standalone: true, 6 | selector: 'app-root', //eslint-disable-line 7 | imports: [AngularTetrisComponent], 8 | template: '', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent {} 12 | -------------------------------------------------------------------------------- /src/app/components/button/button.component.html: -------------------------------------------------------------------------------- 1 |
4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 |
-------------------------------------------------------------------------------- /src/app/components/button/button.component.scss: -------------------------------------------------------------------------------- 1 | $blue: #5a65f1; 2 | $green: #2dc421; 3 | $red: #dd1a1a; 4 | 5 | @mixin background($color) { 6 | background: $color; 7 | } 8 | 9 | .button { 10 | position: absolute; 11 | text-align: center; 12 | color: #111; 13 | position: absolute; 14 | white-space: nowrap; 15 | line-height: 1.6; 16 | cursor: pointer; 17 | span { 18 | &.absolute { 19 | position: absolute; 20 | top: 5px; 21 | left: 102px; 22 | } 23 | } 24 | 25 | i { 26 | display: block; 27 | position: relative; 28 | border: 1px solid #000; 29 | border-radius: 50%; 30 | &:before, 31 | &:after { 32 | content: ''; 33 | display: block; 34 | width: 100%; 35 | height: 100%; 36 | position: absolute; 37 | top: 0; 38 | left: 0; 39 | border-radius: 50%; 40 | box-shadow: 0 5px 10px rgba(255, 255, 255, 0.8) inset; 41 | } 42 | 43 | &:after { 44 | box-shadow: 0 -5px 10px rgba(0, 0, 0, 0.8) inset; 45 | } 46 | 47 | &.active { 48 | &:before { 49 | box-shadow: 0 -3px 6px rgba(255, 255, 255, 0.6) inset; 50 | } 51 | &:after { 52 | box-shadow: 0 5px 5px rgba(0, 0, 0, 0.6) inset; 53 | } 54 | } 55 | box-shadow: 0 3px 3px rgba(0, 0, 0, 0.2); 56 | } 57 | 58 | &.blue i { 59 | @include background($blue); 60 | } 61 | 62 | &.green i { 63 | @include background($green); 64 | } 65 | 66 | &.red i { 67 | @include background($red); 68 | } 69 | 70 | &.btn-lg { 71 | i { 72 | width: 160px; 73 | height: 160px; 74 | } 75 | } 76 | 77 | &.btn-sm { 78 | font-size: 16px; 79 | 80 | i { 81 | width: 52px; 82 | height: 52px; 83 | &:before, 84 | &:after { 85 | box-shadow: 0px 3px 6px rgba(255, 255, 255, 0.8) inset; 86 | } 87 | &:after { 88 | box-shadow: 0px -3px 6px rgba(0, 0, 0, 0.8) inset; 89 | } 90 | &.active { 91 | &:before { 92 | box-shadow: 0px -1px 2px rgba(255, 255, 255, 0.6) inset; 93 | } 94 | &:after { 95 | box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.7) inset; 96 | } 97 | } 98 | box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); 99 | } 100 | } 101 | 102 | &.btn-md { 103 | em { 104 | display: block; 105 | width: 0; 106 | height: 0; 107 | border: 8px solid; 108 | border-color: transparent transparent #111; 109 | position: absolute; 110 | top: 50%; 111 | left: 50%; 112 | margin: -12px 0 0 -8px; 113 | } 114 | 115 | i { 116 | width: 100px; 117 | height: 100px; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/components/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { ArrowButton, ArrowButtonTransform } from '@angular-tetris/interface/ui-model/arrow-button'; 2 | import { NgClass, NgIf, NgStyle } from '@angular/common'; 3 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 4 | @Component({ 5 | selector: 't-button', 6 | standalone: true, 7 | imports: [NgClass, NgStyle, NgIf], 8 | templateUrl: './button.component.html', 9 | styleUrls: ['./button.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class ButtonComponent { 13 | @Input() className = ''; 14 | @Input() isAbsolute = false; 15 | @Input() top: number; 16 | @Input() left: number; 17 | 18 | @Input() active: boolean; 19 | @Input() arrowButton: ArrowButton; 20 | 21 | get arrowTransforms() { 22 | return ArrowButtonTransform[this.arrowButton]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/clock/clock.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 |
-------------------------------------------------------------------------------- /src/app/components/clock/clock.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/app/components/clock/clock.component.scss -------------------------------------------------------------------------------- /src/app/components/clock/clock.component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPipe, NgClass, NgFor } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 4 | import { map, timer } from 'rxjs'; 5 | 6 | const REFRESH_CLOCK_INTERVAL = 1000; 7 | @UntilDestroy() 8 | @Component({ 9 | selector: 't-clock', 10 | standalone: true, 11 | imports: [NgClass, NgFor, AsyncPipe], 12 | templateUrl: './clock.component.html', 13 | styleUrls: ['./clock.component.scss'], 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class ClockComponent { 17 | clock$ = timer(0, REFRESH_CLOCK_INTERVAL).pipe( 18 | untilDestroyed(this), 19 | map(() => this.renderClock()) 20 | ); 21 | 22 | renderClock(): string[] { 23 | const now = new Date(); 24 | const hours = this.formatTwoDigits(now.getHours()); 25 | const minutes = this.formatTwoDigits(now.getMinutes()); 26 | const isOddSecond = now.getSeconds() % 2 !== 0; 27 | const blinking = `colon-${isOddSecond ? 'solid' : 'faded'}`; 28 | return [...hours, blinking, ...minutes]; 29 | } 30 | 31 | formatTwoDigits(num: number): string[] { 32 | return `${num}`.padStart(2, '0').split(''); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/github/github.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Scan QR code to play with a mobile phone 5 |
6 | 7 |
8 | 13 | 18 | 23 | 28 | 33 |
34 | 36 |
37 |
-------------------------------------------------------------------------------- /src/app/components/github/github.component.scss: -------------------------------------------------------------------------------- 1 | .d-flex { 2 | position: fixed; 3 | top: 50%; 4 | transform: translateY(-50%); 5 | left: -300px; 6 | 7 | .qr { 8 | left: auto; 9 | top: 5%; 10 | text-align: left; 11 | cursor: pointer; 12 | 13 | .hint { 14 | margin-bottom: 10px; 15 | font-size: 14px; 16 | max-width: 300px; 17 | color: #cfd2d6; 18 | opacity: 0; 19 | } 20 | 21 | img { 22 | width: 60px; 23 | height: 60px; 24 | transition: transform 0.2s; 25 | transform-origin: 0 0; 26 | &:hover { 27 | transform: scale(4.5); 28 | } 29 | } 30 | } 31 | 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | .tweet-button { 37 | cursor: pointer; 38 | margin-top: 7px; 39 | margin-bottom: 10px; 40 | 41 | &.top { 42 | margin-top: 15px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/components/github/github.component.ts: -------------------------------------------------------------------------------- 1 | import { GoogleAnalyticsService } from '@angular-tetris/services/google-analytics.service'; 2 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 3 | import { NgIf } from '@angular/common'; 4 | import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; 5 | import { SharedButtonComponent } from '../shared-button/shared-button.component'; 6 | const HASHTAG = 'angular,angulartetris,akita,typescript'; 7 | 8 | @Component({ 9 | selector: 't-github', 10 | standalone: true, 11 | imports: [SharedButtonComponent, NgIf], 12 | templateUrl: './github.component.html', 13 | styleUrls: ['./github.component.scss'], 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class GithubComponent { 17 | private tetrisState = inject(TetrisStateService); 18 | private googleAnalytics = inject(GoogleAnalyticsService); 19 | 20 | max = this.tetrisState.max; 21 | 22 | tweetMaxScoreShareUrl = computed(() => { 23 | const text = encodeURIComponent( 24 | `Woo-hoo! I got a ${this.max()} points on Angular Tetris @trungvose. Wanna join the party?` 25 | ); 26 | return `https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Ftrungk18%2Fangular-tetris&text=${text}&hashtags=${HASHTAG}`; 27 | }); 28 | 29 | //eslint-disable-next-line max-len 30 | tweetAngularTetrisUrl = `https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Ftrungk18%2Fangular-tetris&text=Awesome%20Tetris%20game%20built%20with%20Angular%2010%20and%20Akita%2C%20can%20you%20get%20999999%20points%3F&hashtags=${HASHTAG}`; 31 | 32 | sendTwitterShareMaxScoreEvent() { 33 | this.googleAnalytics.sendEvent('Share Twitter High Score', 'button'); 34 | } 35 | 36 | sendTwitterShareEvent() { 37 | this.googleAnalytics.sendEvent('Share Twitter', 'button'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/hold/hold.component.html: -------------------------------------------------------------------------------- 1 |

Hold

2 |
3 |
5 | 7 | 8 |
9 |
-------------------------------------------------------------------------------- /src/app/components/hold/hold.component.scss: -------------------------------------------------------------------------------- 1 | .hold { 2 | .row { 3 | height: 22px; 4 | width: 88px; 5 | float: right; 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/components/hold/hold.component.ts: -------------------------------------------------------------------------------- 1 | import { Tile, TileValue } from '@angular-tetris/interface/tile/tile'; 2 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 3 | import { NgFor } from '@angular/common'; 4 | import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; 5 | import { TileComponent } from '../tile/tile.component'; 6 | 7 | @Component({ 8 | selector: 't-hold', 9 | standalone: true, 10 | imports: [NgFor, TileComponent], 11 | templateUrl: './hold.component.html', 12 | styleUrls: ['./hold.component.scss'], 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class HoldComponent { 16 | private tetrisState = inject(TetrisStateService); 17 | 18 | hold = computed(() => 19 | this.tetrisState.hold().next.map((row) => row.map((value) => new Tile(value as TileValue))) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/keyboard/keyboard.component.html: -------------------------------------------------------------------------------- 1 |
3 | 13 | Rotation 14 | 15 | 24 | Down 25 | 26 | 35 | Left 36 | 37 | 46 | Right 47 | 48 | 56 | Hold (C) 57 | 58 | 66 | Drop (SPACE) 67 | 68 | 76 | Reset (R) 77 | 78 | 86 | Sound (S) 87 | 88 | 96 | {{ pauseButtonLabel()}} (P) 97 | 98 |
-------------------------------------------------------------------------------- /src/app/components/keyboard/keyboard.component.scss: -------------------------------------------------------------------------------- 1 | .keyboard { 2 | width: 580px; 3 | height: 330px; 4 | margin: 15px auto 0; 5 | position: relative; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/keyboard/keyboard.component.ts: -------------------------------------------------------------------------------- 1 | import { GameState } from '@angular-tetris/interface/game-state'; 2 | import { ArrowButton } from '@angular-tetris/interface/ui-model/arrow-button'; 3 | import { KeyboardService } from '@angular-tetris/state/keyboard/keyboard.service'; 4 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 5 | import { 6 | ChangeDetectionStrategy, 7 | Component, 8 | EventEmitter, 9 | Input, 10 | Output, 11 | computed, 12 | inject 13 | } from '@angular/core'; 14 | import { ButtonComponent } from '../button/button.component'; 15 | 16 | @Component({ 17 | selector: 't-keyboard', 18 | standalone: true, 19 | imports: [ButtonComponent], 20 | templateUrl: './keyboard.component.html', 21 | styleUrls: ['./keyboard.component.scss'], 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class KeyboardComponent { 25 | private tetrisState = inject(TetrisStateService); 26 | keyboardService = inject(KeyboardService); 27 | 28 | @Input() filling = 20; 29 | @Output() onMouseDown = new EventEmitter(); 30 | @Output() onMouseUp = new EventEmitter(); 31 | ArrowButton = ArrowButton; //eslint-disable-line @typescript-eslint/naming-convention 32 | 33 | pauseButtonLabel = computed(() => 34 | this.tetrisState.gameState() === GameState.Paused ? 'Play' : 'Pause' 35 | ); 36 | 37 | mouseDown(e: Event, key: string) { 38 | e.preventDefault(); 39 | this.onMouseDown.emit(key); 40 | } 41 | 42 | mouseUp(e: Event, key: string) { 43 | e.preventDefault(); 44 | this.onMouseUp.emit(key); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/components/level/level.component.html: -------------------------------------------------------------------------------- 1 |

Level

2 | 5 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/components/level/level.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/app/components/level/level.component.scss -------------------------------------------------------------------------------- /src/app/components/level/level.component.ts: -------------------------------------------------------------------------------- 1 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 2 | import { NgIf } from '@angular/common'; 3 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 4 | import { NumberComponent } from '../number/number.component'; 5 | 6 | @Component({ 7 | selector: 't-level', 8 | standalone: true, 9 | imports: [NgIf, NumberComponent], 10 | templateUrl: './level.component.html', 11 | styleUrls: ['./level.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class LevelComponent { 15 | private tetrisState = inject(TetrisStateService); 16 | 17 | speed = this.tetrisState.speed; 18 | hasCurrent = this.tetrisState.hasCurrent; 19 | initSpeed = this.tetrisState.initSpeed; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/logo/logo.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/logo/logo.component.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 224px; 3 | height: 200px; 4 | position: absolute; 5 | top: 100px; 6 | left: 12px; 7 | text-align: center; 8 | overflow: hidden; 9 | 10 | p { 11 | position: absolute; 12 | width: 100%; 13 | line-height: 1.4; 14 | top: 100px; 15 | left: 0; 16 | font-family: initial; 17 | letter-spacing: 2px; 18 | text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.35); 19 | } 20 | 21 | .dragon { 22 | width: 80px; 23 | height: 86px; 24 | margin: 0 auto; 25 | background-position: 0 -100px; 26 | &.r1, 27 | &.l1 { 28 | background-position: 0 -100px; 29 | } 30 | &.r2, 31 | &.l2 { 32 | background-position: -100px -100px; 33 | } 34 | &.r3, 35 | &.l3 { 36 | background-position: -200px -100px; 37 | } 38 | &.r4, 39 | &.l4 { 40 | background-position: -300px -100px; 41 | } 42 | &.l1, 43 | &.l2, 44 | &.l3, 45 | &.l4 { 46 | transform: scale(-1, 1); 47 | -webkit-transform: scale(-1, 1); 48 | -ms-transform: scale(-1, 1); 49 | -moz-transform: scale(-1, 1); 50 | -o-transform: scale(-1, 1); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/logo/logo.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common'; 2 | import { Component, OnInit, inject, ChangeDetectorRef } from '@angular/core'; 3 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 4 | import { concat, Observable, timer } from 'rxjs'; 5 | import { delay, finalize, map, repeat, startWith, takeWhile, tap } from 'rxjs/operators'; 6 | 7 | @UntilDestroy() 8 | @Component({ 9 | selector: 't-logo', 10 | standalone: true, 11 | imports: [NgClass], 12 | templateUrl: './logo.component.html', 13 | styleUrls: ['./logo.component.scss'] 14 | }) 15 | export class LogoComponent implements OnInit { 16 | private cdr = inject(ChangeDetectorRef); 17 | 18 | className = ''; 19 | 20 | ngOnInit(): void { 21 | concat(this.run(), this.eyes()) 22 | .pipe(delay(5000), repeat(1000), untilDestroyed(this)) 23 | .subscribe(); 24 | } 25 | 26 | eyes() { 27 | return timer(0, 500).pipe( 28 | startWith(0), 29 | map((x) => x + 1), 30 | takeWhile((x) => x < 6), 31 | tap((x) => { 32 | const state = x % 2 === 0 ? 1 : 2; 33 | this.className = `l${state}`; 34 | this.cdr.markForCheck(); 35 | }) 36 | ); 37 | } 38 | 39 | run(): Observable { 40 | let side = 'r'; 41 | return timer(0, 100).pipe( 42 | startWith(0), 43 | map((x) => x + 1), 44 | takeWhile((x) => x <= 40), 45 | tap((x) => { 46 | if (x === 10 || x === 20 || x === 30) { 47 | side = side === 'r' ? 'l' : 'r'; 48 | } 49 | const state = x % 2 === 0 ? 3 : 4; 50 | this.className = `${side}${state}`; 51 | this.cdr.markForCheck(); 52 | }), 53 | finalize(() => { 54 | this.className = `${side}1`; 55 | this.cdr.markForCheck(); 56 | }) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/components/matrix/matrix.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/app/components/matrix/matrix.component.scss: -------------------------------------------------------------------------------- 1 | .matrix { 2 | border: 2px solid #000; 3 | padding: 3px 1px 1px 3px; 4 | width: 228px; 5 | overflow: auto; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/matrix/matrix.component.ts: -------------------------------------------------------------------------------- 1 | import { GameState } from '@angular-tetris/interface/game-state'; 2 | import { Tile } from '@angular-tetris/interface/tile/tile'; 3 | import { MatrixUtil } from '@angular-tetris/interface/utils/matrix'; 4 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 5 | import { AsyncPipe, NgFor } from '@angular/common'; 6 | import { Component, OnInit, inject } from '@angular/core'; 7 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 8 | import { Observable, combineLatest, of, timer } from 'rxjs'; 9 | import { map, switchMap, takeWhile } from 'rxjs/operators'; 10 | import { TileComponent } from '../tile/tile.component'; 11 | 12 | @UntilDestroy() 13 | @Component({ 14 | selector: 't-matrix', 15 | standalone: true, 16 | imports: [TileComponent, NgFor, AsyncPipe], 17 | templateUrl: './matrix.component.html', 18 | styleUrls: ['./matrix.component.scss'] 19 | }) 20 | export class MatrixComponent implements OnInit { 21 | private tetrisState = inject(TetrisStateService); 22 | 23 | matrix$: Observable; 24 | 25 | ngOnInit(): void { 26 | this.matrix$ = this.getMatrix(); 27 | } 28 | 29 | getMatrix(): Observable { 30 | return combineLatest([this.tetrisState.gameState$, this.tetrisState.matrix$]).pipe( 31 | untilDestroyed(this), 32 | switchMap(([gameState, matrix]) => { 33 | if (gameState !== GameState.Over && gameState !== GameState.Loading) { 34 | return of(matrix); 35 | } 36 | const newMatrix = [...matrix]; 37 | const rowsLength = MatrixUtil.Height * 2; 38 | const animatedMatrix$: Observable = timer(0, rowsLength).pipe( 39 | map((x) => x + 1), 40 | takeWhile((x) => x <= rowsLength + 1), 41 | switchMap((idx) => { 42 | const gridIndex = idx - 1; 43 | if (gridIndex < MatrixUtil.Height) { 44 | newMatrix.splice( 45 | gridIndex * MatrixUtil.Width, 46 | MatrixUtil.Width, 47 | ...MatrixUtil.FullRow 48 | ); 49 | } 50 | if (gridIndex > MatrixUtil.Height && gridIndex <= rowsLength) { 51 | const startIdx = 52 | (MatrixUtil.Height - (gridIndex - MatrixUtil.Height)) * MatrixUtil.Width; 53 | newMatrix.splice(startIdx, MatrixUtil.Width, ...MatrixUtil.EmptyRow); 54 | } 55 | 56 | return of(newMatrix); 57 | }) 58 | ); 59 | return animatedMatrix$; 60 | }) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/components/next/next.component.html: -------------------------------------------------------------------------------- 1 |

Next

2 | -------------------------------------------------------------------------------- /src/app/components/next/next.component.scss: -------------------------------------------------------------------------------- 1 | .next { 2 | .row { 3 | height: 22px; 4 | width: 88px; 5 | float: right; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/components/next/next.component.ts: -------------------------------------------------------------------------------- 1 | import { Tile, TileValue } from '@angular-tetris/interface/tile/tile'; 2 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 3 | import { NgFor } from '@angular/common'; 4 | import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; 5 | import { TileComponent } from '../tile/tile.component'; 6 | 7 | @Component({ 8 | selector: 't-next', 9 | standalone: true, 10 | imports: [TileComponent, NgFor], 11 | templateUrl: './next.component.html', 12 | styleUrls: ['./next.component.scss'], 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class NextComponent { 16 | private tetrisState = inject(TetrisStateService); 17 | 18 | next = computed(() => 19 | this.tetrisState.next().next.map((row) => row.map((value) => new Tile(value as TileValue))) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/number/number.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 |
-------------------------------------------------------------------------------- /src/app/components/number/number.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/app/components/number/number.component.scss -------------------------------------------------------------------------------- /src/app/components/number/number.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass, NgFor } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 't-number', 6 | standalone: true, 7 | imports: [NgFor, NgClass], 8 | templateUrl: './number.component.html', 9 | styleUrls: ['./number.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class NumberComponent { 13 | @Input() num = 0; 14 | @Input() length = 6; 15 | 16 | get nums(): string[] { 17 | const str = `${this.num}`; 18 | return str.padStart(this.length, 'n').split(''); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/pause/pause.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | -------------------------------------------------------------------------------- /src/app/components/pause/pause.component.scss: -------------------------------------------------------------------------------- 1 | .pause { 2 | width: 20px; 3 | height: 18px; 4 | background-position: -100px -75px; 5 | position: absolute; 6 | top: 3px; 7 | left: 18px; 8 | 9 | &.filled { 10 | background-position: -75px -75px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/pause/pause.component.ts: -------------------------------------------------------------------------------- 1 | import { GameState } from '@angular-tetris/interface/game-state'; 2 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 3 | import { AsyncPipe } from '@angular/common'; 4 | import { Component, inject } from '@angular/core'; 5 | import { Observable, interval, of } from 'rxjs'; 6 | import { map, switchMap } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 't-pause', 10 | standalone: true, 11 | imports: [AsyncPipe], 12 | templateUrl: './pause.component.html', 13 | styleUrls: ['./pause.component.scss'] 14 | }) 15 | export class PauseComponent { 16 | private tetrisState = inject(TetrisStateService); 17 | 18 | paused$: Observable = this.tetrisState.gameState$.pipe( 19 | switchMap((state) => { 20 | if (state === GameState.Paused) { 21 | return interval(250).pipe(map((num) => !!(num % 2))); 22 | } 23 | return of(false); 24 | }) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/point/point.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ labelAndPoint.label }}

3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/components/point/point.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/app/components/point/point.component.scss -------------------------------------------------------------------------------- /src/app/components/point/point.component.ts: -------------------------------------------------------------------------------- 1 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 2 | import { AsyncPipe, NgIf } from '@angular/common'; 3 | import { Component, inject } from '@angular/core'; 4 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 5 | import { Observable, of, timer } from 'rxjs'; 6 | import { map, switchMap } from 'rxjs/operators'; 7 | import { NumberComponent } from '../number/number.component'; 8 | 9 | const REFRESH_LABEL_INTERVAL = 3000; 10 | @UntilDestroy() 11 | @Component({ 12 | selector: 't-point', 13 | standalone: true, 14 | imports: [NumberComponent, NgIf, AsyncPipe], 15 | templateUrl: './point.component.html', 16 | styleUrls: ['./point.component.scss'] 17 | }) 18 | export class PointComponent { 19 | private tetrisState = inject(TetrisStateService); 20 | 21 | labelAndPoints$: Observable = this.tetrisState.current$.pipe( 22 | untilDestroyed(this), 23 | map((current) => !!current), 24 | switchMap((hasCurrent) => { 25 | if (hasCurrent) { 26 | return of(new LabelAndNumber('Score', this.tetrisState.points())); 27 | } 28 | return timer(0, REFRESH_LABEL_INTERVAL).pipe( 29 | map((val) => { 30 | const isOdd = val % 2 === 0; 31 | const points = this.tetrisState.points(); 32 | const max = this.tetrisState.max(); 33 | return isOdd ? new LabelAndNumber('Score', points) : new LabelAndNumber('Max ', max); 34 | }) 35 | ); 36 | }) 37 | ); 38 | } 39 | 40 | class LabelAndNumber { 41 | constructor(public label: string, public points: number) {} 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/screen-decoration/screen-decoration.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{title}}

14 |
15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 |

23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 |

32 | 33 | 34 | 35 | 36 |

37 | 38 |
39 | 40 | 41 |
42 | 43 |

44 | 45 | 46 |
47 | 48 |
49 | 50 |

51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 | 68 | 69 |
70 | 71 |

72 | 73 |
74 | 75 | 76 |
77 | 78 |

79 | 80 | 81 | 82 | 83 |

84 | 85 | 86 |
87 | 88 | 89 |
90 | 91 | 92 |

93 | 94 | 95 |
96 | 97 | 98 |
99 | 100 | 101 |

102 | 103 |
104 | 105 |
106 | 107 |
108 | 109 |
-------------------------------------------------------------------------------- /src/app/components/screen-decoration/screen-decoration.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | 3 | h1 { 4 | position: absolute; 5 | width: 100%; 6 | text-align: center; 7 | font-weight: normal; 8 | top: -12px; 9 | left: 0; 10 | margin: 0; 11 | padding: 0; 12 | font-size: 30px; 13 | } 14 | 15 | .topBorder { 16 | position: absolute; 17 | height: 10px; 18 | width: 100%; 19 | position: absolute; 20 | top: 0px; 21 | left: 0px; 22 | overflow: hidden; 23 | 24 | span { 25 | display: block; 26 | width: 10px; 27 | height: 10px; 28 | overflow: hidden; 29 | background: #000; 30 | } 31 | } 32 | 33 | .view { 34 | position: absolute; 35 | right: -70px; 36 | top: 20px; 37 | width: 44px; 38 | 39 | em { 40 | display: block; 41 | width: 22px; 42 | height: 22px; 43 | overflow: hidden; 44 | float: left; 45 | } 46 | 47 | p { 48 | height: 22px; 49 | clear: both; 50 | } 51 | 52 | &.l { 53 | right: auto; 54 | left: -70px; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/components/screen-decoration/screen-decoration.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FilledTile } from '@angular-tetris/interface/tile/filled-tile'; 3 | import { TileComponent } from '../tile/tile.component'; 4 | 5 | @Component({ 6 | selector: 't-screen-decoration', 7 | standalone: true, 8 | imports: [TileComponent], 9 | templateUrl: './screen-decoration.component.html', 10 | styleUrls: ['./screen-decoration.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class ScreenDecorationComponent { 14 | title = 'Angular Tetris'; 15 | filled = new FilledTile(); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/shared-button/shared-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/shared-button/shared-button.component.scss: -------------------------------------------------------------------------------- 1 | .twitter-button { 2 | background-color: #eee; 3 | background-image: -webkit-gradient( 4 | linear, 5 | left top, 6 | left bottom, 7 | color-stop(0, #fcfcfc), 8 | to(#eee) 9 | ); 10 | background-image: linear-gradient(to bottom, #fcfcfc 0, #eee 100%); 11 | background-repeat: no-repeat; 12 | border: 1px solid #d5d5d5; 13 | padding: 3px 10px 3px 8px; 14 | font-size: 16px; 15 | line-height: 22px; 16 | border-radius: 4px; 17 | font-weight: 700; 18 | color: #333; 19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 20 | 'Noto Sans', sans-serif; 21 | text-decoration: none; 22 | 23 | &:hover { 24 | text-decoration: none; 25 | background-color: #ddd; 26 | background-image: -webkit-gradient( 27 | linear, 28 | left top, 29 | left bottom, 30 | color-stop(0, #eee), 31 | to(#ddd) 32 | ); 33 | background-image: linear-gradient(to bottom, #eee 0, #ddd 100%); 34 | border-color: #ccc; 35 | } 36 | .icon { 37 | width: 20px; 38 | height: 20px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/shared-button/shared-button.component.ts: -------------------------------------------------------------------------------- 1 | import { NgIf } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | HostBinding, 6 | Input, 7 | ViewEncapsulation 8 | } from '@angular/core'; 9 | 10 | @Component({ 11 | selector: '[t-shared-button]', //eslint-disable-line 12 | standalone: true, 13 | imports: [NgIf], 14 | templateUrl: './shared-button.component.html', 15 | styleUrls: ['./shared-button.component.scss'], 16 | encapsulation: ViewEncapsulation.None, 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class SharedButtonComponent { 20 | @HostBinding('class') className = 'twitter-button'; 21 | @Input() showIcon = true; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/sound/sound.component.html: -------------------------------------------------------------------------------- 1 |
3 |
-------------------------------------------------------------------------------- /src/app/components/sound/sound.component.scss: -------------------------------------------------------------------------------- 1 | .music { 2 | width: 25px; 3 | height: 21px; 4 | background-position: -175px -75px; 5 | position: absolute; 6 | top: 2px; 7 | left: -12px; 8 | 9 | &.filled { 10 | background-position: -150px -75px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/sound/sound.component.ts: -------------------------------------------------------------------------------- 1 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 2 | import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 't-sound', 6 | standalone: true, 7 | templateUrl: './sound.component.html', 8 | styleUrls: ['./sound.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class SoundComponent { 12 | private tetrisState = inject(TetrisStateService); 13 | 14 | muted = computed(() => !this.tetrisState.isEnableSound()); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/components/start-line/start-line.component.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | Lines 4 | 5 |

6 | 8 | 9 | 10 | Start Line 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/components/start-line/start-line.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/app/components/start-line/start-line.component.scss -------------------------------------------------------------------------------- /src/app/components/start-line/start-line.component.ts: -------------------------------------------------------------------------------- 1 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 2 | import { NgIf } from '@angular/common'; 3 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 4 | import { NumberComponent } from '../number/number.component'; 5 | 6 | @Component({ 7 | selector: 't-start-line', 8 | standalone: true, 9 | imports: [NumberComponent, NgIf], 10 | templateUrl: './start-line.component.html', 11 | styleUrls: ['./start-line.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class StartLineComponent { 15 | private tetrisState = inject(TetrisStateService); 16 | 17 | hasCurrent = this.tetrisState.hasCurrent; 18 | clearedLines = this.tetrisState.clearedLines; 19 | initLine = this.tetrisState.initLine; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/tile/tile.component.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | $brown: #560000; 3 | 4 | //block 5 | :host { 6 | display: block; 7 | width: 20px; 8 | height: 20px; 9 | padding: 2px; 10 | border: 2px solid #879372; 11 | margin: 0 2px 2px 0; 12 | float: left; 13 | 14 | &:after { 15 | content: ""; 16 | display: block; 17 | width: 12px; 18 | height: 12px; 19 | background: #879372; 20 | overflow: hidden; 21 | } 22 | 23 | &.filled { 24 | border-color: $black; 25 | &:after { 26 | background: $black; 27 | } 28 | } 29 | 30 | &.animated { 31 | border-color: $brown; 32 | &:after { 33 | background: $brown; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/components/tile/tile.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input, 5 | Renderer2, 6 | ElementRef, 7 | ChangeDetectionStrategy 8 | } from '@angular/core'; 9 | import { Tile } from '@angular-tetris/interface/tile/tile'; 10 | 11 | @Component({ 12 | selector: 't-tile', 13 | standalone: true, 14 | template: ``, 15 | styleUrls: ['./tile.component.scss'], 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class TileComponent implements OnInit { 19 | @Input() tile: Tile; 20 | 21 | constructor(public el: ElementRef, private renderer: Renderer2) {} 22 | 23 | ngOnInit(): void { 24 | if (!this.tile) { 25 | return; 26 | } 27 | 28 | if (this.tile.isFilled) { 29 | this.renderer.addClass(this.el.nativeElement, 'filled'); 30 | } 31 | 32 | if (this.tile.isAnimated) { 33 | this.renderer.addClass(this.el.nativeElement, 'animated'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/containers/angular-tetris/angular-tetris.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | 26 | 27 | -------------------------------------------------------------------------------- /src/app/containers/angular-tetris/angular-tetris.component.scss: -------------------------------------------------------------------------------- 1 | $yellow: #efcc19; 2 | $black: #000; 3 | 4 | :host { 5 | width: 640px; 6 | padding-top: 40px; 7 | box-shadow: 0 0 10px #fff inset; 8 | border-radius: 20px; 9 | position: absolute; 10 | top: 50%; 11 | left: 50%; 12 | margin: -480px 0 0 -320px; 13 | background: $yellow; 14 | } 15 | 16 | .react { 17 | width: 480px; 18 | padding: 45px 0 35px; 19 | border: #000 solid; 20 | border-width: 0 10px 10px; 21 | margin: 0 auto; 22 | position: relative; 23 | &.drop { 24 | -webkit-transform: translateY(5px); 25 | transform: translateY(5px); 26 | } 27 | } 28 | 29 | .screen { 30 | width: 390px; 31 | height: 478px; 32 | border: solid 5px; 33 | border-color: #987f0f #fae36c #fae36c #987f0f; 34 | margin: 0 auto; 35 | position: relative; 36 | 37 | .panel { 38 | width: 380px; 39 | height: 468px; 40 | margin: 0 auto; 41 | background: #9ead86; 42 | padding: 8px; 43 | border: 2px solid #494536; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/containers/angular-tetris/angular-tetris.component.ts: -------------------------------------------------------------------------------- 1 | import { ClockComponent } from '@angular-tetris/components/clock/clock.component'; 2 | import { GithubComponent } from '@angular-tetris/components/github/github.component'; 3 | import { HoldComponent } from '@angular-tetris/components/hold/hold.component'; 4 | import { KeyboardComponent } from '@angular-tetris/components/keyboard/keyboard.component'; 5 | import { LevelComponent } from '@angular-tetris/components/level/level.component'; 6 | import { LogoComponent } from '@angular-tetris/components/logo/logo.component'; 7 | import { MatrixComponent } from '@angular-tetris/components/matrix/matrix.component'; 8 | import { NextComponent } from '@angular-tetris/components/next/next.component'; 9 | import { PauseComponent } from '@angular-tetris/components/pause/pause.component'; 10 | import { PointComponent } from '@angular-tetris/components/point/point.component'; 11 | import { ScreenDecorationComponent } from '@angular-tetris/components/screen-decoration/screen-decoration.component'; 12 | import { SoundComponent } from '@angular-tetris/components/sound/sound.component'; 13 | import { StartLineComponent } from '@angular-tetris/components/start-line/start-line.component'; 14 | import { TetrisKeyboard } from '@angular-tetris/interface/keyboard'; 15 | import { SoundManagerService } from '@angular-tetris/services/sound-manager.service'; 16 | import { KeyboardService } from '@angular-tetris/state/keyboard/keyboard.service'; 17 | import { TetrisService } from '@angular-tetris/state/tetris/tetris.service'; 18 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 19 | import { AsyncPipe, NgIf } from '@angular/common'; 20 | import { 21 | ChangeDetectionStrategy, 22 | Component, 23 | ElementRef, 24 | HostListener, 25 | OnInit, 26 | Renderer2, 27 | inject 28 | } from '@angular/core'; 29 | 30 | const KeyUp = 'document:keyup'; 31 | const KeyDown = 'document:keydown'; 32 | @Component({ 33 | selector: 'angular-tetris', // eslint-disable-line @angular-eslint/component-selector 34 | standalone: true, 35 | imports: [ 36 | NgIf, 37 | AsyncPipe, 38 | ClockComponent, 39 | GithubComponent, 40 | HoldComponent, 41 | KeyboardComponent, 42 | LevelComponent, 43 | LogoComponent, 44 | MatrixComponent, 45 | NextComponent, 46 | PauseComponent, 47 | PointComponent, 48 | ScreenDecorationComponent, 49 | SoundComponent, 50 | StartLineComponent 51 | ], 52 | templateUrl: './angular-tetris.component.html', 53 | styleUrls: ['./angular-tetris.component.scss'], 54 | changeDetection: ChangeDetectionStrategy.OnPush 55 | }) 56 | export class AngularTetrisComponent implements OnInit { 57 | private tetrisState = inject(TetrisStateService); 58 | private tetrisService = inject(TetrisService); 59 | private keyboardService = inject(KeyboardService); 60 | private soundManager = inject(SoundManagerService); 61 | private el = inject(ElementRef); 62 | private render = inject(Renderer2); 63 | 64 | drop = this.keyboardService.drop; 65 | isShowLogo$ = this.tetrisState.isShowLogo$; 66 | filling: number; 67 | 68 | @HostListener('window:resize', ['$event']) 69 | resize() { 70 | const width = document.documentElement.clientWidth; 71 | const height = document.documentElement.clientHeight; 72 | const ratio = height / width; 73 | let scale = 1; 74 | if (ratio < 1.5) { 75 | scale = height / 960; 76 | } else { 77 | scale = width / 640; 78 | this.filling = (height - 960 * scale) / scale / 3; 79 | const paddingTop = Math.floor(this.filling) + 42; 80 | const paddingBottom = Math.floor(this.filling); 81 | const marginTop = Math.floor(-480 - this.filling * 1.5); 82 | this.setPaddingMargin(paddingTop, paddingBottom, marginTop); 83 | } 84 | this.render.setStyle(this.el.nativeElement, 'transform', `scale(${scale - 0.01})`); 85 | } 86 | 87 | @HostListener('window:beforeunload', ['$event']) 88 | unloadHandler(event: Event) { 89 | if (this.hasCurrent) { 90 | event.preventDefault(); 91 | event.returnValue = true; 92 | } 93 | } 94 | 95 | @HostListener(`${KeyDown}.${TetrisKeyboard.Left}`) 96 | keyDownLeft() { 97 | this.soundManager.move(); 98 | this.keyboardService.setKeỵ({ 99 | left: true 100 | }); 101 | if (this.hasCurrent) { 102 | this.tetrisService.moveLeft(); 103 | } else { 104 | this.tetrisService.decreaseLevel(); 105 | } 106 | } 107 | 108 | @HostListener(`${KeyUp}.${TetrisKeyboard.Left}`) 109 | keyUpLeft() { 110 | this.keyboardService.setKeỵ({ 111 | left: false 112 | }); 113 | } 114 | 115 | @HostListener(`${KeyDown}.${TetrisKeyboard.Right}`) 116 | keyDownRight() { 117 | this.soundManager.move(); 118 | this.keyboardService.setKeỵ({ 119 | right: true 120 | }); 121 | if (this.hasCurrent) { 122 | this.tetrisService.moveRight(); 123 | } else { 124 | this.tetrisService.increaseLevel(); 125 | } 126 | } 127 | 128 | @HostListener(`${KeyUp}.${TetrisKeyboard.Right}`) 129 | keyUpRight() { 130 | this.keyboardService.setKeỵ({ 131 | right: false 132 | }); 133 | } 134 | 135 | @HostListener(`${KeyDown}.${TetrisKeyboard.Up}`) 136 | keyDownUp() { 137 | this.soundManager.rotate(); 138 | this.keyboardService.setKeỵ({ 139 | up: true 140 | }); 141 | if (this.hasCurrent) { 142 | this.tetrisService.rotate(); 143 | } else { 144 | this.tetrisService.increaseStartLine(); 145 | } 146 | } 147 | 148 | @HostListener(`${KeyUp}.${TetrisKeyboard.Up}`) 149 | keyUpUp() { 150 | this.keyboardService.setKeỵ({ 151 | up: false 152 | }); 153 | } 154 | 155 | @HostListener(`${KeyDown}.${TetrisKeyboard.Down}`) 156 | keyDownDown() { 157 | this.soundManager.move(); 158 | this.keyboardService.setKeỵ({ 159 | down: true 160 | }); 161 | if (this.hasCurrent) { 162 | this.tetrisService.moveDown(); 163 | } else { 164 | this.tetrisService.decreaseStartLine(); 165 | } 166 | } 167 | 168 | @HostListener(`${KeyUp}.${TetrisKeyboard.Down}`) 169 | keyUpDown() { 170 | this.keyboardService.setKeỵ({ 171 | down: false 172 | }); 173 | } 174 | 175 | @HostListener(`${KeyDown}.${TetrisKeyboard.Space}`) 176 | keyDownSpace() { 177 | this.keyboardService.setKeỵ({ 178 | drop: true 179 | }); 180 | if (this.hasCurrent) { 181 | this.soundManager.fall(); 182 | this.tetrisService.drop(); 183 | return; 184 | } 185 | this.soundManager.start(); 186 | this.tetrisService.start(); 187 | } 188 | 189 | @HostListener(`${KeyUp}.${TetrisKeyboard.Space}`) 190 | keyUpSpace() { 191 | this.keyboardService.setKeỵ({ 192 | drop: false 193 | }); 194 | } 195 | 196 | @HostListener(`${KeyDown}.${TetrisKeyboard.C}`) 197 | keyDownHold() { 198 | this.soundManager.move(); 199 | this.keyboardService.setKeỵ({ 200 | hold: true 201 | }); 202 | this.tetrisService.holdPiece(); 203 | } 204 | 205 | @HostListener(`${KeyUp}.${TetrisKeyboard.C}`) 206 | keyUpHold() { 207 | this.keyboardService.setKeỵ({ 208 | hold: false 209 | }); 210 | } 211 | 212 | @HostListener(`${KeyDown}.${TetrisKeyboard.S}`) 213 | keyDownSound() { 214 | this.soundManager.move(); 215 | this.tetrisService.toggleSound(); 216 | this.keyboardService.setKeỵ({ 217 | sound: true 218 | }); 219 | } 220 | 221 | @HostListener(`${KeyUp}.${TetrisKeyboard.S}`) 222 | keyUpSound() { 223 | this.keyboardService.setKeỵ({ 224 | sound: false 225 | }); 226 | } 227 | 228 | @HostListener(`${KeyDown}.${TetrisKeyboard.P}`) 229 | keyDownPause() { 230 | this.soundManager.move(); 231 | this.keyboardService.setKeỵ({ 232 | pause: true 233 | }); 234 | if (this.tetrisState.canStartGame()) { 235 | this.tetrisService.resume(); 236 | } else { 237 | this.tetrisService.pause(); 238 | } 239 | } 240 | 241 | @HostListener(`${KeyUp}.${TetrisKeyboard.P}`) 242 | keyUpPause() { 243 | this.keyboardService.setKeỵ({ 244 | pause: false 245 | }); 246 | } 247 | 248 | @HostListener(`${KeyDown}.${TetrisKeyboard.R}`) 249 | keyDownReset() { 250 | this.soundManager.move(); 251 | this.keyboardService.setKeỵ({ 252 | reset: true 253 | }); 254 | this.tetrisService.pause(); 255 | setTimeout(() => { 256 | if (confirm('You are having a good game. Are you sure you want to reset?')) { 257 | this.tetrisService.reset(); 258 | } else { 259 | this.tetrisService.resume(); 260 | } 261 | this.keyUpReset(); 262 | }); 263 | } 264 | 265 | @HostListener(`${KeyUp}.${TetrisKeyboard.R}`) 266 | keyUpReset() { 267 | this.keyboardService.setKeỵ({ 268 | reset: false 269 | }); 270 | } 271 | 272 | get hasCurrent() { 273 | return this.tetrisState.hasCurrent(); 274 | } 275 | 276 | ngOnInit(): void { 277 | setTimeout(() => { 278 | this.resize(); 279 | }); 280 | } 281 | 282 | keyboardMouseDown(key: string) { 283 | this[`keyDown${key}`](); 284 | } 285 | 286 | keyboardMouseUp(key: string) { 287 | this[`keyUp${key}`](); 288 | } 289 | 290 | private setPaddingMargin(paddingTop: number, paddingBottom: number, marginTop: number) { 291 | this.render.setStyle(this.el.nativeElement, 'padding-top', `${paddingTop}px`); 292 | this.render.setStyle(this.el.nativeElement, 'padding-bottom', `${paddingBottom}px`); 293 | this.render.setStyle(this.el.nativeElement, 'margin-top', `${marginTop}px`); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/app/factory/piece-factory.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from '../interface/piece/piece'; 2 | import { NonePiece } from '../interface/piece/none'; 3 | import { PieceI } from '../interface/piece/I'; 4 | import { PieceJ } from '../interface/piece/J'; 5 | import { PieceL } from '../interface/piece/L'; 6 | import { PieceO } from '../interface/piece/O'; 7 | import { PieceS } from '../interface/piece/S'; 8 | import { PieceT } from '../interface/piece/T'; 9 | import { PieceZ } from '../interface/piece/Z'; 10 | import { Injectable } from '@angular/core'; 11 | 12 | export const SPAWN_POSITION_X = 4; 13 | export const SPAWN_POSITION_Y = -4; 14 | 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class PieceFactory { 19 | private available: (typeof Piece)[] = []; 20 | private currentBag: (typeof Piece)[] = []; 21 | 22 | constructor() { 23 | this.available.push(PieceI); 24 | this.available.push(PieceJ); 25 | this.available.push(PieceL); 26 | this.available.push(PieceO); 27 | this.available.push(PieceS); 28 | this.available.push(PieceT); 29 | this.available.push(PieceZ); 30 | } 31 | 32 | getRandomPiece(x = SPAWN_POSITION_X, y = SPAWN_POSITION_Y): Piece { 33 | if (this.currentBag.length === 0) { 34 | this.generateNewBag(); 35 | } 36 | const nextPiece = this.currentBag.pop(); 37 | return new nextPiece(x, y); 38 | } 39 | 40 | getNonePiece(x = SPAWN_POSITION_X, y = SPAWN_POSITION_Y): Piece { 41 | return new NonePiece(x, y); 42 | } 43 | 44 | generateNewBag() { 45 | this.currentBag = this.available.slice(); 46 | this.shuffleArray(this.currentBag); 47 | } 48 | 49 | shuffleArray(array: (typeof Piece)[]) { 50 | for (let i = array.length - 1; i > 0; i--) { 51 | const j = Math.floor(Math.random() * (i + 1)); 52 | [array[i], array[j]] = [array[j], array[i]]; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/interface/callback.ts: -------------------------------------------------------------------------------- 1 | export type CallBack = (param: T1) => void; 2 | -------------------------------------------------------------------------------- /src/app/interface/game-state.ts: -------------------------------------------------------------------------------- 1 | export enum GameState { // eslint-disable-line no-shadow 2 | Loading, 3 | Paused, 4 | Started, 5 | Over 6 | } 7 | -------------------------------------------------------------------------------- /src/app/interface/keyboard.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | export enum TetrisKeyboard { 3 | Up = 'arrowup', 4 | Down = 'arrowdown', 5 | Left = 'arrowleft', 6 | Right = 'arrowright', 7 | Space = 'space', 8 | P = 'p', 9 | R = 'r', 10 | S = 's', 11 | C = 'c' 12 | } 13 | /* eslint-enable no-shadow */ 14 | -------------------------------------------------------------------------------- /src/app/interface/piece/Dot.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { Shapes } from './shape'; 3 | import { PieceRotation, PieceTypes } from './piece-enum'; 4 | 5 | const SHAPES_DOT: Shapes = []; 6 | SHAPES_DOT[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 0, 0, 0], 9 | [0, 0, 0, 0], 10 | [1, 0, 0, 0] 11 | ]; 12 | 13 | export class PieceDot extends Piece { 14 | constructor(x: number, y: number) { 15 | super(x, y); 16 | this.type = PieceTypes.Dot; 17 | this.next = [ 18 | [0, 0, 0, 0], 19 | [1, 0, 0, 0] 20 | ]; 21 | this.setShapes(SHAPES_DOT); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/interface/piece/I.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { PieceRotation, PieceTypes } from './piece-enum'; 3 | import { Shapes } from './shape'; 4 | 5 | const SHAPES_I: Shapes = []; 6 | SHAPES_I[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 0, 0, 0], 9 | [0, 0, 0, 0], 10 | [1, 1, 1, 1] 11 | ]; 12 | 13 | SHAPES_I[PieceRotation.Deg90] = [ 14 | [1, 0, 0, 0], 15 | [1, 0, 0, 0], 16 | [1, 0, 0, 0], 17 | [1, 0, 0, 0] 18 | ]; 19 | 20 | export class PieceI extends Piece { 21 | constructor(x: number, y: number) { 22 | super(x, y); 23 | this.type = PieceTypes.I; 24 | this.next = [ 25 | [0, 0, 0, 0], 26 | [1, 1, 1, 1] 27 | ]; 28 | this.setShapes(SHAPES_I); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/interface/piece/J.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { Shapes } from './shape'; 3 | import { PieceRotation, PieceTypes } from './piece-enum'; 4 | 5 | const SHAPES_J: Shapes = []; 6 | SHAPES_J[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 1, 0, 0], 9 | [0, 1, 0, 0], 10 | [1, 1, 0, 0] 11 | ]; 12 | 13 | SHAPES_J[PieceRotation.Deg90] = [ 14 | [0, 0, 0, 0], 15 | [0, 0, 0, 0], 16 | [1, 1, 1, 0], 17 | [0, 0, 1, 0] 18 | ]; 19 | SHAPES_J[PieceRotation.Deg180] = [ 20 | [0, 0, 0, 0], 21 | [1, 1, 0, 0], 22 | [1, 0, 0, 0], 23 | [1, 0, 0, 0] 24 | ]; 25 | SHAPES_J[PieceRotation.Deg270] = [ 26 | [0, 0, 0, 0], 27 | [0, 0, 0, 0], 28 | [1, 0, 0, 0], 29 | [1, 1, 1, 0] 30 | ]; 31 | 32 | export class PieceJ extends Piece { 33 | constructor(x: number, y: number) { 34 | super(x, y); 35 | this.type = PieceTypes.J; 36 | this.next = [ 37 | [1, 0, 0, 0], 38 | [1, 1, 1, 0] 39 | ]; 40 | this.setShapes(SHAPES_J); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/interface/piece/L.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { Shapes } from './shape'; 3 | import { PieceRotation, PieceTypes } from './piece-enum'; 4 | 5 | const SHAPES_L: Shapes = []; 6 | SHAPES_L[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [1, 0, 0, 0], 9 | [1, 0, 0, 0], 10 | [1, 1, 0, 0] 11 | ]; 12 | 13 | SHAPES_L[PieceRotation.Deg90] = [ 14 | [0, 0, 0, 0], 15 | [0, 0, 0, 0], 16 | [1, 1, 1, 0], 17 | [1, 0, 0, 0] 18 | ]; 19 | SHAPES_L[PieceRotation.Deg180] = [ 20 | [0, 0, 0, 0], 21 | [1, 1, 0, 0], 22 | [0, 1, 0, 0], 23 | [0, 1, 0, 0] 24 | ]; 25 | SHAPES_L[PieceRotation.Deg270] = [ 26 | [0, 0, 0, 0], 27 | [0, 0, 0, 0], 28 | [0, 0, 1, 0], 29 | [1, 1, 1, 0] 30 | ]; 31 | 32 | export class PieceL extends Piece { 33 | constructor(x: number, y: number) { 34 | super(x, y); 35 | this.type = PieceTypes.L; 36 | this.next = [ 37 | [0, 0, 1, 0], 38 | [1, 1, 1, 0] 39 | ]; 40 | this.setShapes(SHAPES_L); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/interface/piece/O.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { PieceRotation, PieceTypes } from './piece-enum'; 3 | import { Shapes } from './shape'; 4 | 5 | const SHAPES_O: Shapes = []; 6 | SHAPES_O[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 0, 0, 0], 9 | [1, 1, 0, 0], 10 | [1, 1, 0, 0] 11 | ]; 12 | 13 | export class PieceO extends Piece { 14 | constructor(x: number, y: number) { 15 | super(x, y); 16 | this.type = PieceTypes.O; 17 | this.next = [ 18 | [0, 1, 1, 0], 19 | [0, 1, 1, 0] 20 | ]; 21 | this.setShapes(SHAPES_O); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/interface/piece/S.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { Shapes } from './shape'; 3 | import { PieceRotation, PieceTypes } from './piece-enum'; 4 | 5 | const SHAPES_S: Shapes = []; 6 | SHAPES_S[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [1, 0, 0, 0], 9 | [1, 1, 0, 0], 10 | [0, 1, 0, 0] 11 | ]; 12 | 13 | SHAPES_S[PieceRotation.Deg90] = [ 14 | [0, 0, 0, 0], 15 | [0, 0, 0, 0], 16 | [0, 1, 1, 0], 17 | [1, 1, 0, 0] 18 | ]; 19 | 20 | export class PieceS extends Piece { 21 | constructor(x: number, y: number) { 22 | super(x, y); 23 | this.type = PieceTypes.S; 24 | this.next = [ 25 | [0, 1, 1, 0], 26 | [1, 1, 0, 0] 27 | ]; 28 | this.setShapes(SHAPES_S); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/interface/piece/T.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { Shapes } from './shape'; 3 | import { PieceRotation, PieceTypes } from './piece-enum'; 4 | 5 | const SHAPES_T: Shapes = []; 6 | SHAPES_T[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 0, 0, 0], 9 | [0, 1, 0, 0], 10 | [1, 1, 1, 0] 11 | ]; 12 | 13 | SHAPES_T[PieceRotation.Deg90] = [ 14 | [0, 0, 0, 0], 15 | [1, 0, 0, 0], 16 | [1, 1, 0, 0], 17 | [1, 0, 0, 0] 18 | ]; 19 | 20 | SHAPES_T[PieceRotation.Deg180] = [ 21 | [0, 0, 0, 0], 22 | [0, 0, 0, 0], 23 | [1, 1, 1, 0], 24 | [0, 1, 0, 0] 25 | ]; 26 | 27 | SHAPES_T[PieceRotation.Deg270] = [ 28 | [0, 0, 0, 0], 29 | [0, 1, 0, 0], 30 | [1, 1, 0, 0], 31 | [0, 1, 0, 0] 32 | ]; 33 | 34 | export class PieceT extends Piece { 35 | constructor(x: number, y: number) { 36 | super(x, y); 37 | this.type = PieceTypes.T; 38 | this.next = [ 39 | [0, 1, 0, 0], 40 | [1, 1, 1, 0] 41 | ]; 42 | this.setShapes(SHAPES_T); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/interface/piece/Z.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { Shapes } from './shape'; 3 | import { PieceRotation, PieceTypes } from './piece-enum'; 4 | 5 | const SHAPES_Z: Shapes = []; 6 | SHAPES_Z[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 1, 0, 0], 9 | [1, 1, 0, 0], 10 | [1, 0, 0, 0] 11 | ]; 12 | 13 | SHAPES_Z[PieceRotation.Deg90] = [ 14 | [0, 0, 0, 0], 15 | [0, 0, 0, 0], 16 | [1, 1, 0, 0], 17 | [0, 1, 1, 0] 18 | ]; 19 | 20 | export class PieceZ extends Piece { 21 | constructor(x: number, y: number) { 22 | super(x, y); 23 | this.type = PieceTypes.Z; 24 | this.next = [ 25 | [1, 1, 0, 0], 26 | [0, 1, 1, 0] 27 | ]; 28 | this.setShapes(SHAPES_Z); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/interface/piece/none.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from './piece'; 2 | import { PieceRotation, PieceTypes } from './piece-enum'; 3 | import { Shapes } from './shape'; 4 | 5 | const NONE_SHAPE: Shapes = []; 6 | NONE_SHAPE[PieceRotation.Deg0] = [ 7 | [0, 0, 0, 0], 8 | [0, 0, 0, 0] 9 | ]; 10 | 11 | export class NonePiece extends Piece { 12 | constructor(x: number, y: number) { 13 | super(x, y); 14 | this.type = PieceTypes.None; 15 | this.next = [ 16 | [0, 0, 0, 0], 17 | [0, 0, 0, 0] 18 | ]; 19 | this.setShapes(NONE_SHAPE); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/interface/piece/piece-enum.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | export enum PieceRotation { 3 | Deg0, 4 | Deg90, 5 | Deg180, 6 | Deg270 7 | } 8 | 9 | export enum PieceTypes { 10 | Dot = 'Dot', 11 | O = 'O', 12 | I = 'I', 13 | T = 'T', 14 | L = 'L', 15 | J = 'J', 16 | Z = 'Z', 17 | S = 'S', 18 | None = 'None' 19 | } 20 | /* eslint-enable no-shadow */ 21 | -------------------------------------------------------------------------------- /src/app/interface/piece/piece.ts: -------------------------------------------------------------------------------- 1 | import { PieceRotation, PieceTypes } from './piece-enum'; 2 | import { Shape, Shapes } from './shape'; 3 | import { MatrixUtil } from '../utils/matrix'; 4 | 5 | export class Piece { 6 | x: number; 7 | y: number; 8 | rotation = PieceRotation.Deg0; 9 | type: PieceTypes; 10 | shape: Shape; 11 | next: Shape; 12 | 13 | private shapes: Shapes; 14 | private lastConfig: Partial; 15 | 16 | constructor(x: number, y: number) { 17 | this.x = x; 18 | this.y = y; 19 | } 20 | 21 | store(): Piece { 22 | this.lastConfig = { 23 | x: this.x, 24 | y: this.y, 25 | rotation: this.rotation, 26 | shape: this.shape 27 | }; 28 | return this.newPiece(); 29 | } 30 | 31 | clearStore(): Piece { 32 | this.lastConfig = null; 33 | return this.newPiece(); 34 | } 35 | 36 | revert(): Piece { 37 | if (this.lastConfig) { 38 | for (const key in this.lastConfig) { 39 | if (this.lastConfig.hasOwnProperty(key)) { 40 | this[key] = this.lastConfig[key]; 41 | } 42 | } 43 | this.lastConfig = null; 44 | } 45 | return this.newPiece(); 46 | } 47 | 48 | rotate(): Piece { 49 | const keys = Object.keys(this.shapes); 50 | const idx = keys.indexOf(this.rotation.toString()); 51 | const isTurnOver = idx >= keys.length - 1; 52 | this.rotation = Number(isTurnOver ? keys[0] : keys[idx + 1]); 53 | this.shape = this.shapes[this.rotation]; 54 | return this.newPiece(); 55 | } 56 | 57 | reset(): Piece { 58 | this.rotation = PieceRotation.Deg0; 59 | this.shape = this.shapes[this.rotation]; 60 | return this.newPiece(); 61 | } 62 | 63 | moveDown(step = 1): Piece { 64 | this.y = this.y + step; 65 | return this.newPiece(); 66 | } 67 | 68 | moveRight(): Piece { 69 | this.x++; 70 | return this.newPiece(); 71 | } 72 | 73 | moveLeft(): Piece { 74 | this.x--; 75 | return this.newPiece(); 76 | } 77 | 78 | isNone(): boolean { 79 | return this.type === PieceTypes.None; 80 | } 81 | 82 | get positionOnGrid(): number[] { 83 | const positions = []; 84 | for (let row = 0; row < 4; row++) { 85 | for (let col = 0; col < 4; col++) { 86 | if (this.shape[row][col]) { 87 | const position = (this.y + row) * MatrixUtil.Width + this.x + col; 88 | if (position >= 0) { 89 | positions.push(position); 90 | } 91 | } 92 | } 93 | } 94 | return positions; 95 | } 96 | 97 | get bottomRow() { 98 | return this.y + 3; 99 | } 100 | 101 | get rightCol() { 102 | let col = 3; 103 | while (col >= 0) { 104 | for (let row = 0; row <= 3; row++) { 105 | if (this.shape[row][col]) { 106 | return this.x + col; 107 | } 108 | } 109 | col--; 110 | } 111 | } 112 | 113 | get leftCol() { 114 | return this.x; 115 | } 116 | 117 | protected setShapes(shapes: Shapes) { 118 | this.shapes = shapes; 119 | this.shape = shapes[this.rotation]; 120 | } 121 | 122 | private newPiece(): Piece { 123 | const piece = new Piece(this.x, this.y); 124 | piece.rotation = this.rotation; 125 | piece.type = this.type; 126 | piece.next = this.next; 127 | piece.setShapes(this.shapes); 128 | piece.lastConfig = this.lastConfig; 129 | return piece; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/app/interface/piece/shape.ts: -------------------------------------------------------------------------------- 1 | export type Shape = number[][]; 2 | 3 | export type Shapes = Shape[]; 4 | -------------------------------------------------------------------------------- /src/app/interface/speed.ts: -------------------------------------------------------------------------------- 1 | export type Speed = 1 | 2 | 3 | 4 | 5 | 6; 2 | -------------------------------------------------------------------------------- /src/app/interface/tile/animated-tile.ts: -------------------------------------------------------------------------------- 1 | import { Tile } from './tile'; 2 | 3 | export class AnimatedTile extends Tile { 4 | constructor(isSolid = false) { 5 | super(2); 6 | this.isSolid = isSolid; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/interface/tile/empty-tile.ts: -------------------------------------------------------------------------------- 1 | import { Tile } from './tile'; 2 | 3 | export class EmptyTile extends Tile { 4 | constructor() { 5 | super(0); 6 | this.isSolid = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/interface/tile/filled-tile.ts: -------------------------------------------------------------------------------- 1 | import { Tile } from './tile'; 2 | 3 | export class FilledTile extends Tile { 4 | constructor(isSolid = false) { 5 | super(1); 6 | this.isSolid = isSolid; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/interface/tile/tile.ts: -------------------------------------------------------------------------------- 1 | export type TileValue = 0 | 1 | 2; 2 | export class Tile { 3 | public isSolid: boolean; 4 | private value: TileValue; 5 | 6 | constructor(val: TileValue) { 7 | this.value = val; 8 | } 9 | 10 | get isFilled(): boolean { 11 | return this.value === 1; 12 | } 13 | 14 | get isAnimated(): boolean { 15 | return this.value === 2; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/interface/ui-model/arrow-button.ts: -------------------------------------------------------------------------------- 1 | export enum ArrowButton { // eslint-disable-line no-shadow 2 | UP = 'UP', 3 | DOWN = 'DOWN', 4 | LEFT = 'LEFT', 5 | RIGHT = 'RIGHT' 6 | } 7 | 8 | export const ArrowButtonTransform = { 9 | [ArrowButton.UP]: 'translate(0px, 63px) scale(1, 2)', 10 | [ArrowButton.DOWN]: 'translate(0,-71px) rotate(180deg) scale(1, 2)', 11 | [ArrowButton.LEFT]: 'translate(60px, -12px) rotate(270deg) scale(1, 2)', 12 | [ArrowButton.RIGHT]: 'translate(-60px, -12px) rotate(90deg) scale(1, 2)' 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/interface/utils/matrix.ts: -------------------------------------------------------------------------------- 1 | import { EmptyTile } from '../tile/empty-tile'; 2 | import { Tile } from '../tile/tile'; 3 | import { FilledTile } from '../tile/filled-tile'; 4 | 5 | /* eslint-disable @typescript-eslint/naming-convention */ 6 | export class MatrixUtil { 7 | static readonly Width = 10; 8 | static readonly Height = 20; 9 | static Points = [100, 300, 700, 1500]; 10 | static MaxPoint = 999999; 11 | static SpeedDelay = [700, 600, 450, 320, 240, 160]; 12 | 13 | static getStartBoard(startLines: number = 0): Tile[] { 14 | if (startLines === 0) { 15 | return new Array(this.Width * this.Height).fill(new EmptyTile()); 16 | } 17 | const startMatrix: Tile[] = []; 18 | 19 | for (let i = 0; i < startLines; i++) { 20 | if (i <= 2) { 21 | // 0-3 22 | startMatrix.push(...this.getRandomFilledRow(5, 8)); 23 | } else if (i <= 6) { 24 | // 4-6 25 | startMatrix.push(...this.getRandomFilledRow(4, 9)); 26 | } else { 27 | // 7-9 28 | startMatrix.push(...this.getRandomFilledRow(3, 9)); 29 | } 30 | } 31 | 32 | for (let i = 0, len = 20 - startLines; i < len; i++) { 33 | startMatrix.unshift(...this.EmptyRow); 34 | } 35 | return startMatrix; 36 | } 37 | 38 | static getRandomFilledRow(min: number, max: number): Tile[] { 39 | const count = parseInt(`${(max - min + 1) * Math.random() + min}`, 10); 40 | const line: Tile[] = new Array(count).fill(new FilledTile(true)); 41 | 42 | for (let i = 0, len = 10 - count; i < len; i++) { 43 | const index = parseInt(`${(line.length + 1) * Math.random()}`, 10); 44 | line.splice(index, 0, new EmptyTile()); 45 | } 46 | 47 | return line; 48 | } 49 | 50 | static get EmptyRow(): Tile[] { 51 | return new Array(this.Width).fill(new EmptyTile()); 52 | } 53 | 54 | static get FullRow(): Tile[] { 55 | return new Array(this.Width).fill(new FilledTile()); 56 | } 57 | 58 | static getSpeedDelay(speed: number) { 59 | return this.SpeedDelay[speed - 1] ?? this.SpeedDelay[0]; 60 | } 61 | } 62 | /* eslint-enable @typescript-eslint/naming-convention */ 63 | -------------------------------------------------------------------------------- /src/app/services/google-analytics.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | declare let gtag: any; 3 | const GOOGLE_ANALYTICS_ID = 'UA-80363801-4'; 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class GoogleAnalyticsService { 8 | constructor() {} 9 | 10 | public sendEvent( 11 | eventName: string, 12 | eventCategory: string, 13 | eventLabel: string = null, 14 | eventValue: number = null 15 | ) { 16 | if (!gtag) { 17 | return; 18 | } 19 | /* eslint-disable @typescript-eslint/naming-convention */ 20 | gtag('event', eventName, { 21 | event_category: eventCategory, 22 | event_label: eventLabel, 23 | value: eventValue 24 | }); 25 | /* eslint-enable @typescript-eslint/naming-convention */ 26 | } 27 | 28 | public sendPageView(url: string) { 29 | if (!gtag) { 30 | return; 31 | } 32 | gtag('config', GOOGLE_ANALYTICS_ID, { page_path: url }); // eslint-disable-line @typescript-eslint/naming-convention 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/services/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | const ANGULAR_TETRIS_STORAGE_KEY = 'ANGULAR_TETRIS'; 2 | export class LocalStorageService { 3 | constructor() {} 4 | 5 | static setMaxPoint(max: number) { 6 | localStorage.setItem(ANGULAR_TETRIS_STORAGE_KEY, `${max}`); 7 | } 8 | 9 | static get maxPoint(): number { 10 | const max = parseInt(localStorage.getItem(ANGULAR_TETRIS_STORAGE_KEY)); 11 | return Number.isInteger(max) ? max : 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/services/sound-manager.service.ts: -------------------------------------------------------------------------------- 1 | import { TetrisStateService } from '@angular-tetris/state/tetris/tetris.state'; 2 | import { Injectable, inject } from '@angular/core'; 3 | 4 | const SOUND_FILE_PATH = '/assets/tetris-sound.mp3'; 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class SoundManagerService { 9 | private context: AudioContext; 10 | private buffer: AudioBuffer; 11 | private tetrisState = inject(TetrisStateService); 12 | 13 | start() { 14 | this.playMusic(0, 3.7202, 3.6224); 15 | } 16 | 17 | clear() { 18 | this.playMusic(0, 0, 0.7675); 19 | } 20 | 21 | fall() { 22 | this.playMusic(0, 1.2558, 0.3546); 23 | } 24 | 25 | gameOver() { 26 | this.playMusic(0, 8.1276, 1.1437); 27 | } 28 | 29 | rotate() { 30 | this.playMusic(0, 2.2471, 0.0807); 31 | } 32 | 33 | move() { 34 | this.playMusic(0, 2.9088, 0.1437); 35 | } 36 | 37 | private playMusic(when: number, offset: number, duration: number) { 38 | if (!this.tetrisState.isEnableSound()) { 39 | return; 40 | } 41 | this.loadSound().then((source) => { 42 | if (source) { 43 | source.start(when, offset, duration); 44 | } 45 | }); 46 | } 47 | 48 | private loadSound(): Promise { 49 | return new Promise((resolve, reject) => { 50 | if (this.context && this.buffer) { 51 | resolve(this.getSource(this.context, this.buffer)); 52 | return; 53 | } 54 | const context = new AudioContext(); 55 | const req = new XMLHttpRequest(); 56 | req.open('GET', SOUND_FILE_PATH, true); 57 | req.responseType = 'arraybuffer'; 58 | 59 | req.onload = () => { 60 | context.decodeAudioData( 61 | req.response, 62 | (buffer) => { 63 | this.context = context; 64 | this.buffer = buffer; 65 | resolve(this.getSource(context, buffer)); 66 | }, 67 | () => { 68 | const msg = 'Sorry lah, cannot play sound. But I hope you still enjoy Angular Tetris!!'; 69 | alert(msg); 70 | reject(msg); 71 | } 72 | ); 73 | }; 74 | req.send(); 75 | }); 76 | } 77 | 78 | private getSource(context: AudioContext, buffer: AudioBuffer): AudioBufferSourceNode { 79 | const source = context.createBufferSource(); 80 | source.buffer = buffer; 81 | source.connect(context.destination); 82 | return source; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/state/keyboard/keyboard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, computed, signal } from '@angular/core'; 2 | 3 | export interface KeyboardState { 4 | up: boolean; 5 | down: boolean; 6 | left: boolean; 7 | right: boolean; 8 | pause: boolean; 9 | sound: boolean; 10 | reset: boolean; 11 | drop: boolean; 12 | hold: boolean; 13 | } 14 | 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class KeyboardService { 19 | private keyboardState = signal({ 20 | up: false, 21 | down: false, 22 | left: false, 23 | right: false, 24 | pause: false, 25 | sound: false, 26 | reset: false, 27 | drop: false, 28 | hold: false 29 | }); 30 | 31 | // computed does memorized value so it won't emit if 32 | // value doesn't change 33 | up = computed(() => this.keyboardState().up); 34 | down = computed(() => this.keyboardState().down); 35 | left = computed(() => this.keyboardState().left); 36 | right = computed(() => this.keyboardState().right); 37 | drop = computed(() => this.keyboardState().drop); 38 | pause = computed(() => this.keyboardState().pause); 39 | sound = computed(() => this.keyboardState().sound); 40 | reset = computed(() => this.keyboardState().reset); 41 | hold = computed(() => this.keyboardState().hold); 42 | 43 | setKeỵ(keyState: Partial) { 44 | this.keyboardState.update((currentKeyState) => ({ ...currentKeyState, ...keyState })); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/state/tetris/tetris.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PieceFactory, 3 | SPAWN_POSITION_X, 4 | SPAWN_POSITION_Y 5 | } from '@angular-tetris/factory/piece-factory'; 6 | import { CallBack } from '@angular-tetris/interface/callback'; 7 | import { GameState } from '@angular-tetris/interface/game-state'; 8 | import { Piece } from '@angular-tetris/interface/piece/piece'; 9 | import { Speed } from '@angular-tetris/interface/speed'; 10 | import { EmptyTile } from '@angular-tetris/interface/tile/empty-tile'; 11 | import { FilledTile } from '@angular-tetris/interface/tile/filled-tile'; 12 | import { Tile } from '@angular-tetris/interface/tile/tile'; 13 | import { MatrixUtil } from '@angular-tetris/interface/utils/matrix'; 14 | import { LocalStorageService } from '@angular-tetris/services/local-storage.service'; 15 | import { SoundManagerService } from '@angular-tetris/services/sound-manager.service'; 16 | import { Injectable, inject } from '@angular/core'; 17 | import { TetrisStateService } from './tetris.state'; 18 | 19 | @Injectable({ providedIn: 'root' }) 20 | export class TetrisService { 21 | gameInterval: number | null; 22 | 23 | private soundManager = inject(SoundManagerService); 24 | private pieceFactory = inject(PieceFactory); 25 | private tetrisState = inject(TetrisStateService); 26 | 27 | start() { 28 | if (!this.tetrisState.hasCurrent()) { 29 | this.setCurrentPiece(this.tetrisState.next()); 30 | this.setNext(); 31 | } 32 | 33 | this.tetrisState.updateState({ 34 | points: 0, 35 | gameState: GameState.Started, 36 | matrix: MatrixUtil.getStartBoard(this.tetrisState.initLine()), 37 | speed: this.tetrisState.initSpeed() 38 | }); 39 | this.stopGameInterval(); 40 | this.auto(MatrixUtil.getSpeedDelay(this.tetrisState.initSpeed())); 41 | this.setLocked(false); 42 | } 43 | 44 | auto(delay: number) { 45 | this.update(); 46 | 47 | this.gameInterval = setInterval(() => { 48 | this.update(); 49 | }, delay); 50 | } 51 | 52 | resume() { 53 | if (!this.tetrisState.isPause()) { 54 | return; 55 | } 56 | 57 | this.tetrisState.updateState({ 58 | locked: false, 59 | gameState: GameState.Started 60 | }); 61 | this.auto(MatrixUtil.getSpeedDelay(this.tetrisState.speed())); 62 | } 63 | 64 | pause() { 65 | if (!this.tetrisState.isPlaying()) { 66 | return; 67 | } 68 | this.tetrisState.updateState({ 69 | locked: true, 70 | gameState: GameState.Paused 71 | }); 72 | this.stopGameInterval(); 73 | } 74 | 75 | reset() { 76 | this.tetrisState.resetState({ 77 | sound: this.tetrisState.isEnableSound() 78 | }); 79 | } 80 | 81 | moveLeft() { 82 | if (this.tetrisState.locked()) { 83 | return; 84 | } 85 | this.clearPiece(); 86 | this.setCurrentPiece(this.tetrisState.current().store()); 87 | this.setCurrentPiece(this.tetrisState.current().moveLeft()); 88 | if (this.isCollidesLeft) { 89 | this.setCurrentPiece(this.tetrisState.current().revert()); 90 | } 91 | this.drawPiece(); 92 | } 93 | 94 | moveRight() { 95 | if (this.tetrisState.locked()) { 96 | return; 97 | } 98 | this.clearPiece(); 99 | this.setCurrentPiece(this.tetrisState.current().store()); 100 | this.setCurrentPiece(this.tetrisState.current().moveRight()); 101 | if (this.isCollidesRight) { 102 | this.setCurrentPiece(this.tetrisState.current().revert()); 103 | } 104 | this.drawPiece(); 105 | } 106 | 107 | rotate() { 108 | if (this.tetrisState.locked()) { 109 | return; 110 | } 111 | 112 | this.clearPiece(); 113 | this.setCurrentPiece(this.tetrisState.current().store()); 114 | this.setCurrentPiece(this.tetrisState.current().rotate()); 115 | while (this.isCollidesRight) { 116 | this.setCurrentPiece(this.tetrisState.current().moveLeft()); 117 | if (this.isCollidesLeft) { 118 | this.setCurrentPiece(this.tetrisState.current().revert()); 119 | break; 120 | } 121 | } 122 | this.drawPiece(); 123 | } 124 | 125 | moveDown() { 126 | this.update(); 127 | } 128 | 129 | drop() { 130 | if (this.tetrisState.locked()) { 131 | return; 132 | } 133 | while (!this.isCollidesBottom) { 134 | this.clearPiece(); 135 | this.setCurrentPiece(this.tetrisState.current().store()); 136 | this.setCurrentPiece(this.tetrisState.current().moveDown()); 137 | } 138 | this.setCurrentPiece(this.tetrisState.current().revert()); 139 | this.drawPiece(); 140 | this.setCanHold(true); 141 | } 142 | 143 | holdPiece(): void { 144 | if (this.tetrisState.locked() || !this.tetrisState.canHold()) { 145 | return; 146 | } 147 | this.clearPiece(); 148 | const isHoldNonePiece = this.tetrisState.hold().isNone(); 149 | const newCurrent = isHoldNonePiece ? this.tetrisState.next() : this.tetrisState.hold(); 150 | if (isHoldNonePiece) { 151 | this.setNext(); 152 | } 153 | this.setHolded(this.tetrisState.current().reset()); 154 | this.setCurrentPiece(newCurrent); 155 | this.resetPosition(this.tetrisState.hold()); 156 | this.setCanHold(false); 157 | } 158 | 159 | toggleSound() { 160 | this.tetrisState.updateState({ 161 | sound: !this.tetrisState.isEnableSound() 162 | }); 163 | } 164 | 165 | decreaseLevel() { 166 | const initSpeed = this.tetrisState.initSpeed(); 167 | const newSpeed = (initSpeed - 1 < 1 ? 6 : initSpeed - 1) as Speed; 168 | this.tetrisState.updateState({ 169 | initSpeed: newSpeed 170 | }); 171 | } 172 | 173 | increaseLevel() { 174 | const initSpeed = this.tetrisState.initSpeed(); 175 | const newSpeed = (initSpeed + 1 > 6 ? 1 : initSpeed + 1) as Speed; 176 | this.tetrisState.updateState({ 177 | initSpeed: newSpeed 178 | }); 179 | } 180 | 181 | increaseStartLine() { 182 | const initLine = this.tetrisState.initLine(); 183 | const startLine = initLine + 1 > 10 ? 1 : initLine + 1; 184 | this.tetrisState.updateState({ 185 | initLine: startLine 186 | }); 187 | } 188 | 189 | decreaseStartLine() { 190 | const initLine = this.tetrisState.initLine(); 191 | const startLine = initLine - 1 < 1 ? 10 : initLine - 1; 192 | this.tetrisState.updateState({ 193 | initLine: startLine 194 | }); 195 | } 196 | 197 | private update() { 198 | if (this.tetrisState.locked()) { 199 | return; 200 | } 201 | this.setLocked(true); 202 | this.setCurrentPiece(this.tetrisState.current().revert()); 203 | this.clearPiece(); 204 | this.setCurrentPiece(this.tetrisState.current().store()); 205 | this.setCurrentPiece(this.tetrisState.current().moveDown()); 206 | 207 | if (this.isCollidesBottom) { 208 | this.setCurrentPiece(this.tetrisState.current().revert()); 209 | this.markAsSolid(); 210 | this.drawPiece(); 211 | this.clearFullLines(); 212 | this.setCurrentPiece(this.tetrisState.next()); 213 | this.setNext(); 214 | this.setCanHold(true); 215 | if (this.isGameOver) { 216 | this.onGameOver(); 217 | return; 218 | } 219 | } 220 | 221 | this.drawPiece(); 222 | this.setLocked(false); 223 | } 224 | 225 | private clearFullLines() { 226 | let numberOfClearedLines = 0; 227 | const newMatrix = [...this.tetrisState.matrix()]; 228 | for (let row = MatrixUtil.Height - 1; row >= 0; row--) { 229 | const pos = row * MatrixUtil.Width; 230 | const fullRowTiles = newMatrix.slice(pos, pos + MatrixUtil.Width); 231 | const isFullRow = fullRowTiles.every((x) => x.isSolid); 232 | if (isFullRow) { 233 | numberOfClearedLines++; 234 | const topPortion = this.tetrisState.matrix().slice(0, row * MatrixUtil.Width); 235 | newMatrix.splice(0, ++row * MatrixUtil.Width, ...MatrixUtil.EmptyRow.concat(topPortion)); 236 | this.setMatrix(newMatrix); 237 | } 238 | } 239 | this.setPointsAndSpeed(numberOfClearedLines); 240 | } 241 | 242 | private get isGameOver() { 243 | this.setCurrentPiece(this.tetrisState.current().store()); 244 | this.setCurrentPiece(this.tetrisState.current().moveDown()); 245 | if (this.isCollidesBottom) { 246 | return true; 247 | } 248 | this.setCurrentPiece(this.tetrisState.current().revert()); 249 | return false; 250 | } 251 | 252 | private onGameOver() { 253 | this.pause(); 254 | this.soundManager.gameOver(); 255 | const maxPoint = Math.max(this.tetrisState.points(), this.tetrisState.max()); 256 | LocalStorageService.setMaxPoint(maxPoint); 257 | this.tetrisState.resetState({ 258 | max: maxPoint, 259 | gameState: GameState.Over, 260 | sound: this.tetrisState.isEnableSound() 261 | }); 262 | } 263 | 264 | private get isCollidesBottom(): boolean { 265 | if (this.tetrisState.current().bottomRow >= MatrixUtil.Height) { 266 | return true; 267 | } 268 | return this.collides(); 269 | } 270 | 271 | private get isCollidesLeft(): boolean { 272 | if (this.tetrisState.current().leftCol < 0) { 273 | return true; 274 | } 275 | return this.collides(); 276 | } 277 | 278 | private get isCollidesRight(): boolean { 279 | if (this.tetrisState.current().rightCol >= MatrixUtil.Width) { 280 | return true; 281 | } 282 | return this.collides(); 283 | } 284 | 285 | private collides(): boolean { 286 | return this.tetrisState.current().positionOnGrid.some((pos) => { 287 | if (this.tetrisState.matrix()[pos].isSolid) { 288 | return true; 289 | } 290 | return false; 291 | }); 292 | } 293 | 294 | private drawPiece() { 295 | this.setCurrentPiece(this.tetrisState.current().clearStore()); 296 | this.loopThroughPiecePosition((position) => { 297 | const isSolid = this.tetrisState.matrix()[position].isSolid; 298 | this.updateMatrix(position, new FilledTile(isSolid)); 299 | }); 300 | } 301 | 302 | private markAsSolid() { 303 | this.loopThroughPiecePosition((position) => { 304 | this.updateMatrix(position, new FilledTile(true)); 305 | }); 306 | } 307 | 308 | private clearPiece() { 309 | this.loopThroughPiecePosition((position) => { 310 | this.updateMatrix(position, new EmptyTile()); 311 | }); 312 | } 313 | 314 | private loopThroughPiecePosition(callback: CallBack) { 315 | this.tetrisState.current().positionOnGrid.forEach((position) => { 316 | callback(position); 317 | }); 318 | } 319 | 320 | private setPointsAndSpeed(numberOfClearedLines: number) { 321 | if (!numberOfClearedLines) { 322 | return; 323 | } 324 | this.soundManager.clear(); 325 | const newLines = this.tetrisState.clearedLines() + numberOfClearedLines; 326 | const newPoints = this.getPoints(numberOfClearedLines, this.tetrisState.points()); 327 | const newSpeed = this.getSpeed(newLines, this.tetrisState.initSpeed()); 328 | 329 | this.tetrisState.updateState({ 330 | points: newPoints, 331 | clearedLines: newLines, 332 | speed: newSpeed 333 | }); 334 | 335 | if (newSpeed !== this.tetrisState.speed()) { 336 | this.stopGameInterval(); 337 | this.auto(MatrixUtil.getSpeedDelay(newSpeed)); 338 | } 339 | } 340 | 341 | private getSpeed(totalLines: number, initSpeed: number): Speed { 342 | const addedSpeed = Math.floor(totalLines / MatrixUtil.Height); 343 | let newSpeed = (initSpeed + addedSpeed) as Speed; 344 | newSpeed = newSpeed > 6 ? 6 : newSpeed; 345 | return newSpeed; 346 | } 347 | 348 | private getPoints(numberOfClearedLines: number, points: number): number { 349 | const addedPoints = MatrixUtil.Points[numberOfClearedLines - 1]; 350 | const newPoints = points + addedPoints; 351 | return newPoints > MatrixUtil.MaxPoint ? MatrixUtil.MaxPoint : newPoints; 352 | } 353 | 354 | private updateMatrix(pos: number, tile: Tile) { 355 | const newMatrix = [...this.tetrisState.matrix()]; 356 | newMatrix[pos] = tile; 357 | this.setMatrix(newMatrix); 358 | } 359 | 360 | private setNext() { 361 | this.tetrisState.updateState({ 362 | next: this.pieceFactory.getRandomPiece() 363 | }); 364 | } 365 | 366 | private setCurrentPiece(piece: Piece) { 367 | this.tetrisState.updateState({ 368 | current: piece 369 | }); 370 | } 371 | 372 | private setMatrix(matrix: Tile[]) { 373 | this.tetrisState.updateState({ 374 | matrix 375 | }); 376 | } 377 | 378 | private setLocked(locked: boolean) { 379 | this.tetrisState.updateState({ 380 | locked 381 | }); 382 | } 383 | 384 | private setHolded(piece: Piece): void { 385 | this.tetrisState.updateState({ 386 | hold: piece 387 | }); 388 | } 389 | 390 | private setCanHold(canHoldPiece: boolean) { 391 | this.tetrisState.updateState({ 392 | canHold: canHoldPiece 393 | }); 394 | } 395 | 396 | private stopGameInterval() { 397 | if (this.gameInterval) { 398 | clearInterval(this.gameInterval); 399 | } 400 | } 401 | 402 | private resetPosition(piece: Piece) { 403 | piece.x = SPAWN_POSITION_X; 404 | piece.y = SPAWN_POSITION_Y; 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/app/state/tetris/tetris.state.ts: -------------------------------------------------------------------------------- 1 | import { PieceFactory } from '@angular-tetris/factory/piece-factory'; 2 | import { GameState } from '@angular-tetris/interface/game-state'; 3 | import { Piece } from '@angular-tetris/interface/piece/piece'; 4 | import { Speed } from '@angular-tetris/interface/speed'; 5 | import { Tile } from '@angular-tetris/interface/tile/tile'; 6 | import { MatrixUtil } from '@angular-tetris/interface/utils/matrix'; 7 | import { LocalStorageService } from '@angular-tetris/services/local-storage.service'; 8 | import { Injectable, computed, inject, signal } from '@angular/core'; 9 | import { toObservable } from '@angular/core/rxjs-interop'; 10 | import { combineLatest, delay, of, switchMap } from 'rxjs'; 11 | 12 | export interface TetrisState { 13 | matrix: Tile[]; 14 | current: Piece | null; 15 | next: Piece; 16 | hold: Piece; 17 | canHold: boolean; 18 | points: number; 19 | locked: boolean; 20 | sound: boolean; 21 | initSpeed: Speed; 22 | speed: Speed; 23 | initLine: number; 24 | clearedLines: number; 25 | gameState: GameState; 26 | saved: TetrisState; 27 | max: number; 28 | } 29 | 30 | const createInitialState = (pieceFactory: PieceFactory): TetrisState => ({ 31 | matrix: MatrixUtil.getStartBoard(), 32 | current: null, 33 | next: pieceFactory.getRandomPiece(), 34 | hold: pieceFactory.getNonePiece(), 35 | canHold: true, 36 | points: 0, 37 | locked: true, 38 | sound: true, 39 | initLine: 0, 40 | clearedLines: 0, 41 | initSpeed: 1, 42 | speed: 1, 43 | gameState: GameState.Loading, 44 | saved: null, 45 | max: LocalStorageService.maxPoint 46 | }); 47 | 48 | const isObjectShallowEqual = (a: any, b: any) => a === b; 49 | 50 | @Injectable({ providedIn: 'root' }) 51 | export class TetrisStateService { 52 | private pieceFactory = inject(PieceFactory); 53 | private tetrisState = signal(createInitialState(this.pieceFactory)); 54 | 55 | next = computed(() => this.tetrisState().next, { 56 | equal: isObjectShallowEqual 57 | }); 58 | hold = computed(() => this.tetrisState().hold, { 59 | equal: isObjectShallowEqual 60 | }); 61 | matrix = computed(() => this.tetrisState().matrix, { 62 | equal: isObjectShallowEqual 63 | }); 64 | current = computed(() => this.tetrisState().current, { 65 | equal: isObjectShallowEqual 66 | }); 67 | isEnableSound = computed(() => this.tetrisState().sound); 68 | gameState = computed(() => this.tetrisState().gameState); 69 | hasCurrent = computed(() => !!this.current()); 70 | points = computed(() => this.tetrisState().points); 71 | clearedLines = computed(() => this.tetrisState().clearedLines); 72 | initLine = computed(() => this.tetrisState().initLine); 73 | speed = computed(() => this.tetrisState().speed); 74 | initSpeed = computed(() => this.tetrisState().initSpeed); 75 | max = computed(() => this.tetrisState().max); 76 | canStartGame = computed(() => this.gameState() !== GameState.Started); 77 | isPlaying = computed(() => this.gameState() === GameState.Started); 78 | isPause = computed(() => this.gameState() === GameState.Paused); 79 | locked = computed(() => this.tetrisState().locked); 80 | canHold = computed(() => this.tetrisState().canHold); 81 | 82 | gameState$ = toObservable(this.gameState); 83 | matrix$ = toObservable(this.matrix); 84 | current$ = toObservable(this.current); 85 | 86 | isShowLogo$ = combineLatest([this.gameState$, this.current$]).pipe( 87 | switchMap(([state, current]) => { 88 | const isLoadingOrOver = state === GameState.Loading || state === GameState.Over; 89 | const isRenderingLogo$ = of(isLoadingOrOver && !current); 90 | return isLoadingOrOver ? isRenderingLogo$.pipe(delay(1800)) : isRenderingLogo$; 91 | }) 92 | ); 93 | 94 | updateState(updatedState: Partial) { 95 | this.tetrisState.update((currentState) => ({ 96 | ...currentState, 97 | ...updatedState 98 | })); 99 | } 100 | 101 | resetState(updatedState: Partial) { 102 | this.tetrisState.set({ 103 | ...createInitialState(this.pieceFactory), 104 | ...updatedState 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/app/styles/_number.scss: -------------------------------------------------------------------------------- 1 | .number { 2 | height: 24px; 3 | font-size: 14px; 4 | float: right; 5 | 6 | span.num { 7 | float: left; 8 | width: 14px; 9 | height: 24px; 10 | } 11 | 12 | .num-0 { 13 | background-position: -75px -25px; 14 | } 15 | .num-1 { 16 | background-position: -89px -25px; 17 | } 18 | .num-2 { 19 | background-position: -103px -25px; 20 | } 21 | .num-3 { 22 | background-position: -117px -25px; 23 | } 24 | .num-4 { 25 | background-position: -131px -25px; 26 | } 27 | .num-5 { 28 | background-position: -145px -25px; 29 | } 30 | .num-6 { 31 | background-position: -159px -25px; 32 | } 33 | .num-7 { 34 | background-position: -173px -25px; 35 | } 36 | .num-8 { 37 | background-position: -187px -25px; 38 | } 39 | .num-9 { 40 | background-position: -201px -25px; 41 | } 42 | .num-n { 43 | background-position: -215px -25px; 44 | } 45 | .num-colon-faded { 46 | background-position: -243px -25px; 47 | } 48 | .num-colon-solid { 49 | background-position: -229px -25px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | $light-teal: #009688; 2 | 3 | * { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | background: $light-teal; 11 | 12 | padding: 0; 13 | margin: 0; 14 | font: 20px/1 "Helvetica Neue", "Helvetica", "Arial", sans-serif; 15 | 16 | overflow: hidden; 17 | cursor: default; 18 | text-rendering: optimizeLegibility; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | direction: ltr; 22 | text-align: left; 23 | } 24 | 25 | .state { 26 | width: 108px; 27 | position: absolute; 28 | top: 0; 29 | right: 15px; 30 | 31 | p { 32 | line-height: 43px; 33 | height: 50px; 34 | padding: 10px 0 0; 35 | white-space: nowrap; 36 | clear: both; 37 | } 38 | 39 | .last-row { 40 | position: absolute; 41 | width: 114px; 42 | top: 430px; 43 | left: 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/styles/_util.scss: -------------------------------------------------------------------------------- 1 | .r { 2 | float: right; 3 | } 4 | .l { 5 | float: left; 6 | } 7 | 8 | .clear { 9 | clear: both; 10 | } 11 | 12 | .ml-10 { 13 | margin-left: 10px; 14 | } 15 | 16 | .mr-10 { 17 | margin-right: 10px; 18 | } 19 | 20 | .w-40 { 21 | width: 40px; 22 | } 23 | 24 | .bg { 25 | background: url("/assets/img/bg.png") no-repeat; 26 | overflow: hidden; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/styles/tetris.scss: -------------------------------------------------------------------------------- 1 | @import "./reset"; 2 | @import "./number"; 3 | @import "./util"; 4 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/img/bg.png -------------------------------------------------------------------------------- /src/assets/js/AudioContextMonkeyPatch.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Chris Wilson 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | /* 17 | 18 | This monkeypatch library is intended to be included in projects that are 19 | written to the proper AudioContext spec (instead of webkitAudioContext), 20 | and that use the new naming and proper bits of the Web Audio API (e.g. 21 | using BufferSourceNode.start() instead of BufferSourceNode.noteOn()), but may 22 | have to run on systems that only support the deprecated bits. 23 | 24 | This library should be harmless to include if the browser supports 25 | unprefixed "AudioContext", and/or if it supports the new names. 26 | 27 | The patches this library handles: 28 | if window.AudioContext is unsupported, it will be aliased to webkitAudioContext(). 29 | if AudioBufferSourceNode.start() is unimplemented, it will be routed to noteOn() or 30 | noteGrainOn(), depending on parameters. 31 | 32 | The following aliases only take effect if the new names are not already in place: 33 | 34 | AudioBufferSourceNode.stop() is aliased to noteOff() 35 | AudioContext.createGain() is aliased to createGainNode() 36 | AudioContext.createDelay() is aliased to createDelayNode() 37 | AudioContext.createScriptProcessor() is aliased to createJavaScriptNode() 38 | AudioContext.createPeriodicWave() is aliased to createWaveTable() 39 | OscillatorNode.start() is aliased to noteOn() 40 | OscillatorNode.stop() is aliased to noteOff() 41 | OscillatorNode.setPeriodicWave() is aliased to setWaveTable() 42 | AudioParam.setTargetAtTime() is aliased to setTargetValueAtTime() 43 | 44 | This library does NOT patch the enumerated type changes, as it is 45 | recommended in the specification that implementations support both integer 46 | and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel 47 | BiquadFilterNode.type and OscillatorNode.type. 48 | 49 | */ 50 | (function (global, exports, perf) { 51 | 'use strict'; 52 | 53 | function fixSetTarget(param) { 54 | if (!param) // if NYI, just return 55 | return; 56 | if (!param.setTargetAtTime) 57 | param.setTargetAtTime = param.setTargetValueAtTime; 58 | } 59 | 60 | if (window.hasOwnProperty('webkitAudioContext') && 61 | !window.hasOwnProperty('AudioContext')) { 62 | window.AudioContext = webkitAudioContext; 63 | 64 | if (!AudioContext.prototype.hasOwnProperty('createGain')) 65 | AudioContext.prototype.createGain = AudioContext.prototype.createGainNode; 66 | if (!AudioContext.prototype.hasOwnProperty('createDelay')) 67 | AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode; 68 | if (!AudioContext.prototype.hasOwnProperty('createScriptProcessor')) 69 | AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode; 70 | if (!AudioContext.prototype.hasOwnProperty('createPeriodicWave')) 71 | AudioContext.prototype.createPeriodicWave = AudioContext.prototype.createWaveTable; 72 | 73 | 74 | AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain; 75 | AudioContext.prototype.createGain = function() { 76 | var node = this.internal_createGain(); 77 | fixSetTarget(node.gain); 78 | return node; 79 | }; 80 | 81 | AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay; 82 | AudioContext.prototype.createDelay = function(maxDelayTime) { 83 | var node = maxDelayTime ? this.internal_createDelay(maxDelayTime) : this.internal_createDelay(); 84 | fixSetTarget(node.delayTime); 85 | return node; 86 | }; 87 | 88 | AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource; 89 | AudioContext.prototype.createBufferSource = function() { 90 | var node = this.internal_createBufferSource(); 91 | if (!node.start) { 92 | node.start = function ( when, offset, duration ) { 93 | if ( offset || duration ) 94 | this.noteGrainOn( when || 0, offset, duration ); 95 | else 96 | this.noteOn( when || 0 ); 97 | }; 98 | } else { 99 | node.internal_start = node.start; 100 | node.start = function( when, offset, duration ) { 101 | if( typeof duration !== 'undefined' ) 102 | node.internal_start( when || 0, offset, duration ); 103 | else 104 | node.internal_start( when || 0, offset || 0 ); 105 | }; 106 | } 107 | if (!node.stop) { 108 | node.stop = function ( when ) { 109 | this.noteOff( when || 0 ); 110 | }; 111 | } else { 112 | node.internal_stop = node.stop; 113 | node.stop = function( when ) { 114 | node.internal_stop( when || 0 ); 115 | }; 116 | } 117 | fixSetTarget(node.playbackRate); 118 | return node; 119 | }; 120 | 121 | AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor; 122 | AudioContext.prototype.createDynamicsCompressor = function() { 123 | var node = this.internal_createDynamicsCompressor(); 124 | fixSetTarget(node.threshold); 125 | fixSetTarget(node.knee); 126 | fixSetTarget(node.ratio); 127 | fixSetTarget(node.reduction); 128 | fixSetTarget(node.attack); 129 | fixSetTarget(node.release); 130 | return node; 131 | }; 132 | 133 | AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter; 134 | AudioContext.prototype.createBiquadFilter = function() { 135 | var node = this.internal_createBiquadFilter(); 136 | fixSetTarget(node.frequency); 137 | fixSetTarget(node.detune); 138 | fixSetTarget(node.Q); 139 | fixSetTarget(node.gain); 140 | return node; 141 | }; 142 | 143 | if (AudioContext.prototype.hasOwnProperty( 'createOscillator' )) { 144 | AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator; 145 | AudioContext.prototype.createOscillator = function() { 146 | var node = this.internal_createOscillator(); 147 | if (!node.start) { 148 | node.start = function ( when ) { 149 | this.noteOn( when || 0 ); 150 | }; 151 | } else { 152 | node.internal_start = node.start; 153 | node.start = function ( when ) { 154 | node.internal_start( when || 0); 155 | }; 156 | } 157 | if (!node.stop) { 158 | node.stop = function ( when ) { 159 | this.noteOff( when || 0 ); 160 | }; 161 | } else { 162 | node.internal_stop = node.stop; 163 | node.stop = function( when ) { 164 | node.internal_stop( when || 0 ); 165 | }; 166 | } 167 | if (!node.setPeriodicWave) 168 | node.setPeriodicWave = node.setWaveTable; 169 | fixSetTarget(node.frequency); 170 | fixSetTarget(node.detune); 171 | return node; 172 | }; 173 | } 174 | } 175 | 176 | if (window.hasOwnProperty('webkitOfflineAudioContext') && 177 | !window.hasOwnProperty('OfflineAudioContext')) { 178 | window.OfflineAudioContext = webkitOfflineAudioContext; 179 | } 180 | 181 | }(window)); 182 | 183 | -------------------------------------------------------------------------------- /src/assets/readme/akita-devtool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/akita-devtool.gif -------------------------------------------------------------------------------- /src/assets/readme/angular-tetris-demo-sound.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/angular-tetris-demo-sound.mp4 -------------------------------------------------------------------------------- /src/assets/readme/angular-tetris-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/angular-tetris-demo.gif -------------------------------------------------------------------------------- /src/assets/readme/angular-tetris-iphonex.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/angular-tetris-iphonex.gif -------------------------------------------------------------------------------- /src/assets/readme/compare01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/compare01.png -------------------------------------------------------------------------------- /src/assets/readme/compare02-result.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/compare02-result.gif -------------------------------------------------------------------------------- /src/assets/readme/compare02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/compare02.png -------------------------------------------------------------------------------- /src/assets/readme/piecef-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/piecef-demo.gif -------------------------------------------------------------------------------- /src/assets/readme/retro-tetris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/retro-tetris.jpg -------------------------------------------------------------------------------- /src/assets/readme/tech-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/tech-stack.png -------------------------------------------------------------------------------- /src/assets/readme/tetris-social-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/tetris-social-cover.png -------------------------------------------------------------------------------- /src/assets/readme/time-spending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/readme/time-spending.png -------------------------------------------------------------------------------- /src/assets/tetris-sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/assets/tetris-sound.mp3 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trungvose/angular-tetris/179b593e0cbdfc4c7f1f542aabeebd413dc37d8a/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | Angular Tetris built with Akita - by trung18 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/index.prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | Angular Tetris built with Akita - by trung18 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, ErrorHandler, importProvidersFrom } from '@angular/core'; 2 | import { bootstrapApplication } from '@angular/platform-browser'; 3 | import { AkitaNgDevtools } from '@datorama/akita-ngdevtools'; 4 | import * as Sentry from '@sentry/angular'; 5 | import { Integrations } from '@sentry/tracing'; 6 | import { AppComponent } from '@angular-tetris/app.component'; 7 | import { environment } from './environments/environment'; 8 | 9 | const initSentry = () => { 10 | Sentry.init({ 11 | dsn: 'https://91dfe2ed3a6c47f8a5a14188066cc9f2@o495789.ingest.sentry.io/5570178', 12 | autoSessionTracking: true, 13 | integrations: [ 14 | new Integrations.BrowserTracing({ 15 | tracingOrigins: ['localhost', 'https://tetris.trungk18.com'], 16 | routingInstrumentation: Sentry.routingInstrumentation 17 | }) 18 | ], 19 | tracesSampleRate: 1.0 20 | }); 21 | }; 22 | 23 | if (environment.production) { 24 | enableProdMode(); 25 | initSentry(); 26 | } 27 | 28 | bootstrapApplication(AppComponent, { 29 | providers: [ 30 | importProvidersFrom(AkitaNgDevtools.forRoot()), 31 | { 32 | provide: ErrorHandler, 33 | useValue: Sentry.createErrorHandler() 34 | } 35 | ] 36 | }); 37 | -------------------------------------------------------------------------------- /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'; 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 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '../src/app/styles/tetris.scss'; 2 | 3 | t-keyboard, t-button { 4 | touch-action: pan-x pan-y pinch-zoom; 5 | } 6 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "paths": { 6 | "@angular-tetris/*": [ 7 | "src/app/*" 8 | ], 9 | "@angular-tetris/interface/*": [ 10 | "src/app/interface/*" 11 | ] 12 | }, 13 | "outDir": "./dist/out-tsc", 14 | "sourceMap": true, 15 | "declaration": false, 16 | "downlevelIteration": true, 17 | "experimentalDecorators": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "importHelpers": true, 21 | "target": "ES2022", 22 | "lib": [ 23 | "es2018", 24 | "dom" 25 | ], 26 | "useDefineForClassFields": false 27 | }, 28 | "angularCompilerOptions": { 29 | "fullTemplateTypeCheck": true, 30 | "strictInjectionParameters": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | --------------------------------------------------------------------------------