├── .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 | ![Preview](preview.gif) 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 | 7 | 8 | 10 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/icons/play-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/react-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/tetris-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/icons/twitter-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 18 | 19 | -------------------------------------------------------------------------------- /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 | Ícone do React 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 | --------------------------------------------------------------------------------