├── .env.dev
├── .env.prod
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── index.html
├── package.json
├── preview.gif
├── public
├── audio
│ ├── back_001.ogg
│ ├── back_002.ogg
│ ├── back_003.ogg
│ ├── back_004.ogg
│ ├── bong_001.ogg
│ ├── click_001.ogg
│ ├── click_002.ogg
│ ├── click_003.ogg
│ ├── click_004.ogg
│ ├── click_005.ogg
│ ├── close_001.ogg
│ ├── close_002.ogg
│ ├── close_003.ogg
│ ├── close_004.ogg
│ ├── confirmation_001.ogg
│ ├── confirmation_002.ogg
│ ├── confirmation_003.ogg
│ ├── confirmation_004.ogg
│ ├── drop_001.ogg
│ ├── drop_002.ogg
│ ├── drop_003.ogg
│ ├── drop_004.ogg
│ ├── error_001.ogg
│ ├── error_002.ogg
│ ├── error_003.ogg
│ ├── error_004.ogg
│ ├── error_005.ogg
│ ├── error_006.ogg
│ ├── error_007.ogg
│ ├── error_008.ogg
│ ├── glass_001.ogg
│ ├── glass_002.ogg
│ ├── glass_003.ogg
│ ├── glass_004.ogg
│ ├── glass_005.ogg
│ ├── glass_006.ogg
│ ├── glitch_001.ogg
│ ├── glitch_002.ogg
│ ├── glitch_003.ogg
│ ├── glitch_004.ogg
│ ├── maximize_001.ogg
│ ├── maximize_002.ogg
│ ├── maximize_003.ogg
│ ├── maximize_004.ogg
│ ├── maximize_005.ogg
│ ├── maximize_006.ogg
│ ├── maximize_007.ogg
│ ├── maximize_008.ogg
│ ├── maximize_009.ogg
│ ├── minimize_001.ogg
│ ├── minimize_002.ogg
│ ├── minimize_003.ogg
│ ├── minimize_004.ogg
│ ├── minimize_005.ogg
│ ├── minimize_006.ogg
│ ├── minimize_007.ogg
│ ├── minimize_008.ogg
│ ├── minimize_009.ogg
│ ├── open_001.ogg
│ ├── open_002.ogg
│ ├── open_003.ogg
│ ├── open_004.ogg
│ ├── pluck_001.ogg
│ ├── pluck_002.ogg
│ ├── question_001.ogg
│ ├── question_002.ogg
│ ├── question_003.ogg
│ ├── question_004.ogg
│ ├── scratch_001.ogg
│ ├── scratch_002.ogg
│ ├── scratch_003.ogg
│ ├── scratch_004.ogg
│ ├── scratch_005.ogg
│ ├── scroll_001.ogg
│ ├── scroll_002.ogg
│ ├── scroll_003.ogg
│ ├── scroll_004.ogg
│ ├── scroll_005.ogg
│ ├── select_001.ogg
│ ├── select_002.ogg
│ ├── select_003.ogg
│ ├── select_004.ogg
│ ├── select_005.ogg
│ ├── select_006.ogg
│ ├── select_007.ogg
│ ├── select_008.ogg
│ ├── switch_001.ogg
│ ├── switch_002.ogg
│ ├── switch_003.ogg
│ ├── switch_004.ogg
│ ├── switch_005.ogg
│ ├── switch_006.ogg
│ ├── switch_007.ogg
│ ├── tick_001.ogg
│ ├── tick_002.ogg
│ ├── tick_004.ogg
│ ├── toggle_001.ogg
│ ├── toggle_002.ogg
│ ├── toggle_003.ogg
│ └── toggle_004.ogg
├── fonts
│ ├── Lato-Italic.woff2
│ ├── Lato-Regular.woff2
│ └── MonomaniacOne-Regular.woff2
└── icons
│ ├── crown-icon.svg
│ ├── github-icon.svg
│ ├── play-icon.svg
│ ├── react-icon.svg
│ ├── tetris-favicon.svg
│ └── twitter-icon.svg
├── src
├── App.css
├── App.tsx
├── __tests__
│ └── NextPieceCalculator.test.ts
├── components
│ ├── Block.tsx
│ ├── BoardContainer.tsx
│ ├── EmptyBlock.tsx
│ ├── GridView.tsx
│ ├── GroupPieceView.tsx
│ ├── Particle.tsx
│ ├── PieceView.tsx
│ ├── Score.tsx
│ └── ScoreContainer.tsx
├── configs
│ └── index.ts
├── constants
│ └── index.ts
├── containers
│ ├── AudioContainer.tsx
│ ├── ContainerBoard.tsx
│ ├── ContainerScore.tsx
│ └── ContainerTicker.tsx
├── controller
│ ├── graph.ts
│ └── index.ts
├── factories
│ ├── BlockFactory.tsx
│ ├── NextPieceCalculator.ts
│ ├── ParticlesData.ts
│ └── PieceFactory.ts
├── handlers
│ └── index.ts
├── index.css
├── input
│ └── keyboardInput.ts
├── main.tsx
├── screens
│ ├── Menu.css
│ └── Menu.tsx
├── store
│ ├── actions
│ │ ├── audio.ts
│ │ ├── blocks.ts
│ │ ├── score.ts
│ │ └── ticks.ts
│ ├── index.ts
│ └── reducers
│ │ ├── audio.ts
│ │ ├── blocks.ts
│ │ ├── collision
│ │ └── index.ts
│ │ ├── index.ts
│ │ ├── score.ts
│ │ └── ticks.ts
├── styles
│ ├── blocks.module.css
│ ├── particles.module.css
│ ├── reset.css
│ └── score.module.css
└── types
│ ├── block.d.ts
│ ├── grid.d.ts
│ └── index.ts
├── tsconfig.json
├── vercel.json
├── vite.config.ts
├── vitest.setup.ts
└── yarn.lock
/.env.dev:
--------------------------------------------------------------------------------
1 | VITE_PORT=3000
2 | VITE_ENVIRONMENT=DEV
3 | VITE_PUBLIC_PATH=
4 |
--------------------------------------------------------------------------------
/.env.prod:
--------------------------------------------------------------------------------
1 | VITE_PORT=3001
2 | VITE_ENVIRONMENT=PROD
3 | VITE_PUBLIC_PATH=https://tetris-react-tau.vercel.app
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | yarn.lock
3 | .gitignore
4 | .prettierignore
5 | .gitattributes
6 | coverage
7 | vite.config.ts
8 | vitest.setup.ts
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2022": true,
5 | "node": true
6 | },
7 | "settings": {
8 | "react": {
9 | "version": "detect"
10 | },
11 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
12 | "import/parsers": {
13 | "@typescript-eslint/parser": [".ts", ".tsx"]
14 | },
15 | "import/resolver": {
16 | "typescript": {
17 | "project": "./tsconfig.json"
18 | }
19 | }
20 | },
21 | "plugins": [
22 | "react",
23 | "react-hooks",
24 | "@typescript-eslint",
25 | "promise",
26 | "prettier",
27 | "testing-library",
28 | "jest-dom",
29 | "jest",
30 | "jsx-a11y",
31 | "simple-import-sort"
32 | ],
33 | "extends": [
34 | "standard-with-typescript",
35 | "eslint:recommended",
36 | "plugin:react/recommended",
37 | "plugin:react-hooks/recommended",
38 | "plugin:@typescript-eslint/recommended",
39 | "plugin:promise/recommended",
40 | "plugin:jsx-a11y/recommended",
41 | "plugin:import/errors",
42 | "plugin:import/typescript",
43 | "prettier",
44 | "plugin:prettier/recommended",
45 | "plugin:jest-dom/recommended",
46 | "plugin:testing-library/react",
47 | "plugin:jest/style"
48 | ],
49 | "parser": "@typescript-eslint/parser",
50 | "parserOptions": {
51 | "ecmaFeatures": {
52 | "jsx": true
53 | },
54 | "ecmaVersion": "latest",
55 | "sourceType": "module"
56 | },
57 | "rules": {
58 | "simple-import-sort/imports": "error",
59 | "@typescript-eslint/prefer-nullish-coalescing": "off",
60 | "@typescript-eslint/strict-boolean-expressions": "off",
61 | "@typescript-eslint/explicit-function-return-type": "off",
62 | "@typescript-eslint/consistent-type-definitions": "off",
63 | "@typescript-eslint/no-floating-promises": [
64 | "error",
65 | {
66 | "ignoreVoid": false
67 | }
68 | ],
69 | "react/jsx-sort-props": "warn",
70 | "object-shorthand": "warn",
71 | "@typescript-eslint/no-var-requires": "off",
72 | "@typescript-eslint/ban-ts-comment": "off",
73 | "no-console": "error",
74 | "react/jsx-filename-extension": [1, { "extensions": [".ts", ".tsx"] }],
75 | "@typescript-eslint/no-explicit-any": "off",
76 | "@typescript-eslint/no-unused-vars": "error",
77 | "react/jsx-uses-react": "error",
78 | "react/jsx-uses-vars": "error",
79 | "jsx-a11y/anchor-is-valid": "off",
80 | "react-hooks/rules-of-hooks": "error",
81 | "react-hooks/exhaustive-deps": "warn",
82 | "react/prop-types": "off",
83 | "react/react-in-jsx-scope": "off",
84 | "@typescript-eslint-in-jsx-scope": "off",
85 | "@typescript-eslint/no-non-null-assertion": "warn",
86 | "space-before-function-paren": "off",
87 | "@typescript-eslint/naming-convention": "off",
88 | "eslint-disable-next-line": "off",
89 | "react/display-name": "off",
90 | "prettier/prettier": "error",
91 | "@typescript-eslint/no-invalid-void-type": "off"
92 | },
93 | "overrides": [
94 | {
95 | "files": ["**/__tests__/?(*.)+(spec|test).tsx?"],
96 | "plugins": ["jest-dom", "testing-library", "jest"],
97 | "extends": [
98 | "plugin:jest-dom/recommended",
99 | "plugin:testing-library/react",
100 | "plugin:jest/recommended",
101 | "plugin:jest/style"
102 | ]
103 | }
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /dist
11 | /build
12 |
13 | # misc
14 | .DS_Store
15 | *.pem
16 |
17 | # debug
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | # vercel
23 | .vercel
24 |
25 | # jetbrains and vscode
26 | .idea/
27 | .vscode/
28 |
29 | # PWA
30 | **/public/workbox-*.js
31 | **/public/sw.js
32 | **/public/fallback-*.js
33 |
34 | # storybook
35 | /storybook-static
36 | /src/stories
37 |
38 | # linters
39 | .eslintcache
40 | .stylelintcache
41 | .husky
42 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | yarn.lock
3 | .gitignore
4 | .eslintignore
5 | coverage
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "tabWidth": 2,
4 | "printWidth": 80,
5 | "singleQuote": true,
6 | "bracketSpacing": true,
7 | "trailingComma": "none",
8 | "arrowParens": "avoid",
9 | "endOfLine": "lf"
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 José Paulo
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-tetris! ⚛️🕹️
2 |
3 | A minimalist tetris game made with React.js.
4 | 
5 |
6 | ## How to play
7 |
8 | You can play it here: https://tetris-react-lmnz.vercel.app/
9 |
10 | Use directional inputs or swipe on mobile to move sideways, swipe up to rotate, and swipe down for a faster fall.
11 |
12 | ## Project description
13 |
14 | I made this game primarily to study Redux and Error handling. Although it has some code smells, it is generally well made, and more importantly for a game: has great time sessions and replay rate. Key features include:
15 |
16 | - Modular architecture of piece and blocks
17 | - Rudimentar AI balancement
18 | - Redux for state management
19 | - Errors as collision triggers
20 | - Pseudo 3d visuals and VFX
21 |
22 | ## Installation Instructions
23 |
24 | The project uses this base TS React template with linting and more: https://github.com/TheSwordBreaker/vite-reactts-eslint-prettier. You can run the commands below for install dependencies and run the project locally:
25 |
26 | ```bash
27 | yarn
28 | yarn dev
29 | ```
30 |
31 | ## Contact and license
32 |
33 | For any inquiries or feedback, feel free to reach out:
34 |
35 | - Email: [fernandes.josepaulo95@gmail.com](mailto:fernandes.josepaulo95@gmail.com)
36 | - Twitter: [@paulinhogamedev](https://twitter.com/paulinhogamedev)
37 |
38 | This project is licensed under the [MIT License](LICENSE).
39 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tetris
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tetris-react",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "author": "José Paulo Fernandes Neto",
7 | "description": "This project is a tetris game made in React with Typescript and Redux Toolkit.",
8 | "repository": "https://github.com/JosePaulo95/tetris-react",
9 | "scripts": {
10 | "dev": "vite --mode dev",
11 | "build:dev": "vite build --mode dev",
12 | "build:prod": "vite build --mode prod",
13 | "serve": "vite preview --mode dev --port 3001",
14 | "lint": "eslint --cache src --ext js,jsx,ts,tsx,json --max-warnings=0",
15 | "lint:fix": "eslint --cache --fix src --ext js,jsx,ts,tsx,json",
16 | "format": "prettier --cache --write src",
17 | "check:format": "prettier --cache --check src",
18 | "typecheck": "tsc --noEmit",
19 | "test": "vitest run",
20 | "test:c": "vitest run --coverage",
21 | "test:w": "vitest",
22 | "test:ci": "vitest run"
23 | },
24 | "dependencies": {
25 | "@reduxjs/toolkit": "^1.9.1",
26 | "audio-loader": "^1.0.3",
27 | "framer-motion": "^8.5.4",
28 | "game-inputs": "^0.7.0",
29 | "hammerjs": "^2.0.8",
30 | "howler": "^2.2.3",
31 | "react": "^18.2.0",
32 | "react-dom": "^18.2.0",
33 | "react-redux": "^8.0.5",
34 | "react-router-dom": "^6.13.0"
35 | },
36 | "devDependencies": {
37 | "@testing-library/dom": "^9.3.3",
38 | "@testing-library/jest-dom": "^6.1.4",
39 | "@testing-library/react": "^14.0.0",
40 | "@testing-library/user-event": "^14.5.1",
41 | "@types/hammerjs": "^2.0.41",
42 | "@types/howler": "^2.2.7",
43 | "@types/jsdom-global": "^3.0.5",
44 | "@types/node": "^20.8.6",
45 | "@types/react": "^18.0.26",
46 | "@types/react-dom": "^18.0.10",
47 | "@typescript-eslint/eslint-plugin": "^6.8.0",
48 | "@typescript-eslint/parser": "^6.8.0",
49 | "@vitejs/plugin-react-swc": "^3.4.0",
50 | "@vitest/coverage-v8": "^0.34.6",
51 | "eslint": "^8.8.0",
52 | "eslint-config-prettier": "9.0.0",
53 | "eslint-config-standard-with-typescript": "^39.1.0",
54 | "eslint-import-resolver-typescript": "3.6.1",
55 | "eslint-plugin-import": "2.28.1",
56 | "eslint-plugin-jest": "^27.4.2",
57 | "eslint-plugin-jest-dom": "^5.1.0",
58 | "eslint-plugin-jsx-a11y": "6.7.1",
59 | "eslint-plugin-n": "16.1.0",
60 | "eslint-plugin-prettier": "5.0.1",
61 | "eslint-plugin-promise": "6.1.1",
62 | "eslint-plugin-react": "7.33.2",
63 | "eslint-plugin-react-hooks": "4.6.0",
64 | "eslint-plugin-simple-import-sort": "^10.0.0",
65 | "eslint-plugin-storybook": "^0.6.14",
66 | "eslint-plugin-testing-library": "6.0.2",
67 | "happy-dom": "^12.9.1",
68 | "jsdom": "^22.1.0",
69 | "jsdom-global": "^3.0.2",
70 | "pre-commit": "^1.2.2",
71 | "prettier": "^3.0.3",
72 | "typescript": "^5.2.2",
73 | "vite": "^4.4.11",
74 | "vite-tsconfig-paths": "^4.2.1",
75 | "vitest": "^0.34.6"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/preview.gif
--------------------------------------------------------------------------------
/public/audio/back_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/back_001.ogg
--------------------------------------------------------------------------------
/public/audio/back_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/back_002.ogg
--------------------------------------------------------------------------------
/public/audio/back_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/back_003.ogg
--------------------------------------------------------------------------------
/public/audio/back_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/back_004.ogg
--------------------------------------------------------------------------------
/public/audio/bong_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/bong_001.ogg
--------------------------------------------------------------------------------
/public/audio/click_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/click_001.ogg
--------------------------------------------------------------------------------
/public/audio/click_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/click_002.ogg
--------------------------------------------------------------------------------
/public/audio/click_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/click_003.ogg
--------------------------------------------------------------------------------
/public/audio/click_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/click_004.ogg
--------------------------------------------------------------------------------
/public/audio/click_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/click_005.ogg
--------------------------------------------------------------------------------
/public/audio/close_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/close_001.ogg
--------------------------------------------------------------------------------
/public/audio/close_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/close_002.ogg
--------------------------------------------------------------------------------
/public/audio/close_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/close_003.ogg
--------------------------------------------------------------------------------
/public/audio/close_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/close_004.ogg
--------------------------------------------------------------------------------
/public/audio/confirmation_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/confirmation_001.ogg
--------------------------------------------------------------------------------
/public/audio/confirmation_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/confirmation_002.ogg
--------------------------------------------------------------------------------
/public/audio/confirmation_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/confirmation_003.ogg
--------------------------------------------------------------------------------
/public/audio/confirmation_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/confirmation_004.ogg
--------------------------------------------------------------------------------
/public/audio/drop_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/drop_001.ogg
--------------------------------------------------------------------------------
/public/audio/drop_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/drop_002.ogg
--------------------------------------------------------------------------------
/public/audio/drop_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/drop_003.ogg
--------------------------------------------------------------------------------
/public/audio/drop_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/drop_004.ogg
--------------------------------------------------------------------------------
/public/audio/error_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_001.ogg
--------------------------------------------------------------------------------
/public/audio/error_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_002.ogg
--------------------------------------------------------------------------------
/public/audio/error_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_003.ogg
--------------------------------------------------------------------------------
/public/audio/error_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_004.ogg
--------------------------------------------------------------------------------
/public/audio/error_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_005.ogg
--------------------------------------------------------------------------------
/public/audio/error_006.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_006.ogg
--------------------------------------------------------------------------------
/public/audio/error_007.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_007.ogg
--------------------------------------------------------------------------------
/public/audio/error_008.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/error_008.ogg
--------------------------------------------------------------------------------
/public/audio/glass_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glass_001.ogg
--------------------------------------------------------------------------------
/public/audio/glass_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glass_002.ogg
--------------------------------------------------------------------------------
/public/audio/glass_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glass_003.ogg
--------------------------------------------------------------------------------
/public/audio/glass_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glass_004.ogg
--------------------------------------------------------------------------------
/public/audio/glass_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glass_005.ogg
--------------------------------------------------------------------------------
/public/audio/glass_006.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glass_006.ogg
--------------------------------------------------------------------------------
/public/audio/glitch_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glitch_001.ogg
--------------------------------------------------------------------------------
/public/audio/glitch_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glitch_002.ogg
--------------------------------------------------------------------------------
/public/audio/glitch_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glitch_003.ogg
--------------------------------------------------------------------------------
/public/audio/glitch_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/glitch_004.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_001.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_002.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_003.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_004.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_005.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_006.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_006.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_007.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_007.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_008.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_008.ogg
--------------------------------------------------------------------------------
/public/audio/maximize_009.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/maximize_009.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_001.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_002.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_003.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_004.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_005.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_006.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_006.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_007.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_007.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_008.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_008.ogg
--------------------------------------------------------------------------------
/public/audio/minimize_009.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/minimize_009.ogg
--------------------------------------------------------------------------------
/public/audio/open_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/open_001.ogg
--------------------------------------------------------------------------------
/public/audio/open_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/open_002.ogg
--------------------------------------------------------------------------------
/public/audio/open_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/open_003.ogg
--------------------------------------------------------------------------------
/public/audio/open_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/open_004.ogg
--------------------------------------------------------------------------------
/public/audio/pluck_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/pluck_001.ogg
--------------------------------------------------------------------------------
/public/audio/pluck_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/pluck_002.ogg
--------------------------------------------------------------------------------
/public/audio/question_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/question_001.ogg
--------------------------------------------------------------------------------
/public/audio/question_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/question_002.ogg
--------------------------------------------------------------------------------
/public/audio/question_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/question_003.ogg
--------------------------------------------------------------------------------
/public/audio/question_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/question_004.ogg
--------------------------------------------------------------------------------
/public/audio/scratch_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scratch_001.ogg
--------------------------------------------------------------------------------
/public/audio/scratch_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scratch_002.ogg
--------------------------------------------------------------------------------
/public/audio/scratch_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scratch_003.ogg
--------------------------------------------------------------------------------
/public/audio/scratch_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scratch_004.ogg
--------------------------------------------------------------------------------
/public/audio/scratch_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scratch_005.ogg
--------------------------------------------------------------------------------
/public/audio/scroll_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scroll_001.ogg
--------------------------------------------------------------------------------
/public/audio/scroll_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scroll_002.ogg
--------------------------------------------------------------------------------
/public/audio/scroll_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scroll_003.ogg
--------------------------------------------------------------------------------
/public/audio/scroll_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scroll_004.ogg
--------------------------------------------------------------------------------
/public/audio/scroll_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/scroll_005.ogg
--------------------------------------------------------------------------------
/public/audio/select_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_001.ogg
--------------------------------------------------------------------------------
/public/audio/select_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_002.ogg
--------------------------------------------------------------------------------
/public/audio/select_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_003.ogg
--------------------------------------------------------------------------------
/public/audio/select_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_004.ogg
--------------------------------------------------------------------------------
/public/audio/select_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_005.ogg
--------------------------------------------------------------------------------
/public/audio/select_006.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_006.ogg
--------------------------------------------------------------------------------
/public/audio/select_007.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_007.ogg
--------------------------------------------------------------------------------
/public/audio/select_008.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/select_008.ogg
--------------------------------------------------------------------------------
/public/audio/switch_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_001.ogg
--------------------------------------------------------------------------------
/public/audio/switch_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_002.ogg
--------------------------------------------------------------------------------
/public/audio/switch_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_003.ogg
--------------------------------------------------------------------------------
/public/audio/switch_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_004.ogg
--------------------------------------------------------------------------------
/public/audio/switch_005.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_005.ogg
--------------------------------------------------------------------------------
/public/audio/switch_006.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_006.ogg
--------------------------------------------------------------------------------
/public/audio/switch_007.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/switch_007.ogg
--------------------------------------------------------------------------------
/public/audio/tick_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/tick_001.ogg
--------------------------------------------------------------------------------
/public/audio/tick_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/tick_002.ogg
--------------------------------------------------------------------------------
/public/audio/tick_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/tick_004.ogg
--------------------------------------------------------------------------------
/public/audio/toggle_001.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/toggle_001.ogg
--------------------------------------------------------------------------------
/public/audio/toggle_002.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/toggle_002.ogg
--------------------------------------------------------------------------------
/public/audio/toggle_003.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/toggle_003.ogg
--------------------------------------------------------------------------------
/public/audio/toggle_004.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/audio/toggle_004.ogg
--------------------------------------------------------------------------------
/public/fonts/Lato-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/fonts/Lato-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Lato-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/fonts/Lato-Regular.woff2
--------------------------------------------------------------------------------
/public/fonts/MonomaniacOne-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosePaulo95/tetris-react/91105c8af7d4d5e82e07fb2d42fa15b9ece784d0/public/fonts/MonomaniacOne-Regular.woff2
--------------------------------------------------------------------------------
/public/icons/crown-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
38 |
--------------------------------------------------------------------------------
/public/icons/github-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
28 |
--------------------------------------------------------------------------------
/public/icons/play-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/react-icon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/icons/tetris-favicon.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/public/icons/twitter-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | width: 100%;
3 | height: 100vh;
4 | background-color: cornflowerblue;
5 | display: grid;
6 | place-items: center;
7 | }
8 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import './App.css'
2 |
3 | import AudioContainer from './containers/AudioContainer'
4 | import ContainerBoard from './containers/ContainerBoard'
5 | import ContainerScore from './containers/ContainerScore'
6 | import ContainerTicker from './containers/ContainerTicker'
7 |
8 | function App() {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 | >
16 | )
17 | }
18 | export default App
19 |
--------------------------------------------------------------------------------
/src/__tests__/NextPieceCalculator.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EMPTY_GRID,
3 | PIECE_I_GRIDS,
4 | PIECE_L_GRIDS,
5 | PIECE_Z_GRIDS
6 | } from '../constants'
7 | import { calcAvgHeight, countCombinations, wrapGrid } from '../controller'
8 | import {
9 | calcMaxMatches,
10 | getMaxY,
11 | getPossiveisX
12 | } from '../factories/NextPieceCalculator'
13 | import { createPiece } from '../factories/PieceFactory'
14 | import type { Grid } from '../types'
15 |
16 | test('calcula x possíveis adequadamente da peça z', () => {
17 | const piece_grids = PIECE_Z_GRIDS(1)
18 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, 6, 8)))
19 | const x_possiveis = getPossiveisX(piece)
20 | expect(x_possiveis).toEqual([-1, 0, 1, 2])
21 | })
22 |
23 | test('calcula x possíveis adequadamente da peça l', () => {
24 | const piece_grids = PIECE_I_GRIDS(1)
25 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, 6, 8)))
26 | const x_possiveis = getPossiveisX({ ...piece, rotations: 1 })
27 | expect(x_possiveis).toEqual([-1, 0, 1])
28 | })
29 |
30 | test('calcula max y para peça L e tabuleiro vazio', () => {
31 | const width = 6
32 | const height = 6
33 |
34 | const piece_grids = PIECE_L_GRIDS(1)
35 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, width, height)))
36 | const board = wrapGrid(EMPTY_GRID(), width, height)
37 |
38 | const max_y = getMaxY(piece, board)
39 | expect(max_y).toBe(3)
40 | })
41 |
42 | test('calcula max y para peça z num tabuleiro exato', () => {
43 | const width = 6
44 | const height = 6
45 |
46 | const piece_grids = PIECE_Z_GRIDS(1)
47 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, width, height)))
48 | const b = [
49 | [0, 0, 0, 0],
50 | [0, 0, 0, 0],
51 | [0, 0, 0, 0],
52 | [1, 0, 0, 1],
53 | [1, 0, 1, 1],
54 | [1, 1, 1, 1]
55 | ]
56 | const board = wrapGrid(b, width, height)
57 |
58 | piece.rotations = 1
59 | const max_y = getMaxY(piece, board)
60 | expect(max_y).toBe(2)
61 | })
62 |
63 | test('conta corretamente a qtde de matches', () => {
64 | const width = 4
65 | const height = 6
66 |
67 | const piece_grids = PIECE_Z_GRIDS(1)
68 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, width, height)))
69 | const b = [
70 | [0, 0, 0, 0],
71 | [0, 0, 0, 0],
72 | [0, 0, 0, 0],
73 | [1, 0, 0, 1],
74 | [1, 0, 1, 1],
75 | [1, 1, 1, 1]
76 | ]
77 | const board = wrapGrid(b, width, height)
78 |
79 | piece.rotations = 1
80 | piece.y = 2
81 | expect(countCombinations(board, piece)).toBe(3)
82 | })
83 |
84 | test('conta matches parciais', () => {
85 | const width = 4
86 | const height = 6
87 |
88 | const piece_grids = [EMPTY_GRID()]
89 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, width, height)))
90 | const b = [
91 | [0, 0, 0, 0],
92 | [0, 0, 0, 0],
93 | [1, 0, 3, 0],
94 | [1, 2, 3, 0],
95 | [1, 1, 3, 2],
96 | [1, 0, 1, 1]
97 | ]
98 | const board = wrapGrid(b, width, height)
99 |
100 | expect(countCombinations(board, piece)).toBe(2.4)
101 | })
102 |
103 | test('conta corretamente a qtde de matches para peça L', () => {
104 | const width = 6
105 | const height = 6
106 |
107 | const piece_grids = PIECE_L_GRIDS(1)
108 | const piece = createPiece(piece_grids.map(g => wrapGrid(g, width, height)))
109 | const b = [
110 | [0, 0, 0, 0, 0, 0],
111 | [0, 0, 0, 0, 0, 0],
112 | [0, 0, 0, 0, 0, 0],
113 | [2, 0, 0, 0, 0, 0],
114 | [2, 0, 0, 0, 0, 3],
115 | [2, 2, 0, 0, 0, 3]
116 | ]
117 | const board = wrapGrid(b, width, height)
118 |
119 | expect(calcMaxMatches(board, piece)).toBe(1)
120 | })
121 |
122 | it('avgHeight should return the average height of the board', () => {
123 | const board: Grid = [
124 | [0, 0, 1, 0],
125 | [0, 1, 1, 1],
126 | [1, 1, 1, 1],
127 | [1, 1, 1, 1]
128 | ]
129 |
130 | const result = calcAvgHeight(board)
131 | expect(result).toBe(3) // Ajuste conforme o cálculo correto para sua grade
132 | })
133 |
134 | it('avgHeight should return 0 for an empty board', () => {
135 | const board: Grid = [
136 | [0, 0, 0, 0],
137 | [0, 0, 0, 0],
138 | [0, 0, 0, 0],
139 | [0, 0, 0, 0]
140 | ]
141 |
142 | const result = calcAvgHeight(board)
143 | expect(result).toBe(0)
144 | })
145 |
--------------------------------------------------------------------------------
/src/components/Block.tsx:
--------------------------------------------------------------------------------
1 | import { motion, type Variants } from 'framer-motion'
2 |
3 | import styles from '../styles/blocks.module.css'
4 |
5 | const mapClass = (type: number) => {
6 | switch (type) {
7 | case 1:
8 | return styles.block_a
9 | case 2:
10 | return styles.block_b
11 | case 3:
12 | return styles.block_c
13 | case 4:
14 | return styles.block_d
15 | case 5:
16 | return styles.block_T
17 | default:
18 | return styles.block_white
19 | }
20 | }
21 |
22 | const variants: Variants = {
23 | match: () => ({
24 | y: [0, 30],
25 | scale: [1.2, 0],
26 | rotateZ: [0, -45],
27 | borderRadius: [0, 100]
28 | // scaleX: [1.1, 3],
29 | })
30 | }
31 |
32 | const Block = ({
33 | type,
34 | section,
35 | anim,
36 | anim_delay
37 | }: {
38 | type: number
39 | section?: string
40 | anim?: string
41 | anim_delay?: number
42 | }) => {
43 | return (
44 |
57 | {(section === 'front' || !section) && (
58 |
59 | )}
60 | {(section === 'sides' || !section) && }
61 |
62 | )
63 | }
64 |
65 | export default Block
66 |
--------------------------------------------------------------------------------
/src/components/BoardContainer.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactElement } from 'react'
2 |
3 | import styles from '../styles/blocks.module.css'
4 |
5 | type Props = {
6 | children: ReactElement[]
7 | }
8 |
9 | const BoardContainer = ({ children }: Props) => {
10 | return {children}
11 | }
12 |
13 | export default BoardContainer
14 |
--------------------------------------------------------------------------------
/src/components/EmptyBlock.tsx:
--------------------------------------------------------------------------------
1 | import styles from '../styles/blocks.module.css'
2 |
3 | const EmptyBlock = () => {
4 | return (
5 |
6 |
7 | |
8 | )
9 | }
10 |
11 | export default EmptyBlock
12 |
--------------------------------------------------------------------------------
/src/components/GridView.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 |
3 | import BlockFactory from '../factories/BlockFactory'
4 | import styles from '../styles/blocks.module.css'
5 |
6 | type GridViewProps = {
7 | grid: number[][] | undefined
8 | section?: string
9 | }
10 |
11 | const GridView = ({ grid, section }: GridViewProps) => {
12 | return (
13 | <>
14 | {grid && (
15 |
16 |
17 | {grid.map((row, i) => (
18 |
19 | {row.map((block, j) => (
20 |
25 | ))}
26 |
27 | ))}
28 |
29 |
30 | )}
31 | >
32 | )
33 | }
34 |
35 | export default GridView
36 |
--------------------------------------------------------------------------------
/src/components/GroupPieceView.tsx:
--------------------------------------------------------------------------------
1 | import type { Block } from '../types'
2 | import PieceView from './PieceView'
3 |
4 | type GroupPieceViewProps = {
5 | pieces: Block[]
6 | section?: string
7 | }
8 |
9 | const GroupPieceView = ({ pieces, section }: GroupPieceViewProps) => {
10 | return (
11 | <>
12 | {pieces.map(
13 | (piece: Block, index: number): React.ReactElement => (
14 |
15 | )
16 | )}
17 | >
18 | )
19 | }
20 |
21 | export default GroupPieceView
22 |
--------------------------------------------------------------------------------
/src/components/Particle.tsx:
--------------------------------------------------------------------------------
1 | import { motion, type Variants } from 'framer-motion'
2 |
3 | import blockStyles from '../styles/blocks.module.css'
4 | import particleStyles from '../styles/particles.module.css'
5 |
6 | const variants: Variants = {
7 | match: () => ({
8 | y: [0, -10],
9 | scale: [0, 1.5],
10 | opacity: [0.7, 0]
11 | })
12 | }
13 |
14 | const Particle = () => {
15 | return (
16 |
21 |
24 | ★
25 |
26 |
29 | ★
30 |
31 |
34 | ★
35 |
36 |
37 | )
38 | }
39 |
40 | export default Particle
41 |
--------------------------------------------------------------------------------
/src/components/PieceView.tsx:
--------------------------------------------------------------------------------
1 | import { motion, type Variants } from 'framer-motion'
2 |
3 | import BlockFactory from '../factories/BlockFactory'
4 | import styles from '../styles/blocks.module.css'
5 | import type { Block } from '../types'
6 |
7 | type PieceViewProps = {
8 | piece: Block
9 | section?: string
10 | }
11 |
12 | const initialFrom = (anim_state: string) => {
13 | if (anim_state === 'biggerSplash') {
14 | return {
15 | scaleX: 1.05
16 | }
17 | }
18 | return {}
19 | }
20 |
21 | const variants: Variants = {
22 | follow: piece => ({
23 | x: piece.x * (95 / 3),
24 | y: piece.y * (95 / 3),
25 | scaleX: 1,
26 | scaleY: 1,
27 | transition: {
28 | type: 'easeInOut',
29 | duration: 0.3
30 | // type: 'cubic-bezier(1, 0, 1, 1)',
31 | // stiffness: 70,
32 | }
33 | }),
34 | show: () => ({
35 | scaleX: [0, 1],
36 | scaleY: [0, 1]
37 | }),
38 | biggerSplash: () => ({
39 | // scaleY: [0.5, 1],
40 | scaleX: [0.95, 1.05, 0.95, 1]
41 | // ease: 'easeIn',
42 | }),
43 | smallerSplash: () => ({
44 | scaleX: [5]
45 | }),
46 | match: () => ({
47 | // scaleY: [1, 0],
48 | // opacity: [0.3, 0.3],
49 | }),
50 | static: () => ({})
51 | }
52 | const PieceView = ({ piece, section }: PieceViewProps) => {
53 | return (
54 | <>
55 | {piece && (
56 |
67 |
68 | {piece.initial_grid[
69 | piece.rotations % piece.initial_grid.length
70 | ].map((row, i) => (
71 |
72 | {row.map((block, j) => (
73 |
80 | ))}
81 |
82 | ))}
83 |
84 |
85 | )}
86 | >
87 | )
88 | }
89 |
90 | export default PieceView
91 |
--------------------------------------------------------------------------------
/src/components/Score.tsx:
--------------------------------------------------------------------------------
1 | import styles from '../styles/score.module.css'
2 |
3 | type ScoreProps = {
4 | current: number
5 | }
6 |
7 | const Score = ({ current }: ScoreProps) => {
8 | return {current}
9 | }
10 |
11 | export default Score
12 |
--------------------------------------------------------------------------------
/src/components/ScoreContainer.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactElement } from 'react'
2 |
3 | import styles from '../styles/score.module.css'
4 |
5 | type Props = {
6 | children: ReactElement[]
7 | }
8 |
9 | const ScoreContainer = ({ children }: Props) => {
10 | return {children}
11 | }
12 |
13 | export default ScoreContainer
14 |
--------------------------------------------------------------------------------
/src/configs/index.ts:
--------------------------------------------------------------------------------
1 | export const configs = {
2 | width: 5,
3 | height: 11,
4 | playable_height: 6
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import type { Grid } from '../types'
2 |
3 | export const EMPTY_GRID = (): Grid => {
4 | return [[0]]
5 | }
6 |
7 | export const LIMIT_GRID = (
8 | width: number,
9 | height: number,
10 | playable_height: number
11 | ): number[][] => {
12 | const grid: number[][] = []
13 | for (let i = 0; i < height; i++) {
14 | grid.push([])
15 | for (let j = 0; j < width; j++) {
16 | if (i < height - playable_height) {
17 | grid[i].push(0)
18 | } else {
19 | grid[i].push(-1)
20 | }
21 | }
22 | }
23 | return grid
24 | }
25 |
26 | export const PIECE_Z_GRIDS = (type: number): Grid[] => {
27 | const o = type
28 | const _ = 0
29 | const grids: Grid[] = [
30 | [
31 | [o, o, _],
32 | [_, o, o],
33 | [_, _, _]
34 | ],
35 | [
36 | [_, _, o],
37 | [_, o, o],
38 | [_, o, _]
39 | ]
40 | ]
41 |
42 | return grids
43 | }
44 |
45 | export const PIECE_L_GRIDS = (type: number): Grid[] => {
46 | const o = type
47 | const _ = 0
48 | const grids: Grid[] = [
49 | [
50 | [o, _, _],
51 | [o, _, _],
52 | [o, o, _]
53 | ],
54 | [
55 | [_, _, o],
56 | [o, o, o],
57 | [_, _, _]
58 | ],
59 | [
60 | [o, o, _],
61 | [_, o, _],
62 | [_, o, _]
63 | ],
64 | [
65 | [o, o, o],
66 | [o, _, _],
67 | [_, _, _]
68 | ]
69 | ]
70 |
71 | return grids
72 | }
73 |
74 | export const PIECE_O_GRIDS = (type: number): Grid[] => {
75 | const o = type
76 | const grids: Grid[] = [
77 | [
78 | [o, o],
79 | [o, o]
80 | ]
81 | ]
82 |
83 | return grids
84 | }
85 |
86 | export const PIECE_I_GRIDS = (type: number): Grid[] => {
87 | const o = type
88 | const _ = 0
89 | const grids: Grid[] = [
90 | [
91 | [o, _, _, _],
92 | [o, _, _, _],
93 | [o, _, _, _],
94 | [o, _, _, _]
95 | ],
96 | [
97 | [_, _, _, _],
98 | [_, _, _, _],
99 | [o, o, o, o],
100 | [_, _, _, _]
101 | ]
102 | ]
103 |
104 | return grids
105 | }
106 |
107 | export const PIECE_T_GRIDS = (type: number): Grid[] => {
108 | const o = type
109 | const _ = 0
110 | const grids: Grid[] = [
111 | [
112 | [_, _, _],
113 | [o, o, o],
114 | [_, o, _]
115 | ],
116 | [
117 | [_, o, _],
118 | [_, o, o],
119 | [_, o, _]
120 | ],
121 | [
122 | [_, o, _],
123 | [o, o, o],
124 | [_, _, _]
125 | ],
126 | [
127 | [_, o, _],
128 | [o, o, _],
129 | [_, o, _]
130 | ]
131 | ]
132 |
133 | return grids
134 | }
135 |
--------------------------------------------------------------------------------
/src/containers/AudioContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Howl } from 'howler'
2 | import { useEffect } from 'react'
3 | import { connect, type ConnectedProps } from 'react-redux'
4 |
5 | type RootState = {
6 | audio: {
7 | name: string
8 | }
9 | }
10 |
11 | const mapStateToProps = (state: RootState): RootState => ({
12 | audio: state.audio
13 | })
14 |
15 | const connector = connect(mapStateToProps)
16 |
17 | type PropsFromRedux = ConnectedProps
18 |
19 | type AudioContainerProps = PropsFromRedux
20 |
21 | function AudioContainer({ audio }: AudioContainerProps) {
22 | const audioMap = new Map([
23 | ['side_move', 'drop_002.ogg'],
24 | ['rotation_move', 'drop_001.ogg'],
25 | ['max_down_move', ''],
26 | ['piece_join', 'click_003.ogg'],
27 | ['combination', 'maximize_006.ogg']
28 | ])
29 | useEffect(() => {
30 | const fileName = audioMap.get(audio.name)
31 |
32 | const sound = new Howl({
33 | src: `./audio/${fileName}`,
34 | format: 'ogg',
35 | volume: 0.3
36 | })
37 |
38 | // Toca o áudio quando o componente é montado
39 | sound.play()
40 | // eslint-disable-next-line react-hooks/exhaustive-deps
41 | }, [audio])
42 |
43 | return <>>
44 | }
45 |
46 | export default connector(AudioContainer)
47 |
--------------------------------------------------------------------------------
/src/containers/ContainerBoard.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { connect, type ConnectedProps } from 'react-redux'
3 |
4 | import BoardContainer from '../components/BoardContainer'
5 | import GridView from '../components/GridView'
6 | import GroupPieceView from '../components/GroupPieceView'
7 | import PieceView from '../components/PieceView'
8 | import {
9 | handleCollision,
10 | handleFloatingsGoingDown,
11 | handleMatches,
12 | handlePieceGoingDown,
13 | handleResetPiece,
14 | handleUserInput
15 | } from '../handlers'
16 | import { userController } from '../input/keyboardInput'
17 | import type { BlocksState } from '../types/block'
18 |
19 | type RootState = {
20 | blocks: BlocksState
21 | ticks: number
22 | }
23 |
24 | const mapStateToProps = (state: RootState): RootState => ({
25 | blocks: state.blocks,
26 | ticks: state.ticks
27 | })
28 |
29 | const connector = connect(mapStateToProps)
30 |
31 | type PropsFromRedux = ConnectedProps
32 |
33 | export type ContainerBoardProps = PropsFromRedux
34 |
35 | function ContainerBoard({ blocks, ticks, dispatch }: ContainerBoardProps) {
36 | useEffect(() => {
37 | try {
38 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
39 | handleMatches(blocks, ticks, dispatch)
40 | handleResetPiece(blocks, dispatch)
41 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
42 | // @ts-expect-error
43 | handleFloatingsGoingDown(blocks, ticks, dispatch)
44 | handlePieceGoingDown(blocks, ticks, dispatch)
45 | handleUserInput(
46 | userController.current_input_x,
47 | userController.current_input_y,
48 | dispatch
49 | )
50 | } catch (collision) {
51 | handleCollision(collision as Error, dispatch)
52 | }
53 | // eslint-disable-next-line react-hooks/exhaustive-deps
54 | }, [ticks])
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | {/* isso aqui mostra grid do dados ajuda a debugar */}
75 |
76 | )
77 | }
78 |
79 | export default connector(ContainerBoard)
80 |
--------------------------------------------------------------------------------
/src/containers/ContainerScore.tsx:
--------------------------------------------------------------------------------
1 | import { connect, type ConnectedProps } from 'react-redux'
2 |
3 | import ScoreContainer from '../components/ScoreContainer'
4 | import styles from '../styles/score.module.css'
5 |
6 | type RootState = {
7 | score: number
8 | }
9 |
10 | const mapStateToProps = (state: RootState): RootState => ({
11 | score: state.score
12 | })
13 |
14 | const connector = connect(mapStateToProps)
15 |
16 | type PropsFromRedux = ConnectedProps
17 |
18 | type ContainerAudioProps = PropsFromRedux
19 |
20 | function ContainerScore({ score }: ContainerAudioProps) {
21 | return (
22 |
23 |
24 |
{7500}
25 |

26 |
27 | {score}
28 |
29 | )
30 | }
31 |
32 | export default connector(ContainerScore)
33 |
--------------------------------------------------------------------------------
/src/containers/ContainerTicker.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { connect, type ConnectedProps } from 'react-redux'
3 |
4 | type RootState = {
5 | ticks: number
6 | }
7 |
8 | const mapStateToProps = (state: RootState): RootState => ({
9 | ticks: state.ticks
10 | })
11 |
12 | const connector = connect(mapStateToProps)
13 |
14 | type PropsFromRedux = ConnectedProps
15 |
16 | type ContainerAudioProps = PropsFromRedux
17 |
18 | function ContainerTicker({ ticks, dispatch }: ContainerAudioProps) {
19 | useEffect(() => {
20 | window.addEventListener('blur', () => dispatch({ type: 'ticker/pause' }))
21 | window.addEventListener('focus', () => dispatch({ type: 'ticker/resume' }))
22 | // eslint-disable-next-line react-hooks/exhaustive-deps
23 | }, [])
24 | useEffect(() => {
25 | const interval = setInterval(() => {
26 | dispatch({ type: 'ticker/increment' })
27 | }, 50)
28 | return () => {
29 | clearInterval(interval)
30 | }
31 | // eslint-disable-next-line react-hooks/exhaustive-deps
32 | }, [ticks])
33 |
34 | return <>>
35 | }
36 |
37 | export default connector(ContainerTicker)
38 |
--------------------------------------------------------------------------------
/src/controller/graph.ts:
--------------------------------------------------------------------------------
1 | import type { Grid } from '../types'
2 |
3 | export function splitDisconnectedGraphs(grid: number[][]): Grid[] {
4 | const coordinatesGroups: number[][][] = findDisconnectedGraphs(grid)
5 | return coordinatesGroups.map((coordinates: number[][]) =>
6 | extractGridValues(grid, coordinates)
7 | )
8 | }
9 |
10 | function extractGridValues(
11 | grid: number[][],
12 | coordinates: number[][]
13 | ): number[][] {
14 | const extractedGrid: number[][] = []
15 |
16 | for (let i = 0; i < grid.length; i++) {
17 | extractedGrid.push(Array(grid[i].length).fill(0))
18 | }
19 |
20 | for (const [row, col] of coordinates) {
21 | extractedGrid[row][col] = grid[row][col]
22 | }
23 |
24 | return extractedGrid
25 | }
26 |
27 | function findDisconnectedGraphs(grid: number[][]): number[][][] {
28 | const rows: number = grid.length
29 | const cols: number = grid[0].length
30 | const visited: boolean[][] = Array.from({ length: rows }, () =>
31 | Array(cols).fill(false)
32 | )
33 | const disconnectedGraphs: number[][][] = []
34 |
35 | for (let row = 0; row < rows; row++) {
36 | for (let col = 0; col < cols; col++) {
37 | if (grid[row][col] !== 0 && !visited[row][col]) {
38 | const disconnectedGraph: number[][] = []
39 |
40 | bfs(row, col, grid, visited, disconnectedGraph)
41 |
42 | disconnectedGraphs.push(disconnectedGraph)
43 | }
44 | }
45 | }
46 |
47 | return disconnectedGraphs
48 | }
49 |
50 | function bfs(
51 | startRow: number,
52 | startCol: number,
53 | grid: number[][],
54 | visited: boolean[][],
55 | disconnectedGraph: number[][]
56 | ): void {
57 | const rows: number = grid.length
58 | const cols: number = grid[0].length
59 | const queue: Array<[number, number]> = []
60 |
61 | queue.push([startRow, startCol])
62 | visited[startRow][startCol] = true
63 |
64 | while (queue.length > 0) {
65 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
66 | const [row, col] = queue.shift()!
67 | disconnectedGraph.push([row, col])
68 |
69 | // Define the four possible directions (up, down, left, right)
70 | const directions: Array<[number, number]> = [
71 | [-1, 0],
72 | [1, 0],
73 | [0, -1],
74 | [0, 1]
75 | ]
76 |
77 | for (const [dx, dy] of directions) {
78 | const newRow = row + dx
79 | const newCol = col + dy
80 |
81 | if (
82 | newRow >= 0 &&
83 | newRow < rows &&
84 | newCol >= 0 &&
85 | newCol < cols &&
86 | grid[newRow][newCol] !== 0 &&
87 | !visited[newRow][newCol]
88 | ) {
89 | queue.push([newRow, newCol])
90 | visited[newRow][newCol] = true
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/controller/index.ts:
--------------------------------------------------------------------------------
1 | import { createPiece, emptyPiece } from '../factories/PieceFactory'
2 | import type { Block, Grid } from '../types'
3 | import { splitDisconnectedGraphs } from './graph'
4 |
5 | export const getCurrentGrid = (block: Block): Grid | undefined => {
6 | const b = transform(
7 | block.initial_grid[block.rotations % block.initial_grid.length],
8 | block.x,
9 | block.y
10 | )
11 | return b
12 | }
13 |
14 | export const wrapGrid = (
15 | original_grid: Grid,
16 | new_width: number,
17 | new_height: number
18 | ): Grid => {
19 | if (
20 | original_grid.length > new_height ||
21 | original_grid[0]?.length > new_width
22 | ) {
23 | throw new Error('Wrapping a grid into a smaller is not allowed')
24 | }
25 |
26 | const padding = Math.floor((new_width - original_grid[0]?.length) / 2)
27 | const grid: number[][] = []
28 | for (let i = 0; i < new_height; i++) {
29 | grid.push([])
30 | for (let j = 0; j < new_width; j++) {
31 | grid[i]?.push(get(original_grid, i, j - padding))
32 | }
33 | }
34 |
35 | return grid
36 | }
37 |
38 | const get = (board: Grid, x: number, y: number): number => {
39 | if (board[x]?.[y]) {
40 | return board[x][y]
41 | } else {
42 | return 0
43 | }
44 | }
45 |
46 | export const transform = (
47 | board: number[][],
48 | x: number,
49 | y: number
50 | ): Grid | undefined => {
51 | const b: number[][] = []
52 |
53 | for (let i = 0; i < board.length; i++) {
54 | b.push([])
55 | for (let j = 0; j < board[i].length; j++) {
56 | b[i].push(get(board, i - y, j - x))
57 | }
58 | }
59 |
60 | if (
61 | b.reduce(
62 | (acc, cur) => acc + cur.reduce((acc1, cur1) => acc1 + cur1, 0),
63 | 0
64 | ) ===
65 | board.reduce(
66 | (acc, cur) => acc + cur.reduce((acc1, cur1) => acc1 + cur1, 0),
67 | 0
68 | )
69 | ) {
70 | return b
71 | } else {
72 | return undefined
73 | }
74 | }
75 |
76 | export const wrap = (board: number[][]): Grid => {
77 | const wrapped: number[][] = []
78 | const width = board.length
79 | const height = board[0] ? board[0].length : 0
80 |
81 | for (let i = 0; i < width + 2; i++) {
82 | wrapped.push([])
83 | for (let j = 0; j < height + 2; j++) {
84 | wrapped[i].push(0)
85 | }
86 | }
87 |
88 | for (let i = 0; i < board.length; i++) {
89 | for (let j = 0; j < board[i].length; j++) {
90 | wrapped[i + 1][j + 1] = board[i][j]
91 | }
92 | }
93 |
94 | return wrapped
95 | }
96 |
97 | export const isColliding = (
98 | boardA: number[][],
99 | boardB: number[][]
100 | ): boolean | Error => {
101 | if (!hasSameDimensions(boardA, boardB)) {
102 | throw new Error('Comparing boards with different sizes is not allowed.')
103 | }
104 |
105 | for (let i = 0; i < boardA.length; i++) {
106 | for (let j = 0; j < boardA[i].length; j++) {
107 | if (boardA[i][j] > 0 && boardB[i][j] > 0) {
108 | return true
109 | }
110 | }
111 | }
112 |
113 | return false
114 | }
115 |
116 | export const join = (boardA: number[][], boardB: number[][]): Grid => {
117 | if (!hasSameDimensions(boardA, boardB)) {
118 | throw new Error('Joining boards with different sizes is not allowed.')
119 | }
120 | if (!isColliding(boardA, boardB)) {
121 | // throw new Error('Joining colliding boards is not allowed.');
122 | }
123 |
124 | const join = boardA.map(row => [...row]) // cria uma cópia de boardA
125 |
126 | for (let i = 0; i < boardB.length; i++) {
127 | for (let j = 0; j < boardB[i].length; j++) {
128 | if (boardB[i][j] > 0) {
129 | join[i][j] = boardB[i][j]
130 | }
131 | }
132 | }
133 |
134 | return join
135 | }
136 |
137 | const hasSameDimensions = (boardA: number[][], boardB: number[][]): boolean => {
138 | return (
139 | boardA.length === boardB.length && boardA[0]?.length === boardB[0]?.length
140 | )
141 | }
142 |
143 | export type BoardState = {
144 | remaining: Grid
145 | floating: Block[]
146 | matching: Block[]
147 | }
148 |
149 | export const splitDisconnected = (grid: Grid): Block[] => {
150 | const splitted = splitDisconnectedGraphs(grid)
151 | const blocks: Block[] = splitted.map(i => {
152 | return { ...createPiece([i]), anim_state: 'follow' }
153 | })
154 | return blocks
155 | }
156 |
157 | export const removeMatches = (board: number[][]): BoardState => {
158 | const b: Grid = emptyPiece()
159 | const m: Grid = emptyPiece()
160 |
161 | for (let i = board.length - 1; i >= 0; i--) {
162 | if (board[i].every(cell => cell !== 0)) {
163 | m[i] = board[i].map(() => 10)
164 | } else {
165 | b[i] = board[i]
166 | }
167 | }
168 |
169 | const splitted = splitDisconnected(b)
170 | const grounded = splitted
171 | .filter(i => !getCurrentGrid({ ...i, y: i.y + 1 }))
172 | .reduce(
173 | (cur, acc) => join(cur, getCurrentGrid(acc) || emptyPiece()),
174 | emptyPiece()
175 | )
176 | const floating = splitted.filter(i => getCurrentGrid({ ...i, y: i.y + 1 }))
177 |
178 | // const disc = splitDisconnected(b);
179 |
180 | // for (i = i - 1; i >= 0; i--) {
181 | // if (board[i].some((cell) => cell !== 0)) {
182 | // break;
183 | // }
184 | // }
185 |
186 | // for (; i >= 0; i--) {
187 | // f[i] = board[i];
188 | // }
189 |
190 | const aux = {
191 | remaining: grounded,
192 | floating,
193 | matching: splitDisconnected(m)
194 | }
195 |
196 | return aux
197 | }
198 |
199 | export const removeMatchesMoveDownBlocks = (board: number[][]): Grid => {
200 | const numRows = board.length
201 | const numCols = board[0].length
202 |
203 | const rowsToRemove: number[] = []
204 |
205 | for (let row = 0; row < numRows; row++) {
206 | if (board[row].every(cell => cell > 0)) {
207 | rowsToRemove.push(row)
208 | }
209 | }
210 |
211 | if (rowsToRemove.length === 0) {
212 | return board // no rows to remove, return the original board
213 | }
214 |
215 | const newBoard: number[][] = []
216 |
217 | let newRow = numRows - 1 // start at the bottom of the board
218 | for (let row = numRows - 1; row >= 0; row--) {
219 | if (!rowsToRemove.includes(row)) {
220 | newBoard[newRow] = [...board[row]] // copy the row to its new position
221 | newRow--
222 | }
223 | }
224 |
225 | // fill the top rows with zeros
226 | for (let row = 0; row < rowsToRemove.length; row++) {
227 | newBoard[row] = new Array(numCols).fill(0)
228 | }
229 |
230 | return newBoard
231 | }
232 |
233 | export const hasAnyCombinations = (board: Grid): boolean => {
234 | return board.some(row => row.every(i => i > 0))
235 | }
236 |
237 | export const countExactCombinations = (board: Grid): number => {
238 | return board.filter(row => row.every(i => i > 0)).length
239 | }
240 |
241 | export const countCombinations = (board: Grid, piece: Block) => {
242 | const piece_grid = getCurrentGrid(piece)
243 | if (!piece_grid) {
244 | return 0
245 | }
246 | const joinned = join(board, piece_grid)
247 | const full_combinations = joinned.filter(row => row.every(i => i > 0)).length
248 | const partial_combinations = joinned.filter(
249 | row => row.filter(j => j === 0).length === 1
250 | ).length
251 | return full_combinations + 0.7 * partial_combinations
252 | }
253 |
254 | export const calcAvgHeight = (board: Grid): number => {
255 | let totalHeight = 0
256 | const numCols = board[0].length
257 |
258 | for (let col = 0; col < numCols; col++) {
259 | let colHeight = 0
260 | for (let row = 0; row < board.length; row++) {
261 | if (board[row][col] > 0) {
262 | colHeight = board.length - row
263 | break
264 | }
265 | }
266 | totalHeight += colHeight
267 | }
268 |
269 | return totalHeight / numCols
270 | }
271 |
272 | export const clear = (board: Grid): Grid => {
273 | const b = board
274 | for (let i = 0; i < board.length; i++) {
275 | for (let j = 0; j < board[i].length; j++) {
276 | b[i][j] = 0
277 | }
278 | }
279 | return b
280 | }
281 |
282 | export const isEmptyPiece = (piece: Block) => {
283 | const grid = getCurrentGrid(piece)
284 | if (grid) {
285 | return grid.every(row => row.every(cell => cell === 0))
286 | } else {
287 | return false
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/src/factories/BlockFactory.tsx:
--------------------------------------------------------------------------------
1 | import Block from '../components/Block'
2 | import EmptyBlock from '../components/EmptyBlock'
3 | import Particle from '../components/Particle'
4 | import styles from '../styles/blocks.module.css'
5 | interface BlockFactoryInterface {
6 | type: number
7 | section?: string
8 | anim?: string
9 | anim_delay?: number
10 | }
11 |
12 | const BlockFactory = (props: BlockFactoryInterface) => {
13 | switch (props.type) {
14 | case -1:
15 | return
16 | case 0:
17 | return |
18 | case 100:
19 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
20 | // @ts-expect-error
21 | return
22 | default:
23 | return (
24 |
30 | )
31 | }
32 | }
33 |
34 | export default BlockFactory
35 |
--------------------------------------------------------------------------------
/src/factories/NextPieceCalculator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | calcAvgHeight,
3 | countCombinations,
4 | getCurrentGrid,
5 | isColliding
6 | } from '../controller'
7 | import type { Block, Grid } from '../types'
8 |
9 | export type fitnesses = {
10 | id: number
11 | score: number
12 | }
13 |
14 | export const calcPiecesFitness = (
15 | board: Grid,
16 | pieces: Block[],
17 | targetHeight: number
18 | ): fitnesses[] => {
19 | const avgHeight = calcAvgHeight(board) // Supondo que calcAvgHeight retorna a altura média atual do tabuleiro
20 | const fitnessScores: fitnesses[] = Array.from(
21 | { length: pieces.length },
22 | (_, i) => ({
23 | id: i,
24 | score: 10
25 | })
26 | )
27 | // para cada peça
28 | for (let i = 0; i < pieces.length; i++) {
29 | const maxMatches = calcMaxMatches(board, pieces[i])
30 | const resultingHeight = avgHeight - maxMatches
31 | const score = Math.abs(targetHeight - resultingHeight)
32 | fitnessScores[i].score = Math.min(fitnessScores[i].score, score)
33 | }
34 |
35 | return fitnessScores
36 | }
37 |
38 | export const calcMaxMatches = (board: Grid, piece: Block): number => {
39 | let max = 0
40 | // para cada rotação
41 | for (let j = 0; j < piece.initial_grid.length; j++) {
42 | const rotatedPiece = { ...piece, rotations: j }
43 | const possibleXPositions = getPossiveisX(rotatedPiece)
44 | for (let k = 0; k < possibleXPositions.length; k++) {
45 | const pieceAtX = { ...rotatedPiece, x: possibleXPositions[k] }
46 | const maxYPosition = getMaxY(pieceAtX, board)
47 | if (maxYPosition) {
48 | const pieceAtMaxY = { ...pieceAtX, y: maxYPosition }
49 | const matchCount = countCombinations(board, pieceAtMaxY)
50 | max = Math.max(max, matchCount)
51 | }
52 | }
53 | }
54 |
55 | return max
56 | }
57 |
58 | export function getMaxY(piece: Block, board: Grid): number | undefined {
59 | let p_grid, colide
60 | do {
61 | piece = { ...piece, y: piece.y + 1 }
62 | p_grid = getCurrentGrid(piece)
63 | colide = p_grid ? isColliding(p_grid, board) : undefined
64 | } while (p_grid && !colide)
65 |
66 | const result = piece.y - 1
67 |
68 | if (result === -1) {
69 | return undefined
70 | }
71 |
72 | return result
73 | }
74 |
75 | export function getPossiveisX(piece: Block): number[] {
76 | const xs = []
77 | const grid = getCurrentGrid(piece)
78 | if (!grid) {
79 | return []
80 | }
81 | const width = Array.from(grid[0]).length
82 | for (let x = -width; x <= width; x++) {
83 | if (getCurrentGrid({ ...piece, x })) {
84 | xs.push(x)
85 | }
86 | }
87 |
88 | return xs
89 | }
90 |
--------------------------------------------------------------------------------
/src/factories/ParticlesData.ts:
--------------------------------------------------------------------------------
1 | import { configs } from '../configs'
2 | import { wrapGrid } from '../controller'
3 | import { createPiece } from './PieceFactory'
4 |
5 | export const createParticles = () => {
6 | const grid = wrapGrid([[0]], configs.width, configs.height)
7 | return createPiece([grid])
8 | }
9 |
--------------------------------------------------------------------------------
/src/factories/PieceFactory.ts:
--------------------------------------------------------------------------------
1 | import { configs } from '../configs'
2 | import {
3 | EMPTY_GRID,
4 | LIMIT_GRID,
5 | PIECE_I_GRIDS,
6 | PIECE_L_GRIDS,
7 | PIECE_O_GRIDS,
8 | PIECE_T_GRIDS,
9 | PIECE_Z_GRIDS
10 | } from '../constants'
11 | import { getCurrentGrid, join, wrapGrid } from '../controller'
12 | import type { Block as Piece, Grid } from '../types'
13 | import type { Block, BlocksState } from '../types/block'
14 | import { calcPiecesFitness } from './NextPieceCalculator'
15 |
16 | const allPieces = (): Block[] => {
17 | const options = [
18 | PIECE_Z_GRIDS(1),
19 | PIECE_L_GRIDS(2),
20 | PIECE_O_GRIDS(3),
21 | PIECE_I_GRIDS(4),
22 | PIECE_T_GRIDS(5)
23 | ]
24 | const pieces = options.map(grids =>
25 | createPiece(grids.map(g => wrapGrid(g, configs.width, configs.height)))
26 | )
27 | return pieces
28 | }
29 |
30 | const calcGridPosFloatingJoin = (boardState: BlocksState): Grid => {
31 | const floatingGrids = boardState.floating.map(i =>
32 | getCurrentGrid({ ...i, y: i.y + 1 })
33 | )
34 | const floatingJoinned = floatingGrids
35 | .filter(Boolean)
36 | .reduce((acc, curr) => join(acc, curr), boardState.board)
37 |
38 | return floatingJoinned || boardState.board
39 | }
40 |
41 | export const randomPiece = () => {
42 | const pieces = allPieces()
43 | const index = Math.floor(Math.random() * pieces.length)
44 | return pieces[index]
45 | }
46 |
47 | const ocorencies: number[] = [0, 0, 0, 0, 0]
48 | let nextIndex: number
49 | export const nextPiece = (boardState: BlocksState) => {
50 | const pieces = allPieces()
51 | const floatingJoinned = calcGridPosFloatingJoin(boardState)
52 | const ocorenciesNormalized = normalizeOcorencies(ocorencies)
53 | const avgHeight = calcAverageHeight(floatingJoinned)
54 |
55 | if (avgHeight === 0) {
56 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
57 | nextIndex = ocorenciesNormalized.sort()[0]
58 | } else if (ocorenciesNormalized.some(i => i < 0.8 / ocorencies.length)) {
59 | nextIndex = ocorenciesNormalized.findIndex(i => i < 0.8 / ocorencies.length)
60 | } else {
61 | const withScores = calcPiecesFitness(floatingJoinned, pieces, 0)
62 | let index = withScores.sort((a, b) => a.score - b.score)[0].id
63 | if (index === nextIndex) {
64 | index = withScores.sort((a, b) => a.score - b.score)[1].id
65 | }
66 | nextIndex = index
67 | }
68 |
69 | ocorencies[nextIndex]++
70 | return pieces[nextIndex]
71 | }
72 |
73 | export const erasedPiece = () => {
74 | const grid: Grid = EMPTY_GRID()
75 | const wrapped = wrapGrid(grid, configs.width, configs.height)
76 | return createPiece([wrapped])
77 | }
78 |
79 | export const emptyPiece = () => {
80 | const grid: Grid = EMPTY_GRID()
81 | return wrapGrid(grid, configs.width, configs.height)
82 | }
83 |
84 | export const limitsPiece = () => {
85 | const grid: Grid = LIMIT_GRID(
86 | configs.width,
87 | configs.height,
88 | configs.playable_height
89 | )
90 | return wrapGrid(grid, configs.width, configs.height)
91 | }
92 |
93 | export const createPiece = (initial_grid: Grid[]): Piece => {
94 | const piece = {
95 | key: 0,
96 | initial_grid,
97 | x: 0,
98 | y: 0,
99 | rotations: 0,
100 | anim_state: '-'
101 | }
102 | return piece
103 | }
104 | const calcAverageHeight = (grid: Grid): number => {
105 | let totalHeight = 0
106 | const numberOfColumns = grid.length
107 |
108 | if (numberOfColumns === 0) {
109 | return 0
110 | }
111 |
112 | for (let col = 0; col < numberOfColumns; col++) {
113 | let height = 0
114 | for (let row = 0; row < grid[col].length; row++) {
115 | if (grid[col][row] !== 0) {
116 | height++
117 | }
118 | }
119 | totalHeight += height
120 | }
121 |
122 | return totalHeight / numberOfColumns
123 | }
124 | const normalizeOcorencies = (ocorencies: number[]): number[] => {
125 | const ocorenciesTotal = ocorencies.reduce((acc, val) => acc + val, 0)
126 | const ocorenciesNormalized = ocorencies.map(x => x / ocorenciesTotal)
127 | return ocorenciesNormalized
128 | }
129 |
--------------------------------------------------------------------------------
/src/handlers/index.ts:
--------------------------------------------------------------------------------
1 | import type { Dispatch } from 'react'
2 |
3 | import { countExactCombinations, isEmptyPiece } from '../controller'
4 |
5 | // TODO considerar o uso de UseCallbacks
6 |
7 | const isTimeToMoveDown = (ticks: number) => {
8 | return ticks % 10 === 0
9 | }
10 |
11 | export const handleMatches = async (
12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 | blocks: any,
14 | _ticks: number,
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | dispatch: Dispatch
17 | ): Promise => {
18 | // eslint-disable-next-line @typescript-eslint/naming-convention
19 | const matches_count = countExactCombinations(blocks.board)
20 | if (matches_count > 0) {
21 | dispatch({ type: 'board/combinations' })
22 | dispatch({ type: 'score/increment', payload: matches_count })
23 | dispatch({ type: 'audio/play', payload: 'combination' })
24 | }
25 | }
26 |
27 | export const handleFloatingsGoingDown = (
28 | _blocks: never,
29 | ticks: number,
30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
31 | dispatch: Dispatch
32 | ) => {
33 | if (ticks % 2 === 0) {
34 | dispatch({ type: 'floating/fall' })
35 | // anim.start('follow');
36 | }
37 | }
38 |
39 | export const handlePieceGoingDown = (
40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
41 | blocks: any,
42 | ticks: number,
43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
44 | dispatch: Dispatch
45 | ) => {
46 | if (isTimeToMoveDown(ticks) && !isEmptyPiece(blocks.piece)) {
47 | dispatch({ type: 'piece/move-down' })
48 | // anim.start('follow');
49 | }
50 | }
51 |
52 | export const handleUserInput = (
53 | inputx: number | undefined,
54 | inputy: number | undefined,
55 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
56 | dispatch: Dispatch
57 | ) => {
58 | if (inputx && inputx > 0) {
59 | dispatch({ type: 'piece/move-right' })
60 | dispatch({ type: 'audio/play', payload: 'side_move' })
61 | // anim.start('follow');
62 | }
63 | if (inputx && inputx < 0) {
64 | dispatch({ type: 'piece/move-left' })
65 | dispatch({ type: 'audio/play', payload: 'side_move' })
66 | // anim.start('follow');
67 | }
68 | if (inputy && inputy > 0) {
69 | dispatch({ type: 'piece/rotate' })
70 | dispatch({ type: 'audio/play', payload: 'rotation_move' })
71 | }
72 | if (inputy && inputy < 0) {
73 | dispatch({ type: 'piece/move-down-max' })
74 | dispatch({ type: 'audio/play', payload: 'max_down_move' })
75 | dispatch({ type: 'piece/move-down' })
76 | }
77 | }
78 |
79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
80 | export const handleResetPiece = (blocks: any, dispatch: Dispatch) => {
81 | // eslint-disable-next-line eqeqeq
82 | if (isEmptyPiece(blocks.piece) && blocks.floating.length == 0) {
83 | dispatch({ type: 'piece/reset' })
84 | // anim.reset();
85 | // anim.start('show');
86 | }
87 | }
88 |
89 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
90 | export const handleCollision = (collision: Error, dispatch: Dispatch) => {
91 | switch (collision.message) {
92 | case 'piece-down-move-collision':
93 | dispatch({ type: 'audio/play', payload: 'piece_join' })
94 | dispatch({ type: 'piece/join' })
95 | break
96 | case 'floating-fall-collision':
97 | dispatch({ type: 'floating/join', payload: collision.name })
98 | dispatch({ type: 'audio/play', payload: 'piece_join' })
99 | break
100 | case 'piece-side-move-collision':
101 | case 'piece-rotation-move-collision':
102 | // add some feedback
103 | break
104 | case 'board-collides-with-limits':
105 | dispatch({ type: 'blocks/reset' })
106 | dispatch({ type: 'score/reset' })
107 | break
108 | default:
109 | throw collision
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
--------------------------------------------------------------------------------
/src/input/keyboardInput.ts:
--------------------------------------------------------------------------------
1 | import { GameInputs } from 'game-inputs'
2 | import Hammer from 'hammerjs'
3 |
4 | export const userController = {
5 | _current_input_y: 0 as number | undefined,
6 | _current_input_x: 0 as number | undefined,
7 | get current_input_x(): number | undefined {
8 | if (this._current_input_x) {
9 | const aux = Number(this._current_input_x)
10 | this._current_input_x = undefined
11 | return aux
12 | }
13 | },
14 | set current_input_x(value: number | undefined) {
15 | this._current_input_x = value
16 | },
17 | get current_input_y(): number {
18 | const aux = Number(this._current_input_y)
19 | this._current_input_y = undefined
20 | return aux
21 | },
22 | set current_input_y(value: number) {
23 | this._current_input_y = value
24 | }
25 | }
26 |
27 | const dom = document.documentElement
28 | const inputs = new GameInputs(dom, {
29 | preventDefaults: true,
30 | allowContextMenu: false,
31 | stopPropagation: true,
32 | disabled: false
33 | })
34 | inputs.bind('move-left', 'ArrowLeft')
35 | inputs.bind('move-right', 'ArrowRight')
36 | inputs.bind('rotate', 'ArrowUp')
37 | inputs.bind('move-down', 'ArrowDown')
38 | inputs.bind('move-left', 'KeyA')
39 | inputs.bind('move-right', 'KeyD')
40 | inputs.bind('rotate', 'KeyW')
41 | inputs.bind('move-down', 'KeyS')
42 |
43 | inputs.down.on('move-left', () => {
44 | userController.current_input_x = -1
45 | })
46 |
47 | inputs.down.on('move-right', () => {
48 | userController.current_input_x = 1
49 | })
50 |
51 | inputs.down.on('rotate', () => {
52 | userController.current_input_y = 1
53 | })
54 |
55 | inputs.down.on('move-down', () => {
56 | userController.current_input_y = -1
57 | })
58 |
59 | const hammer = new Hammer(dom)
60 | hammer.get('swipe').set({ direction: Hammer.DIRECTION_ALL })
61 | hammer.get('swipe').set({ threshold: 0.3 })
62 | hammer.on('swipeleft', function () {
63 | userController.current_input_x = -1
64 | })
65 |
66 | hammer.on('swiperight', function () {
67 | userController.current_input_x = 1
68 | })
69 |
70 | hammer.on('swipeup tap', function () {
71 | userController.current_input_y = 1
72 | })
73 |
74 | hammer.on('swipedown', function () {
75 | userController.current_input_y = -1
76 | })
77 |
78 | // Add more event listeners for other gestures as needed
79 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import './styles/reset.css'
2 |
3 | import { StrictMode } from 'react'
4 | import { createRoot } from 'react-dom/client'
5 | import { Provider } from 'react-redux'
6 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
7 |
8 | import App from './App'
9 | import Menu from './screens/Menu'
10 | import store from './store'
11 |
12 | const container = document.getElementById('root')
13 | const root = createRoot(container)
14 |
15 | root.render(
16 |
17 |
18 | {/* */}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 |
--------------------------------------------------------------------------------
/src/screens/Menu.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Monomaniac One';
3 | src: url('/fonts/MonomaniacOne-Regular.woff2') format('woff2');
4 | }
5 |
6 | @font-face {
7 | font-family: 'Lato Italic';
8 | src: url('/fonts/Lato-Italic.woff2') format('woff2');
9 | }
10 |
11 | @font-face {
12 | font-family: 'Lato Regular';
13 | src: url('/fonts/Lato-Regular.woff2') format('woff2');
14 | }
15 |
16 | .vertical-container {
17 | display: flex;
18 | align-items: center;
19 | flex-direction: column;
20 | }
21 | .vertical-container * {
22 | margin: 10%;
23 | }
24 |
25 | .titulo-container {
26 | text-align: center; /* Centraliza o conteúdo horizontalmente */
27 | }
28 |
29 | .titulo-container * {
30 | margin: 0px;
31 | }
32 |
33 | .main-title {
34 | color: #fff;
35 | font-family: 'Monomaniac One', monospace, Arial, sans-serif;
36 | font-size: 96px;
37 | font-style: normal;
38 | font-weight: 400;
39 | line-height: normal;
40 | }
41 |
42 | .subtitle {
43 | color: #fff;
44 | font-family: 'Lato Italic', monospace;
45 | font-size: 28px;
46 | font-style: italic;
47 | font-weight: 400;
48 | line-height: normal;
49 | }
50 |
51 | .tool-icon {
52 | width: inherit; /* Ajuste o tamanho do ícone conforme necessário */
53 | vertical-align: middle; /* Alinha verticalmente o ícone com o texto */
54 | transition: transform 0.5s ease; /* Adiciona uma transição suave à transformação */
55 | }
56 |
57 | .rotate {
58 | animation: rotate 10s linear infinite; /* Aplica a animação 'rotate' ao ícone */
59 | }
60 |
61 | @keyframes rotate {
62 | from {
63 | transform: rotate(0deg); /* Rotação inicial */
64 | }
65 | to {
66 | transform: rotate(360deg); /* Rotação completa */
67 | }
68 | }
69 |
70 | .rounded-button {
71 | /* background-color: #74A0EF; */
72 | border: none;
73 | border-radius: 15px;
74 | width: 20vh;
75 | height: 20vh;
76 | cursor: pointer;
77 | transition: opacity 0.3s ease;
78 | }
79 |
80 | .rounded-button:hover {
81 | opacity: 0.9;
82 | }
83 |
84 | .rounded-button > img {
85 | width: 100%;
86 | }
87 | .footer-container {
88 | /* background-color: #fff; */
89 | /* fill: #74A0EF; */
90 | padding: 1rem;
91 | text-align: right;
92 | position: fixed;
93 | bottom: 0;
94 | right: 20px;
95 | width: 100%;
96 | }
97 |
98 | .footer-link {
99 | margin: 0 15px;
100 | }
101 |
102 | .footer-link img {
103 | width: 4vh;
104 | height: 4vh;
105 | }
106 |
--------------------------------------------------------------------------------
/src/screens/Menu.tsx:
--------------------------------------------------------------------------------
1 | import './Menu.css'
2 |
3 | import { Link } from 'react-router-dom' // Importe o hook useHistory
4 |
5 | function Menu() {
6 | return (
7 | <>
8 |
9 |
10 |
Tetris
11 |
12 | Feito com React{' '}
13 |
18 |
19 |
20 | {/*
*/}
25 |
26 |
44 | >
45 | )
46 | }
47 | export default Menu
48 |
--------------------------------------------------------------------------------
/src/store/actions/audio.ts:
--------------------------------------------------------------------------------
1 | type AudioPlay = {
2 | type: 'audio/play'
3 | payload: string // nome/id da faixa
4 | }
5 |
6 | type AudioPause = {
7 | type: 'audio/pause'
8 | payload: string
9 | }
10 |
11 | export type AudioAction = AudioPlay | AudioPause
12 |
--------------------------------------------------------------------------------
/src/store/actions/blocks.ts:
--------------------------------------------------------------------------------
1 | type PieceMoveDownAction = {
2 | type: 'piece/move-down'
3 | }
4 |
5 | type PieceMoveDownMaxAction = {
6 | type: 'piece/move-down-max'
7 | }
8 |
9 | type PieceMoveRightAction = {
10 | type: 'piece/move-right'
11 | }
12 |
13 | type PieceMoveLeftAction = {
14 | type: 'piece/move-left'
15 | }
16 |
17 | type PieceRotateAction = {
18 | type: 'piece/rotate'
19 | }
20 |
21 | type PieceJoinAction = {
22 | type: 'piece/join'
23 | }
24 |
25 | type FloatingJoinAction = {
26 | type: 'floating/join'
27 | payload: number // index
28 | }
29 |
30 | type PieceResetAction = {
31 | type: 'piece/reset'
32 | }
33 |
34 | type BoardCombinationsAction = {
35 | type: 'board/combinations'
36 | }
37 |
38 | type BlocksResetAction = {
39 | type: 'blocks/reset'
40 | }
41 |
42 | type FloatingFallAction = {
43 | type: 'floating/fall'
44 | }
45 |
46 | export type BlocksAction =
47 | | PieceMoveDownAction
48 | | PieceMoveDownMaxAction
49 | | PieceMoveRightAction
50 | | PieceMoveLeftAction
51 | | PieceRotateAction
52 | | PieceJoinAction
53 | | FloatingJoinAction
54 | | PieceResetAction
55 | | BoardCombinationsAction
56 | | BlocksResetAction
57 | | FloatingFallAction
58 |
--------------------------------------------------------------------------------
/src/store/actions/score.ts:
--------------------------------------------------------------------------------
1 | type ScoreIncrement = {
2 | type: 'score/increment'
3 | payload: number
4 | }
5 |
6 | type ScoreReset = {
7 | type: 'score/reset'
8 | }
9 |
10 | export type ScoreAction = ScoreIncrement | ScoreReset
11 |
--------------------------------------------------------------------------------
/src/store/actions/ticks.ts:
--------------------------------------------------------------------------------
1 | // Definição dos tipos de ação
2 | type TickerIncrement = {
3 | type: 'ticker/increment'
4 | }
5 |
6 | type TickerPause = {
7 | type: 'ticker/pause'
8 | }
9 |
10 | type TickerResume = {
11 | type: 'ticker/resume'
12 | }
13 |
14 | // Tipo unificado para todas as ações relacionadas a "ticks"
15 | export type TicksAction = TickerIncrement | TickerPause | TickerResume
16 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 |
3 | import rootReducer from './reducers'
4 |
5 | const store = configureStore({ reducer: rootReducer })
6 |
7 | export default store
8 |
--------------------------------------------------------------------------------
/src/store/reducers/audio.ts:
--------------------------------------------------------------------------------
1 | import type { AudioAction } from '../actions/audio'
2 |
3 | type AudioState = {
4 | name: string
5 | isPlaying: boolean
6 | }
7 |
8 | const INITIAL_AUDIO_STATE: AudioState = {
9 | name: '',
10 | isPlaying: false
11 | }
12 |
13 | export default function audioReducer(
14 | state: AudioState = INITIAL_AUDIO_STATE,
15 | action: AudioAction
16 | ): AudioState {
17 | switch (action.type) {
18 | case 'audio/play':
19 | return { name: action.payload, isPlaying: true }
20 | case 'audio/pause':
21 | return { ...state, isPlaying: false }
22 | default:
23 | return state
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/store/reducers/blocks.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentGrid, join, removeMatches } from '../../controller'
2 | import { createParticles } from '../../factories/ParticlesData'
3 | import {
4 | createPiece,
5 | emptyPiece,
6 | erasedPiece,
7 | limitsPiece,
8 | nextPiece,
9 | randomPiece
10 | } from '../../factories/PieceFactory'
11 | import type { Grid } from '../../types'
12 | import type { Block, BlocksState } from '../../types/block'
13 | import type { BlocksAction } from '../actions/blocks'
14 | import {
15 | testDownCollision,
16 | testFloatingFallCollision,
17 | testJoinCollision,
18 | testRotationCollision,
19 | testSideCollision
20 | } from './collision'
21 |
22 | const INITIAL_STATE: BlocksState = {
23 | piece: randomPiece(),
24 | board: emptyPiece(),
25 | limits: limitsPiece(),
26 | joinning: erasedPiece(),
27 | floating: [],
28 | matching: [],
29 | particles: createParticles()
30 | }
31 | const keys = { matching: 0, joinning: 0, particles: 0 }
32 |
33 | export default function blocks(
34 | state: BlocksState = INITIAL_STATE,
35 | action: BlocksAction
36 | ): BlocksState {
37 | let distance = 1
38 | let floatingCopy: Block[], boardCopy, pieceCopy, matchingCopy: Block[]
39 | let matchingRows
40 | // eslint-disable-next-line @typescript-eslint/naming-convention
41 | let grid_aux: Grid | undefined
42 | switch (action.type) {
43 | case 'piece/move-down':
44 | if (state.floating.length > 0) {
45 | return state
46 | }
47 | // grid_aux = getCurrentGrid(state.joinning);
48 | testDownCollision(state.piece, state.board)
49 | return {
50 | ...state,
51 | piece: { ...state.piece, y: state.piece.y + 1, anim_state: 'follow' }
52 | }
53 | case 'piece/move-down-max':
54 | if (state.floating.length > 0) {
55 | return state
56 | }
57 | distance = 0
58 | // eslint-disable-next-line no-constant-condition
59 | for (let i = 0; i < 20; i++) {
60 | try {
61 | testDownCollision(
62 | { ...state.piece, y: state.piece.y + distance },
63 | state.board
64 | )
65 | distance++
66 | } catch (error) {
67 | break
68 | }
69 | }
70 | return {
71 | ...state,
72 | piece: {
73 | ...state.piece,
74 | y: state.piece.y + distance,
75 | anim_state: 'follow'
76 | }
77 | }
78 | case 'piece/move-right':
79 | testSideCollision(state.piece, state.board, 1)
80 | return {
81 | ...state,
82 | piece: { ...state.piece, x: state.piece.x + 1, anim_state: 'follow' }
83 | }
84 | case 'piece/move-left':
85 | testSideCollision(state.piece, state.board, -1)
86 | return {
87 | ...state,
88 | piece: { ...state.piece, x: state.piece.x - 1 }
89 | }
90 | case 'piece/rotate':
91 | pieceCopy = testRotationCollision(state.piece, state.board)
92 | return {
93 | ...state,
94 | piece: pieceCopy
95 | }
96 | case 'piece/join':
97 | grid_aux = getCurrentGrid(state.piece)
98 | if (grid_aux) {
99 | pieceCopy = {
100 | ...createPiece([grid_aux]),
101 | anim_state: 'biggerSplash',
102 | key: keys.joinning++
103 | } satisfies Block
104 | return {
105 | ...state,
106 | joinning: pieceCopy,
107 | board: join(grid_aux, state.board),
108 | piece: erasedPiece()
109 | }
110 | }
111 | return state
112 | case 'floating/join':
113 | floatingCopy = state.floating.slice()
114 | floatingCopy.splice(action.payload, 1)
115 | grid_aux = getCurrentGrid(state.floating[action.payload])
116 | if (grid_aux) {
117 | pieceCopy = {
118 | ...createPiece([grid_aux]),
119 | anim_state: 'biggerSplash',
120 | key: keys.joinning++
121 | } satisfies Block
122 | return {
123 | ...state,
124 | joinning: pieceCopy,
125 | board: join(grid_aux, state.board),
126 | floating: floatingCopy
127 | }
128 | }
129 | return state
130 | case 'piece/reset':
131 | testJoinCollision(state.board, state.limits)
132 | return {
133 | ...state,
134 | piece: {
135 | ...nextPiece(state)
136 | }
137 | }
138 | case 'board/combinations':
139 | // TODO: bug cascata buga
140 | // return state;
141 | // avisar cada bloco q ele sera eliminado para ativar a animação
142 | ;({
143 | remaining: boardCopy,
144 | floating: floatingCopy,
145 | matching: matchingCopy
146 | } = removeMatches(state.board))
147 | matchingRows = matchingCopy.map(i => ({
148 | ...i,
149 | anim_state: 'match',
150 | key: keys.matching++
151 | }))
152 |
153 | return {
154 | ...state,
155 | board: boardCopy,
156 | joinning: erasedPiece(),
157 | floating: floatingCopy,
158 | matching: matchingRows,
159 | particles: {
160 | ...state.particles,
161 | key: keys.particles++,
162 | initial_grid: matchingCopy[0]?.initial_grid.map(grid =>
163 | grid.map(row => row.map(cell => (cell > 0 ? 100 : 0)))
164 | )
165 | }
166 | }
167 | case 'blocks/reset':
168 | return { ...INITIAL_STATE, piece: randomPiece() }
169 | case 'floating/fall':
170 | for (let i = 0; i < state.floating.length; i++) {
171 | testFloatingFallCollision(
172 | state.floating[i],
173 | state.board,
174 | state.limits,
175 | i
176 | )
177 | }
178 | return {
179 | ...state,
180 | floating: state.floating.map(i => {
181 | return {
182 | ...i,
183 | y: i.y + 1
184 | }
185 | })
186 | }
187 | default:
188 | return state
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/store/reducers/collision/index.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentGrid, isColliding } from '../../../controller'
2 | import type { Block, Grid } from '../../../types'
3 |
4 | export const testFloatingFallCollision = (
5 | piece: Block,
6 | board: Grid,
7 | limits: Grid,
8 | index: number
9 | ): void | Error => {
10 | const posFall = getCurrentGrid({
11 | ...piece,
12 | y: piece.y + 1
13 | })
14 |
15 | if (!posFall || isColliding(posFall, board)) {
16 | const current = getCurrentGrid(piece)
17 | // eslint-disable-next-line @typescript-eslint/naming-convention
18 | const limits_collider = limits.map(row => row.map(c => (c === 0 ? 1 : 0)))
19 | if (current && isColliding(current, limits_collider)) {
20 | throw new Error('board-collides-with-limits')
21 | }
22 |
23 | const joinCollision = new Error('floating-fall-collision')
24 | joinCollision.name = `${index}`
25 | throw joinCollision
26 | }
27 | }
28 |
29 | export const testDownCollision = (piece: Block, board: Grid): void | Error => {
30 | const posMove = getCurrentGrid({
31 | ...piece,
32 | y: piece.y + 1,
33 | anim_state: 'follow'
34 | })
35 |
36 | if (!posMove || isColliding(posMove, board)) {
37 | throw new Error('piece-down-move-collision')
38 | }
39 | }
40 |
41 | export const testSideCollision = (
42 | piece: Block,
43 | board: Grid,
44 | dir: number
45 | ): void | Error => {
46 | const posMove = getCurrentGrid({
47 | ...piece,
48 | x: piece.x + dir,
49 | anim_state: 'follow'
50 | })
51 |
52 | if (!posMove || isColliding(posMove, board)) {
53 | throw new Error('piece-side-move-collision')
54 | }
55 | }
56 |
57 | export const testRotationCollision = (piece: Block, board: Grid): Block => {
58 | const closestValidX =
59 | [0, -1, 1, -2, 2, -3, 3, 4, -4].find(index =>
60 | getCurrentGrid({
61 | ...piece,
62 | rotations: piece.rotations + 1,
63 | x: piece.x + index
64 | })
65 | ) || 0
66 | const posMove = {
67 | ...piece,
68 | rotations: piece.rotations + 1,
69 | x: piece.x + closestValidX
70 | }
71 | const posMoveGrid = getCurrentGrid(posMove)
72 | if (!posMoveGrid || isColliding(posMoveGrid, board)) {
73 | throw new Error('piece-rotation-move-collision')
74 | }
75 |
76 | return posMove
77 | }
78 |
79 | export const testJoinCollision = (board: Grid, limits: Grid): void | Error => {
80 | // eslint-disable-next-line @typescript-eslint/naming-convention
81 | const limits_collider = limits.map(row => row.map(c => (c === 0 ? 1 : 0)))
82 | if (isColliding(board, limits_collider)) {
83 | throw new Error('board-collides-with-limits')
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/store/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import audio from './audio'
4 | import blocks from './blocks'
5 | import score from './score'
6 | import ticks from './ticks'
7 |
8 | export default combineReducers({
9 | blocks,
10 | ticks,
11 | audio,
12 | score
13 | })
14 |
--------------------------------------------------------------------------------
/src/store/reducers/score.ts:
--------------------------------------------------------------------------------
1 | import type { ScoreAction } from '../actions/score'
2 |
3 | type ScoreState = number
4 | const INITIAL_SCORE_STATE = 0
5 |
6 | export default function ticks(
7 | state: ScoreState = INITIAL_SCORE_STATE,
8 | action: ScoreAction
9 | ) {
10 | let count
11 | switch (action.type) {
12 | case 'score/increment':
13 | count = action.payload
14 | return state + count * 100
15 | case 'score/reset':
16 | return 0
17 | default:
18 | return state
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/store/reducers/ticks.ts:
--------------------------------------------------------------------------------
1 | import type { TicksAction } from '../actions/ticks'
2 |
3 | const INITIAL_TICKS_STATE = 0
4 |
5 | let isGamePaused = false // Variável global para controle de pausa, o certo seria alterar o estado
6 |
7 | export default function ticks(
8 | state = INITIAL_TICKS_STATE,
9 | action: TicksAction
10 | ) {
11 | switch (action.type) {
12 | case 'ticker/increment':
13 | return isGamePaused ? state : state + 1
14 | case 'ticker/pause':
15 | isGamePaused = true
16 | return state
17 | case 'ticker/resume':
18 | isGamePaused = false
19 | return state
20 | default:
21 | return state
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/blocks.module.css:
--------------------------------------------------------------------------------
1 | .shape {
2 | display: block;
3 | }
4 |
5 | .front::before {
6 | content: '';
7 | position: absolute;
8 | border-radius: 20%;
9 | width: 20%; /* Ajuste para controlar o tamanho do destaque interno */
10 | height: 60%; /* Ajuste para controlar o tamanho do destaque interno */
11 | top: 10px; /* Ajuste para controlar a posição do destaque interno */
12 | left: 2px; /* Ajuste para controlar a posição do destaque interno */
13 | z-index: 1; /* Certifique-se de que o destaque interno fique sobre o conteúdo principal */
14 | }
15 |
16 | .front,
17 | .side {
18 | border-radius: 4px;
19 | position: absolute;
20 | width: inherit;
21 | height: inherit;
22 | }
23 |
24 | .front {
25 | background-image: radial-gradient(
26 | circle,
27 | rgba(255, 255, 255, 0.08) 1px,
28 | transparent 1px
29 | );
30 | background-size: 6px 6px;
31 | }
32 |
33 | .block_a > .front {
34 | background-color: hsl(39, 100%, 50%);
35 | }
36 | .block_a > .front::before {
37 | background-color: hsl(39, 100%, 60%);
38 | }
39 |
40 | .block_b > .front {
41 | background-color: hsl(7, 92%, 50%);
42 | }
43 |
44 | .block_b > .front::before {
45 | background-color: hsl(7, 94%, 60%); /* Cor mais clara para block_b */
46 | }
47 |
48 | .block_c > .front {
49 | background-color: hsl(325, 100%, 50%);
50 | }
51 |
52 | .block_c > .front::before {
53 | background-color: hsl(330, 100%, 60%);
54 | }
55 |
56 | .block_d > .front {
57 | background-color: hsl(150, 68%, 48%);
58 | }
59 |
60 | .block_d > .front::before {
61 | background-color: hsl(152, 69%, 60%); /* Cor mais clara para block_d */
62 | }
63 |
64 | .block_T > .front {
65 | background-color: hsl(195, 100%, 50%);
66 | }
67 |
68 | .block_T > .front::before {
69 | background-color: hsl(195, 100%, 60%); /* Cor mais clara para block_T */
70 | }
71 |
72 | .block_a > .side {
73 | /* background-color: rgba(255, 165, 0); */
74 | box-shadow:
75 | 1px 1px rgb(255, 115, 0),
76 | 2px 2px rgb(255, 115, 0),
77 | 3px 3px rgb(255, 115, 0),
78 | 4px 4px rgb(255, 115, 0),
79 | 5px 5px rgb(255, 115, 0),
80 | 6px 6px rgb(255, 115, 0),
81 | 7px 7px rgb(255, 115, 0),
82 | 8px 8px rgb(255, 115, 0),
83 | 9px 9px rgb(255, 115, 0),
84 | 10px 10px rgb(255, 115, 0);
85 | }
86 |
87 | .block_b > .side {
88 | /* background-color: rgb(248, 41, 14); */
89 | box-shadow:
90 | 1px 1px rgb(146, 17, 13),
91 | 2px 2px rgb(146, 17, 13),
92 | 3px 3px rgb(146, 17, 13),
93 | 4px 4px rgb(146, 17, 13),
94 | 5px 5px rgb(146, 17, 13),
95 | 6px 6px rgb(146, 17, 13),
96 | 7px 7px rgb(146, 17, 13),
97 | 8px 8px rgb(146, 17, 13),
98 | 9px 9px rgb(146, 17, 13),
99 | 10px 10px rgb(146, 17, 13);
100 | }
101 |
102 | .block_c > .side {
103 | /* background-color: rgb(233, 31, 149); */
104 | box-shadow:
105 | 1px 1px hsl(330, 100%, 40%),
106 | 2px 2px hsl(330, 100%, 40%),
107 | 3px 3px hsl(330, 100%, 40%),
108 | 4px 4px hsl(330, 100%, 40%),
109 | 5px 5px hsl(330, 100%, 40%),
110 | 6px 6px hsl(330, 100%, 40%),
111 | 7px 7px hsl(330, 100%, 40%),
112 | 8px 8px hsl(330, 100%, 40%),
113 | 9px 9px hsl(330, 100%, 40%),
114 | 10px 10px hsl(330, 100%, 40%);
115 | }
116 |
117 | .block_d > .side {
118 | /* background-color: rgb(36, 197, 122); */
119 | box-shadow:
120 | 1px 1px rgb(20, 114, 48),
121 | 2px 2px rgb(20, 114, 48),
122 | 3px 3px rgb(20, 114, 48),
123 | 4px 4px rgb(20, 114, 48),
124 | 5px 5px rgb(20, 114, 48),
125 | 6px 6px rgb(20, 114, 48),
126 | 7px 7px rgb(20, 114, 48),
127 | 8px 8px rgb(20, 114, 48),
128 | 9px 9px rgb(20, 114, 48),
129 | 10px 10px rgb(20, 114, 48);
130 | }
131 |
132 | .block_T > .side {
133 | /* background-color: rgb(36, 197, 122); */
134 | box-shadow:
135 | 1px 1px rgb(0, 128, 255),
136 | 2px 2px rgb(0, 128, 255),
137 | 3px 3px rgb(0, 128, 255),
138 | 4px 4px rgb(0, 128, 255),
139 | 5px 5px rgb(0, 128, 255),
140 | 6px 6px rgb(0, 128, 255),
141 | 7px 7px rgb(0, 128, 255),
142 | 8px 8px rgb(0, 128, 255),
143 | 9px 9px rgb(0, 128, 255),
144 | 10px 10px rgb(0, 128, 255);
145 | }
146 |
147 | .block_white > .front {
148 | background-color: white;
149 | }
150 |
151 | .void_block {
152 | opacity: 0;
153 | }
154 |
155 | .empty_block > .front {
156 | background-color: white;
157 | opacity: 0.1;
158 | }
159 |
160 | .block {
161 | padding: 0;
162 | width: min(10vw, 30px);
163 | height: min(10vw, 30px);
164 | }
165 |
166 | .boardContainer {
167 | display: flex;
168 | align-items: center;
169 | justify-content: center;
170 | position: relative;
171 | width: min(100vw, 300px);
172 | height: 80vh;
173 | z-index: 10;
174 | }
175 |
176 | .blockGroup {
177 | position: absolute;
178 | }
179 |
--------------------------------------------------------------------------------
/src/styles/particles.module.css:
--------------------------------------------------------------------------------
1 | .particle_block {
2 | content: ' ';
3 | position: relative; /* Adicionado */
4 | }
5 |
6 | .bolinha {
7 | position: absolute; /* Adicionado */
8 | color: white;
9 | }
10 |
11 | .bolinha_top {
12 | font-size: 7px;
13 | top: 80%;
14 | left: 30%;
15 | }
16 |
17 | .bolinha_bottom_left {
18 | font-size: 5px;
19 | top: 10%;
20 | left: 50%;
21 | }
22 |
23 | .bolinha_bottom_right {
24 | font-size: 6px;
25 | bottom: 20%;
26 | right: 20%;
27 | }
28 |
--------------------------------------------------------------------------------
/src/styles/reset.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | -webkit-font-smoothing: antialiased !important;
6 | -moz-osx-font-smoothing: grayscale !important;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/score.module.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Monomaniac One';
3 | src: url('/fonts/MonomaniacOne-Regular.woff2') format('woff2');
4 | }
5 |
6 | .scoreContainer {
7 | display: flex;
8 | align-items: center;
9 | justify-content: space-between;
10 | width: min(50vw, 200px);
11 | margin: auto;
12 | }
13 | .highScore {
14 | display: flex;
15 | /* justify-content: space-between; */
16 | width: 10vw;
17 | }
18 | .highScore img {
19 | margin: 0 0 0 10px;
20 | }
21 |
22 | .scoreContainer img {
23 | width: 5vh;
24 | flex-shrink: 0;
25 | }
26 |
27 | .scoreValue {
28 | font-family: 'Monomaniac One', monospace, Arial, sans-serif;
29 | font-size: 5vh;
30 | color: white;
31 | }
32 |
--------------------------------------------------------------------------------
/src/types/block.d.ts:
--------------------------------------------------------------------------------
1 | import { type Grid } from '.'
2 |
3 | export type Block = {
4 | key: number
5 | initial_grid: Grid[]
6 | x: number
7 | y: number
8 | rotations: number
9 | anim_state: string
10 | }
11 |
12 | export type BlocksState = {
13 | piece: Block
14 | board: Grid
15 | limits: Grid
16 | joinning: Block
17 | floating: Block[]
18 | matching: Block[]
19 | particles: Block
20 | }
21 |
--------------------------------------------------------------------------------
/src/types/grid.d.ts:
--------------------------------------------------------------------------------
1 | export type Grid = number[][]
2 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { Block } from './block'
2 | import type { Grid } from './grid'
3 |
4 | export type { Block, Grid }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "outDir": "dist",
5 | "target": "ES2022",
6 | "allowJs": true,
7 | "esModuleInterop": true,
8 | "jsx": "react-jsx",
9 | "lib": ["dom", "dom.iterable", "esnext"],
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | // "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noEmit": true,
16 | "isolatedModules": true,
17 | "jsxImportSource": "react",
18 | "removeComments": true,
19 | "moduleDetection": "force",
20 | "sourceMap": true,
21 | // "noImplicitThis": true,
22 | // "noImplicitAny": true,
23 | // "strictNullChecks": true,
24 | "allowSyntheticDefaultImports": true,
25 | "noFallthroughCasesInSwitch": true,
26 | "noUnusedParameters": true,
27 | "noUnusedLocals": true,
28 | "noUncheckedIndexedAccess": true,
29 | "verbatimModuleSyntax": true,
30 | "declaration": true,
31 | "skipLibCheck": true,
32 | "incremental": true,
33 | "types": ["vite/client", "vitest/globals"]
34 | },
35 | "include": ["src", "./vitest.setup.ts"],
36 | "exclude": ["dist", "node_modules"]
37 | }
38 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "enabled": false,
4 | "silent": true
5 | },
6 | "headers": [
7 | {
8 | "source": "/(.*)",
9 | "headers": [
10 | {
11 | "key": "X-Content-Type-Options",
12 | "value": "nosniff"
13 | },
14 | {
15 | "key": "X-Frame-Options",
16 | "value": "SAMEORIGIN"
17 | },
18 | {
19 | "key": "X-XSS-Protection",
20 | "value": "1; mode=block"
21 | },
22 | {
23 | "key": "Referrer-Policy",
24 | "value": "strict-origin"
25 | },
26 | {
27 | "key": "Permissions-Policy",
28 | "value": "geolocation=(self), microphone=()"
29 | }
30 | ]
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc'
2 | import { defineConfig, loadEnv } from 'vite'
3 | import tsconfigPaths from 'vite-tsconfig-paths'
4 |
5 | export default defineConfig(({ mode }) => {
6 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
7 | return {
8 | base: `${process.env.VITE_PUBLIC_PATH}/`,
9 | build: {
10 | sourcemap: mode === 'development'
11 | },
12 | plugins: [tsconfigPaths(), react()],
13 | server: {
14 | open: true,
15 | port: Number(process.env.VITE_PORT)
16 | },
17 | test: {
18 | environment: 'happy-dom',
19 | globals: true,
20 | passWithNoTests: true,
21 | setupFiles: ['./vitest.setup.ts']
22 | }
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/vitest.setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest'
2 | import 'jsdom-global'
3 |
--------------------------------------------------------------------------------