├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── Eks-terraform ├── backend.tf ├── main.tf └── provider.tf ├── Jenkins-CICD ├── Jenkinsfile ├── backend.tf ├── error.html ├── index.html ├── main.tf ├── outputs.tf ├── provider.tf ├── s3.tf ├── script.js ├── style.css ├── variables.tf └── website.sh ├── Jenkins-terraform ├── Main.tf ├── backend.tf ├── install_jenkins.sh └── provider.tf ├── README.md ├── deployment-service.yml ├── images └── game.jpg ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── components ├── Cell.js ├── Cell.module.css ├── Game.js ├── Game.module.css ├── Game.test.js ├── Level.js ├── Next.js ├── Next.module.css └── Score.js ├── helpers ├── Generators.js └── Helpers.js ├── index.css ├── index.js ├── reportWebVitals.js ├── setupTests.js ├── tetris ├── Colors.js ├── Piece.js ├── State.js ├── Tetris.js └── Tetromino.js └── webpack.config.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to EKS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: [self-hosted] 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Update kubeconfig 17 | run: aws eks --region ap-south-1 update-kubeconfig --name EKS_CLOUD 18 | 19 | - name: Deploy to EKS 20 | run: kubectl delete -f deployment-service.yml 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ide 2 | #intellij 3 | .idea/ 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | 44 | #build directory 45 | build 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | 113 | #Mac 114 | .DS_Store 115 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js 16 image as the base image 2 | FROM node:16 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json (or yarn.lock) to the container 8 | COPY package*.json ./ 9 | 10 | # Install project dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code to the container 14 | COPY . . 15 | 16 | # Build the React app 17 | RUN npm run build 18 | 19 | # Expose the port that the app will run on (usually 3000 by default) 20 | EXPOSE 3000 21 | 22 | # Start the React app when the container starts 23 | CMD [ "npm", "start" ] 24 | -------------------------------------------------------------------------------- /Eks-terraform/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | bucket = "mario123bucket" # Replace with your actual S3 bucket name 4 | key = "Jenkins/terraform.tfstate" 5 | region = "ap-south-1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Eks-terraform/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "assume_role" { 2 | statement { 3 | effect = "Allow" 4 | 5 | principals { 6 | type = "Service" 7 | identifiers = ["eks.amazonaws.com"] 8 | } 9 | 10 | actions = ["sts:AssumeRole"] 11 | } 12 | } 13 | 14 | resource "aws_iam_role" "example" { 15 | name = "eks-cluster-cloud" 16 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 17 | } 18 | 19 | resource "aws_iam_role_policy_attachment" "example-AmazonEKSClusterPolicy" { 20 | policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" 21 | role = aws_iam_role.example.name 22 | } 23 | 24 | #get vpc data 25 | data "aws_vpc" "default" { 26 | default = true 27 | } 28 | #get public subnets for cluster 29 | data "aws_subnets" "public" { 30 | filter { 31 | name = "vpc-id" 32 | values = [data.aws_vpc.default.id] 33 | } 34 | } 35 | #cluster provision 36 | resource "aws_eks_cluster" "example" { 37 | name = "EKS_CLOUD" 38 | role_arn = aws_iam_role.example.arn 39 | 40 | vpc_config { 41 | subnet_ids = data.aws_subnets.public.ids 42 | } 43 | 44 | # Ensure that IAM Role permissions are created before and deleted after EKS Cluster handling. 45 | # Otherwise, EKS will not be able to properly delete EKS managed EC2 infrastructure such as Security Groups. 46 | depends_on = [ 47 | aws_iam_role_policy_attachment.example-AmazonEKSClusterPolicy, 48 | ] 49 | } 50 | 51 | resource "aws_iam_role" "example1" { 52 | name = "eks-node-group-cloud" 53 | 54 | assume_role_policy = jsonencode({ 55 | Statement = [{ 56 | Action = "sts:AssumeRole" 57 | Effect = "Allow" 58 | Principal = { 59 | Service = "ec2.amazonaws.com" 60 | } 61 | }] 62 | Version = "2012-10-17" 63 | }) 64 | } 65 | 66 | resource "aws_iam_role_policy_attachment" "example-AmazonEKSWorkerNodePolicy" { 67 | policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" 68 | role = aws_iam_role.example1.name 69 | } 70 | 71 | resource "aws_iam_role_policy_attachment" "example-AmazonEKS_CNI_Policy" { 72 | policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" 73 | role = aws_iam_role.example1.name 74 | } 75 | 76 | resource "aws_iam_role_policy_attachment" "example-AmazonEC2ContainerRegistryReadOnly" { 77 | policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" 78 | role = aws_iam_role.example1.name 79 | } 80 | 81 | #create node group 82 | resource "aws_eks_node_group" "example" { 83 | cluster_name = aws_eks_cluster.example.name 84 | node_group_name = "Node-cloud" 85 | node_role_arn = aws_iam_role.example1.arn 86 | subnet_ids = data.aws_subnets.public.ids 87 | 88 | scaling_config { 89 | desired_size = 1 90 | max_size = 2 91 | min_size = 1 92 | } 93 | instance_types = ["t2.medium"] 94 | 95 | # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. 96 | # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces. 97 | depends_on = [ 98 | aws_iam_role_policy_attachment.example-AmazonEKSWorkerNodePolicy, 99 | aws_iam_role_policy_attachment.example-AmazonEKS_CNI_Policy, 100 | aws_iam_role_policy_attachment.example-AmazonEC2ContainerRegistryReadOnly, 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /Eks-terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | 10 | # Configure the AWS Provider 11 | provider "aws" { 12 | region = "ap-south-1" 13 | } 14 | -------------------------------------------------------------------------------- /Jenkins-CICD/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline{ 2 | agent any 3 | tools{ 4 | jdk 'jdk17' 5 | terraform 'terraform' 6 | } 7 | stages{ 8 | stage('clean Workspace'){ 9 | steps{ 10 | cleanWs() 11 | } 12 | } 13 | stage('checkout from Git'){ 14 | steps{ 15 | git branch: 'main', url: 'https://github.com/Aj7Ay/TERRAFORM-JENKINS-CICD.git' 16 | } 17 | } 18 | stage('Terraform version'){ 19 | steps{ 20 | sh 'terraform --version' 21 | } 22 | } 23 | stage("Sonarqube Analysis "){ 24 | steps{ 25 | withSonarQubeEnv('sonar-server') { 26 | sh ''' $SCANNER_HOME/bin/sonar-scanner -Dsonar.projectName=Terraform \ 27 | -Dsonar.projectKey=Terraform ''' 28 | } 29 | } 30 | } 31 | stage("quality gate"){ 32 | steps { 33 | script { 34 | waitForQualityGate abortPipeline: false, credentialsId: 'Sonar-token' 35 | } 36 | } 37 | } 38 | stage('TRIVY FS SCAN') { 39 | steps { 40 | sh "trivy fs . > trivyfs.txt" 41 | } 42 | } 43 | stage('Excutable permission to userdata'){ 44 | steps{ 45 | sh 'chmod 777 website.sh' 46 | } 47 | } 48 | stage('Terraform init'){ 49 | steps{ 50 | sh 'terraform init' 51 | } 52 | } 53 | stage('Terraform plan'){ 54 | steps{ 55 | sh 'terraform plan' 56 | } 57 | } 58 | stage('Terraform apply'){ 59 | steps{ 60 | sh 'terraform ${action} --auto approve' 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Jenkins-CICD/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | bucket = var.bucket_name 4 | key = "my-terraform-environment/main" 5 | region = var.aws_region 6 | dynamodb_table = var.dynamodb_table 7 | } 8 | } -------------------------------------------------------------------------------- /Jenkins-CICD/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 page | Nothing4us 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |

404

19 | 20 | 21 |
22 | 23 |
24 |

25 | Look like you're lost 26 |

27 | 28 |

the page you are looking for not avaible!

29 | 30 | Go to Home 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /Jenkins-CICD/index.html: -------------------------------------------------------------------------------- 1 | HTML CSS JSResult Skip Results Iframe 2 | 3 | 4 | 5 | 6 | Login Page Form | Nothing4us 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 78 | 83 | 84 |
85 |
86 | 89 | 92 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Jenkins-CICD/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_instance" "ec2_instance" { 2 | ami = var.ami_id 3 | instance_type = var.instance_type 4 | key_name = var.key_name 5 | vpc_security_group_ids = [aws_security_group.ec2_security_group.id] 6 | user_data = base64encode(file("website.sh")) 7 | tags = { 8 | Name = "aws-ec2-instance" 9 | } 10 | } 11 | 12 | resource "aws_security_group" "ec2_security_group" { 13 | name = "ec2 security group" 14 | description = "allow access on ports 80 and 22 and 443" 15 | 16 | ingress = [ 17 | for port in [22, 80, 443] : { 18 | description = "TLS from VPC" 19 | from_port = port 20 | to_port = port 21 | protocol = "tcp" 22 | cidr_blocks = ["0.0.0.0/0"] 23 | ipv6_cidr_blocks = [] 24 | prefix_list_ids = [] 25 | security_groups = [] 26 | self = false 27 | } 28 | ] 29 | 30 | egress { 31 | from_port = 0 32 | to_port = 0 33 | protocol = "-1" 34 | cidr_blocks = ["0.0.0.0/0"] 35 | ipv6_cidr_blocks = ["::/0"] 36 | } 37 | 38 | tags = { 39 | Name = "ec2-sg" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Jenkins-CICD/outputs.tf: -------------------------------------------------------------------------------- 1 | output "websiteendpoint" { 2 | value = aws_s3_bucket.s3storage.website_endpoint 3 | } 4 | 5 | output "public_ip" { 6 | value = aws_instance.ec2_instance.public_ip 7 | } -------------------------------------------------------------------------------- /Jenkins-CICD/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.16" 6 | } 7 | } 8 | required_version = ">= 1.2.0" 9 | } 10 | 11 | provider "aws" { 12 | region = var.aws_region 13 | } -------------------------------------------------------------------------------- /Jenkins-CICD/s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "s3storage" { 2 | bucket = var.bucket_name 3 | } 4 | resource "aws_s3_bucket_ownership_controls" "s3storage" { 5 | bucket = aws_s3_bucket.s3storage.id 6 | rule { 7 | object_ownership = "BucketOwnerPreferred" 8 | } 9 | } 10 | 11 | resource "aws_s3_bucket_public_access_block" "s3storage" { 12 | bucket = aws_s3_bucket.s3storage.id 13 | block_public_acls = false 14 | block_public_policy = false 15 | ignore_public_acls = false 16 | restrict_public_buckets = false 17 | } 18 | 19 | resource "aws_s3_bucket_acl" "s3storage" { 20 | depends_on = [ 21 | aws_s3_bucket_ownership_controls.s3storage, 22 | aws_s3_bucket_public_access_block.s3storage, 23 | ] 24 | bucket = aws_s3_bucket.s3storage.id 25 | acl = "public-read" 26 | } 27 | 28 | resource "aws_s3_object" "index" { 29 | bucket = aws_s3_bucket.s3storage 30 | key = "index.html" 31 | source = "index.html" 32 | acl = "public-read" 33 | content_type = "text/html" 34 | } 35 | 36 | 37 | resource "aws_s3_object" "error" { 38 | bucket = aws_s3_bucket.s3storage 39 | key = "error.html" 40 | source = "error.html" 41 | acl = "public-read" 42 | content_type = "text/html" 43 | } 44 | 45 | 46 | resource "aws_s3_object" "style" { 47 | bucket = aws_s3_bucket.s3storage.id 48 | key = "style.css" 49 | source = "style.css" 50 | acl = "public-read" 51 | content_type = "text/css" 52 | } 53 | 54 | 55 | resource "aws_s3_object" "script" { 56 | bucket = aws_s3_bucket.s3storage.id 57 | key = "script.js" 58 | source = "script.js" 59 | acl = "public-read" 60 | content_type = "text/javascript" 61 | } 62 | 63 | 64 | resource "aws_s3_bucket_website_configuration" "website" { 65 | bucket = aws_s3_bucket.s3storage.id 66 | index_document { 67 | suffix = "index.html" 68 | } 69 | 70 | error_document { 71 | key = "error.html" 72 | } 73 | 74 | depends_on = [aws_s3_bucket_acl.s3storage.id] 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Jenkins-CICD/script.js: -------------------------------------------------------------------------------- 1 | let usernameInput = document.querySelector('.username'); 2 | let passwordInput = document.querySelector('.password'); 3 | let showPasswordButton = document.querySelector('.password-button'); 4 | let face = document.querySelector('.face'); 5 | 6 | passwordInput.addEventListener('focus', event => { 7 | document.querySelectorAll('.hand').forEach(hand => { 8 | hand.classList.add('hide'); 9 | }); 10 | document.querySelector('.tongue').classList.remove('breath'); 11 | }); 12 | 13 | passwordInput.addEventListener('blur', event => { 14 | document.querySelectorAll('.hand').forEach(hand => { 15 | hand.classList.remove('hide'); 16 | hand.classList.remove('peek'); 17 | }); 18 | document.querySelector('.tongue').classList.add('breath'); 19 | }); 20 | 21 | usernameInput.addEventListener('focus', event => { 22 | let length = Math.min(usernameInput.value.length - 16, 19); 23 | document.querySelectorAll('.hand').forEach(hand => { 24 | hand.classList.remove('hide'); 25 | hand.classList.remove('peek'); 26 | }); 27 | 28 | face.style.setProperty('--rotate-head', `${-length}deg`); 29 | }); 30 | 31 | usernameInput.addEventListener('blur', event => { 32 | face.style.setProperty('--rotate-head', '0deg'); 33 | }); 34 | 35 | usernameInput.addEventListener('input', _.throttle(event => { 36 | let length = Math.min(event.target.value.length - 16, 19); 37 | 38 | face.style.setProperty('--rotate-head', `${-length}deg`); 39 | }, 100)); 40 | 41 | showPasswordButton.addEventListener('click', event => { 42 | if (passwordInput.type === 'text') { 43 | passwordInput.type = 'password'; 44 | document.querySelectorAll('.hand').forEach(hand => { 45 | hand.classList.remove('peek'); 46 | hand.classList.add('hide'); 47 | }); 48 | } else { 49 | passwordInput.type = 'text'; 50 | document.querySelectorAll('.hand').forEach(hand => { 51 | hand.classList.remove('hide'); 52 | hand.classList.add('peek'); 53 | }); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /Jenkins-CICD/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | body { 5 | width: 100vw; 6 | height: 100vh; 7 | background-color: rgb(41, 0, 75); 8 | overflow: hidden; 9 | font-size: 12px; 10 | } 11 | .inspiration { 12 | position: fixed; 13 | bottom: 0; 14 | right: 0; 15 | padding: 10px; 16 | text-align: center; 17 | text-decoration: none; 18 | font-family: 'Gill Sans', sans-serif; 19 | font-size: 12px; 20 | color: #969696; 21 | } 22 | .inspiration img { 23 | width: 60px; 24 | } 25 | .center { 26 | position: relative; 27 | top: 50%; 28 | left: 50%; 29 | display: inline-block; 30 | width: 275px; 31 | height: 490px; 32 | border-radius: 3px; 33 | transform: translate(-50%, -50%); 34 | overflow: hidden; 35 | background-image: linear-gradient(to top right, rgb(0 168 255), rgb(249 95 230)); 36 | } 37 | @media screen and (max-height: 500px) { 38 | .center { 39 | transition: transform 0.5s; 40 | transform: translate(-50%, -50%) scale(0.8); 41 | } 42 | } 43 | .center .ear { 44 | position: absolute; 45 | top: -110px; 46 | width: 200px; 47 | height: 200px; 48 | border-radius: 50%; 49 | background-color: rgb(50 22 22); 50 | } 51 | .center .ear.ear--left { 52 | left: -135px; 53 | } 54 | .center .ear.ear--right { 55 | right: -135px; 56 | } 57 | .center .face { 58 | display: flex; 59 | flex-direction: column; 60 | align-items: center; 61 | width: 200px; 62 | height: 150px; 63 | margin: 80px auto 10px; 64 | --rotate-head: 0deg; 65 | transform: rotate(var(--rotate-head)); 66 | transition: transform 0.2s; 67 | transform-origin: center 20px; 68 | } 69 | .center .eye { 70 | display: inline-block; 71 | width: 25px; 72 | height: 25px; 73 | border-radius: 50%; 74 | background-color: #243946; 75 | } 76 | .center .eye.eye--left { 77 | margin-right: 40px; 78 | } 79 | .center .eye.eye--right { 80 | margin-left: 40px; 81 | } 82 | .center .eye .glow { 83 | position: relative; 84 | top: 3px; 85 | right: -12px; 86 | width: 12px; 87 | height: 6px; 88 | border-radius: 50%; 89 | background-color: #fff; 90 | transform: rotate(38deg); 91 | } 92 | .center .nose { 93 | position: relative; 94 | top: 30px; 95 | transform: scale(1.1); 96 | } 97 | .center .nose .glow { 98 | position: absolute; 99 | top: 3px; 100 | left: 32%; 101 | width: 15px; 102 | height: 8px; 103 | border-radius: 50%; 104 | background-color: #476375; 105 | } 106 | .center .mouth { 107 | position: relative; 108 | margin-top: 45px; 109 | } 110 | .center svg.smile { 111 | position: absolute; 112 | left: -28px; 113 | top: -19px; 114 | transform: scaleX(1.1); 115 | stroke: #243946; 116 | } 117 | .center .mouth-hole { 118 | position: absolute; 119 | top: 0; 120 | left: -50%; 121 | width: 60px; 122 | height: 15px; 123 | border-radius: 50%/100% 100% 0% 0; 124 | transform: rotate(180deg); 125 | background-color: #243946; 126 | z-index: -1; 127 | } 128 | .center .tongue { 129 | position: relative; 130 | top: 5px; 131 | width: 30px; 132 | height: 20px; 133 | background-color: #ffd7dd; 134 | transform-origin: top; 135 | transform: rotateX(60deg); 136 | } 137 | .center .tongue.breath { 138 | -webkit-animation: breath 0.3s infinite linear; 139 | animation: breath 0.3s infinite linear; 140 | } 141 | .center .tongue-top { 142 | position: absolute; 143 | bottom: -15px; 144 | width: 30px; 145 | height: 30px; 146 | border-radius: 15px; 147 | background-color: #ffd7dd; 148 | } 149 | .center .line { 150 | position: absolute; 151 | top: 0; 152 | width: 30px; 153 | height: 5px; 154 | background-color: #fcb7bf; 155 | } 156 | .center .median { 157 | position: absolute; 158 | top: 0; 159 | left: 50%; 160 | transform: translateX(-50%); 161 | width: 4px; 162 | height: 25px; 163 | border-radius: 5px; 164 | background-color: #fcb7bf; 165 | } 166 | .center .hands { 167 | position: relative; 168 | } 169 | .center .hands .hand { 170 | position: absolute; 171 | top: -6px; 172 | display: flex; 173 | transition: transform 0.5s ease-in-out; 174 | z-index: 1; 175 | } 176 | .center .hands .hand--left { 177 | left: 50px; 178 | } 179 | .center .hands .hand--left.hide { 180 | transform: translate(2px, -155px) rotate(-160deg); 181 | } 182 | .center .hands .hand--left.peek { 183 | transform: translate(0px, -120px) rotate(-160deg); 184 | } 185 | .center .hands .hand--right { 186 | left: 170px; 187 | } 188 | .center .hands .hand--right.hide { 189 | transform: translate(-6px, -155px) rotate(160deg); 190 | } 191 | .center .hands .hand--right.peek { 192 | transform: translate(-4px, -120px) rotate(160deg); 193 | } 194 | .center .hands .finger { 195 | position: relative; 196 | z-index: 0; 197 | } 198 | .center .hands .finger .bone { 199 | width: 20px; 200 | height: 20px; 201 | border: 2px solid #243946; 202 | border-bottom: none; 203 | border-top: none; 204 | background-color: rgb(255 211 11); 205 | } 206 | .center .hands .finger .nail { 207 | position: absolute; 208 | left: 0; 209 | top: 10px; 210 | width: 20px; 211 | height: 18px; 212 | border-radius: 50%; 213 | border: 2px solid #243946; 214 | background-color: #fac555; 215 | z-index: -1; 216 | } 217 | .center .hands .finger:nth-child(1), 218 | .center .hands .finger:nth-child(3) { 219 | left: 4px; 220 | z-index: 1; 221 | } 222 | .center .hands .finger:nth-child(1) .bone, 223 | .center .hands .finger:nth-child(3) .bone { 224 | height: 10px; 225 | } 226 | .center .hands .finger:nth-child(3) { 227 | left: -4px; 228 | } 229 | .center .hands .finger:nth-child(2) { 230 | top: -5px; 231 | z-index: 2; 232 | } 233 | .center .hands .finger:nth-child(1) .nail, 234 | .center .hands .finger:nth-child(3) .nail { 235 | top: 0px; 236 | } 237 | .center .login { 238 | position: relative; 239 | display: flex; 240 | flex-direction: column; 241 | } 242 | .center .login label { 243 | position: relative; 244 | padding: 0 20px; 245 | } 246 | .center .login label .fa { 247 | position: absolute; 248 | top: 40%; 249 | left: 35px; 250 | color: #bbb; 251 | } 252 | .center .login label .fa:before { 253 | position: relative; 254 | left: 1px; 255 | } 256 | .center .login input, 257 | .center .login .login-button { 258 | width: 100%; 259 | height: 35px; 260 | border: none; 261 | border-radius: 30px; 262 | } 263 | .center .login input { 264 | padding: 0 20px 0 40px; 265 | margin: 5px 0; 266 | box-shadow: none; 267 | outline: none; 268 | } 269 | .center .login input::-moz-placeholder { 270 | color: #ccc; 271 | } 272 | .center .login input:-ms-input-placeholder { 273 | color: #ccc; 274 | } 275 | .center .login input::placeholder { 276 | color: #ccc; 277 | } 278 | .center .login input.password { 279 | padding: 0 90px 0 40px; 280 | } 281 | .center .login .password-button { 282 | position: absolute; 283 | top: 9px; 284 | right: 25px; 285 | display: flex; 286 | justify-content: center; 287 | align-items: center; 288 | width: 80px; 289 | height: 27px; 290 | border-radius: 30px; 291 | border: none; 292 | outline: none; 293 | background-color: #243946; 294 | color: #fff; 295 | } 296 | .center .login .password-button:active { 297 | transform: scale(0.95); 298 | } 299 | .center .login .login-button { 300 | width: calc(100% - 40px); 301 | margin: 20px 20px 0; 302 | outline: none; 303 | background-color: #243946; 304 | color: #fff; 305 | transition: transform 0.1s; 306 | } 307 | .center .login .login-button:active { 308 | transform: scale(0.95); 309 | } 310 | .center .social-buttons { 311 | display: flex; 312 | justify-content: center; 313 | margin-top: 25px; 314 | } 315 | .center .social-buttons .social { 316 | display: flex; 317 | justify-content: center; 318 | align-items: center; 319 | width: 35px; 320 | height: 35px; 321 | margin: 0 10px; 322 | border-radius: 50%; 323 | background-color: #243946; 324 | color: #fff; 325 | font-size: 18px; 326 | } 327 | .center .social-buttons .social:active { 328 | transform: scale(0.95); 329 | } 330 | .center .footer { 331 | text-align: center; 332 | margin-top: 15px; 333 | } 334 | @-webkit-keyframes breath { 335 | 0%, 100% { 336 | transform: rotateX(0deg); 337 | } 338 | 50% { 339 | transform: rotateX(60deg); 340 | } 341 | } 342 | @keyframes breath { 343 | 0%, 100% { 344 | transform: rotateX(0deg); 345 | } 346 | 50% { 347 | transform: rotateX(60deg); 348 | } 349 | } -------------------------------------------------------------------------------- /Jenkins-CICD/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | description = "The AWS region to create things in." 3 | default = "ap-southeast-2" 4 | } 5 | variable "key_name" { 6 | description = " SSH keys to connect to ec2 instance" 7 | default = "purplehaze" 8 | } 9 | variable "instance_type" { 10 | description = "instance type for ec2" 11 | default = "t2.medium" 12 | } 13 | variable "ami_id" { 14 | description = "AMI for Ubuntu Ec2 instance" 15 | default = "ami-0df4b2961410d4cff" 16 | } 17 | variable "bucket_name" { 18 | description = "The name of the S3 bucket to create" 19 | type = string 20 | default = "purplehaze777" 21 | } 22 | 23 | variable "dynamodb_table" { 24 | description = "The name of the dynamodb table" 25 | type = string 26 | default = "purplehaze777-dynamodb-table" 27 | } 28 | 29 | variable "acl" { 30 | description = "The ACL (Access Control List) for the S3 bucket" 31 | type = string 32 | default = "private" 33 | } -------------------------------------------------------------------------------- /Jenkins-CICD/website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update the package manager and install Docker 4 | sudo apt-get update -y 5 | sudo apt-get install -y docker.io 6 | 7 | # Start the Docker service 8 | sudo systemctl start docker 9 | 10 | # Enable Docker to start on boot 11 | sudo systemctl enable docker 12 | 13 | # Pull and run a simple Nginx web server container 14 | sudo docker run -d --name zomato -p 3000:3000 sevenajay/zomato:latest 15 | sudo docker run -d --name netflix -p 8081:80 sevenajay/netflix:latest -------------------------------------------------------------------------------- /Jenkins-terraform/Main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "example_role" { 2 | name = "Jenkins-terraform" 3 | assume_role_policy = < /dev/null 9 | echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null 10 | sudo apt-get update -y 11 | sudo apt-get install jenkins -y 12 | sudo systemctl start jenkins 13 | sudo systemctl status jenkins 14 | 15 | #install docker 16 | sudo apt-get update 17 | sudo apt-get install docker.io -y 18 | sudo usermod -aG docker ubuntu 19 | sudo usermod -aG docker jenkins 20 | newgrp docker 21 | sudo chmod 777 /var/run/docker.sock 22 | docker run -d --name sonar -p 9000:9000 sonarqube:lts-community 23 | 24 | # install trivy 25 | sudo apt-get install wget apt-transport-https gnupg lsb-release -y 26 | wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null 27 | echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list 28 | sudo apt-get update 29 | sudo apt-get install trivy -y 30 | 31 | #install terraform 32 | sudo apt install wget -y 33 | wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg 34 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list 35 | sudo apt update && sudo apt install terraform 36 | 37 | #install Kubectl on Jenkins 38 | sudo apt update 39 | sudo apt install curl -y 40 | curl -LO https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl 41 | sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl 42 | kubectl version --client 43 | 44 | #install Aws cli 45 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 46 | sudo apt-get install unzip -y 47 | unzip awscliv2.zip 48 | sudo ./aws/install 49 | 50 | -------------------------------------------------------------------------------- /Jenkins-terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | 10 | # Configure the AWS Provider 11 | provider "aws" { 12 | region = "ap-south-1" 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Tetris V1 2 | 3 | Tetris game built with React 4 | 5 |

6 | React tetris 7 |

8 | 9 | 10 | Use Sonarqube block 11 | ``` 12 | environment { 13 | SCANNER_HOME=tool 'sonar-scanner' 14 | } 15 | 16 | stage("Sonarqube Analysis "){ 17 | steps{ 18 | withSonarQubeEnv('sonar-server') { 19 | sh ''' $SCANNER_HOME/bin/sonar-scanner -Dsonar.projectName=Amazon \ 20 | -Dsonar.projectKey=Amazon ''' 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | Owasp block 27 | ``` 28 | stage('OWASP FS SCAN') { 29 | steps { 30 | dependencyCheck additionalArguments: '--scan ./ --disableYarnAudit --disableNodeAudit', odcInstallation: 'DP-Check' 31 | dependencyCheckPublisher pattern: '**/dependency-check-report.xml' 32 | } 33 | } 34 | ``` 35 | 36 | # ARGO CD SETUP 37 | https://archive.eksworkshop.com/intermediate/290_argocd/install/ 38 | 39 | # Image updater stage 40 | ``` 41 | environment { 42 | GIT_REPO_NAME = "Tetris-deployment-file" 43 | GIT_USER_NAME = "Aakibgithuber" 44 | } 45 | stage('Checkout Code') { 46 | steps { 47 | git branch: 'main', url: 'https://github.com/Aakibgithuber/Tetris-gamev1.git' 48 | } 49 | } 50 | 51 | stage('Update Deployment File') { 52 | steps { 53 | script { 54 | withCredentials([string(credentialsId: 'github', variable: 'GITHUB_TOKEN')]) { 55 | // Determine the image name dynamically based on your versioning strategy 56 | NEW_IMAGE_NAME = "aakibkhan1212/tetrisv1:latest" 57 | 58 | // Replace the image name in the deployment.yaml file 59 | sh "sed -i 's|image: .*|image: $NEW_IMAGE_NAME|' deployment.yml" 60 | 61 | // Git commands to stage, commit, and push the changes 62 | sh 'git add deployment.yml' 63 | sh "git commit -m 'Update deployment image to $NEW_IMAGE_NAME'" 64 | sh "git push https://${GITHUB_TOKEN}@github.com/${GIT_USER_NAME}/${GIT_REPO_NAME} HEAD:main" 65 | } 66 | } 67 |    } 68 |     } 69 | 70 | ``` 71 | -------------------------------------------------------------------------------- /deployment-service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: tetris 6 | spec: 7 | replicas: 3 8 | selector: 9 | matchLabels: 10 | app: tetris 11 | template: 12 | metadata: 13 | labels: 14 | app: tetris 15 | spec: 16 | containers: 17 | - name: tetris 18 | image: sevenajay/tetrisv2:latest 19 | ports: 20 | - containerPort: 3000 # Use port 3000 21 | 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: tetris-service 27 | spec: 28 | selector: 29 | app: tetris 30 | ports: 31 | - protocol: TCP 32 | port: 80 # Expose port 80 33 | targetPort: 3000 34 | type: LoadBalancer 35 | -------------------------------------------------------------------------------- /images/game.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aakibgithuber/Tetris-gamev1/50fdded4a58ed49b22cdc9d41051107ab22f893d/images/game.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tetris-react-js", 3 | "version": "0.1.0", 4 | "homepage": "./", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.10", 8 | "@testing-library/react": "^11.2.6", 9 | "@testing-library/user-event": "^12.8.3", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-scripts": "4.0.3", 13 | "web-vitals": "^1.1.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "css-loader": "^4.3.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aakibgithuber/Tetris-gamev1/50fdded4a58ed49b22cdc9d41051107ab22f893d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aakibgithuber/Tetris-gamev1/50fdded4a58ed49b22cdc9d41051107ab22f893d/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aakibgithuber/Tetris-gamev1/50fdded4a58ed49b22cdc9d41051107ab22f893d/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/Cell.js: -------------------------------------------------------------------------------- 1 | import styles from "./Cell.module.css"; 2 | import React, { useEffect, useState } from 'react'; 3 | import {tetromino2Class} from "../helpers/Helpers"; 4 | export const Cell = (props) => { 5 | const [tetromino, setTetromino] = useState(props.tetromino); 6 | 7 | useEffect(() => { 8 | setTetromino(props.tetromino); 9 | }, [props.tetromino]) 10 | 11 | return ( 12 |
13 | ) 14 | } 15 | 16 | export default Cell; 17 | -------------------------------------------------------------------------------- /src/components/Cell.module.css: -------------------------------------------------------------------------------- 1 | .tetromino { 2 | float: left; /* fix for buggy browsers */ 3 | display: table-column; 4 | width: 25px; 5 | height: 25px; 6 | /* border: 1px solid; */ 7 | margin: 1px; 8 | } 9 | .empty { 10 | background-color: lightgray; 11 | } 12 | 13 | .red { 14 | background-color: red; 15 | } 16 | 17 | .green { 18 | background-color: green; 19 | } 20 | 21 | .blue { 22 | background-color: dodgerblue; 23 | } 24 | 25 | .yellow { 26 | background-color: #ffbc31; 27 | } -------------------------------------------------------------------------------- /src/components/Game.js: -------------------------------------------------------------------------------- 1 | import Next from "./Next.js"; 2 | import Score from "./Score.js"; 3 | import Level from "./Level.js"; 4 | import {createMatrix} from "../helpers/Helpers"; 5 | import React, {useState, useEffect} from 'react'; 6 | import styles from "./Game.module.css"; 7 | import {emtpyPiece} from "../tetris/Piece"; 8 | 9 | export const Game = (props) => { 10 | const tetris = props.tetris; 11 | const [state, setState] = useState(props.tetris.state); 12 | 13 | const onStateChange = (_state) => { 14 | setState(_state); 15 | }; 16 | 17 | useEffect(() => { 18 | tetris.onStateChange(onStateChange); 19 | document.title ="React Tetris"; 20 | }, [tetris]); 21 | 22 | 23 | let resumePauseButton; 24 | if (state.isStarted()) { 25 | if (state.isRunning()) { 26 | resumePauseButton = 27 | 28 | } else { 29 | resumePauseButton = 30 | 31 | } 32 | } 33 | 34 | 35 | const matrix = createMatrix(state.visibleMatrix(), styles); 36 | 37 | const nextPiece = state.nextPiece(); 38 | const visibleNextPiece = nextPiece ? nextPiece : emtpyPiece(); 39 | 40 | const gameOver = state.isGameOver() ? 41 |
42 |

Game Over

43 |
44 | : 45 | ''; 46 | 47 | return ( 48 |
49 |
React Tetris
50 | 51 |
52 | {matrix} 53 | {gameOver} 54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | {resumePauseButton} 71 |
72 |
73 |
74 |

75 | Start/Pause/Resume: Space
76 | Rotate: Arrow Up
77 | Left: Arrow Left
78 | Right: Arrow Right
79 | Soft Drop: Arrow Down
80 |

81 |
82 |
83 |
84 | ); 85 | 86 | } 87 | 88 | export default Game; 89 | 90 | -------------------------------------------------------------------------------- /src/components/Game.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | display: grid; 7 | grid-template-columns: 8 | [left] auto 9 | [right] 180px; 10 | } 11 | 12 | .header { 13 | grid-column: left / right; 14 | font-size: 32px; 15 | color: dimgray; 16 | } 17 | 18 | .matrix { 19 | grid-column: left; 20 | width: auto; 21 | background-color: #eee; 22 | padding: 1px; 23 | display: table; 24 | /*float: left;*/ 25 | border: 1px black solid; 26 | } 27 | 28 | .controls { 29 | /*float: left;*/ 30 | display: grid; 31 | grid-template-rows: 32 | [next] 160px 33 | [level] 70px 34 | [score] 70px 35 | [buttons] 120px 36 | [info ] 60px; 37 | padding-left: 15px; 38 | padding-right: 15px; 39 | background-color: #cbf0cc; 40 | grid-column: right; 41 | grid-row: 2 / 2; 42 | background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); 43 | border: 1px black solid; 44 | border-left: 0; 45 | } 46 | 47 | .controlsNext { 48 | grid-row: next; 49 | margin-top: 5px; 50 | } 51 | 52 | .controlsScore { 53 | grid-row: score; 54 | } 55 | 56 | .controlsLevel { 57 | grid-row: level; 58 | } 59 | 60 | .controlsButtons { 61 | grid-row: buttons; 62 | } 63 | 64 | .controlsInfo { 65 | grid-row: info; 66 | } 67 | 68 | .btn{ 69 | border: none; 70 | color: white; 71 | padding: 12px 25px; 72 | text-align: center; 73 | text-decoration: none; 74 | display: inline-block; 75 | font-size: 16px; 76 | border-radius: 5px; 77 | margin-top: 10px; 78 | width: 100%; 79 | } 80 | 81 | .btnNew { 82 | background-color: #4CAF50; 83 | } 84 | 85 | .btnPause { 86 | background-color: #f44336; 87 | } 88 | 89 | .gameOver { 90 | z-index: 5; 91 | font-size: 24px; 92 | width: 180px; 93 | height: 80px; 94 | position: absolute; 95 | background-color: #f44336; 96 | color: #eeeeee; 97 | border-radius: 15px; 98 | top: 50%; 99 | left: 10%; 100 | align-content: center; 101 | -webkit-box-shadow: inset 0 10px 10px -10px black; 102 | -moz-box-shadow: inset 0 10px 10px -10px black; 103 | box-shadow: inset 0 10px 10px -10px black; 104 | display:table; 105 | vertical-align:middle; 106 | text-align:center; 107 | } 108 | -------------------------------------------------------------------------------- /src/components/Game.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import Game from './Game'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/Level.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | export const Level = (props) => { 3 | const [level, setLevel] = useState(props.level); 4 | 5 | useEffect(() => { 6 | setLevel(props.level); 7 | }, [props.level]) 8 | 9 | return ( 10 |
11 | Level 12 |

{level}

13 |
14 | ) 15 | } 16 | 17 | export default Level; 18 | -------------------------------------------------------------------------------- /src/components/Next.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | import styles from "./Next.module.css"; 3 | import {createMatrix} from "../helpers/Helpers"; 4 | 5 | export const Next = (props) => { 6 | const [next, setNext] = useState(props.next); 7 | 8 | useEffect(() => { 9 | setNext(props.next); 10 | }, [props.next]) 11 | 12 | const tetrominos = next?.tetrominos(); 13 | const matrix = tetrominos ? createMatrix(tetrominos) : ""; 14 | 15 | return ( 16 |
17 |
18 | Next 19 |
20 |
21 | {matrix} 22 |
23 |
24 | ) 25 | } 26 | 27 | export default Next; 28 | 29 | -------------------------------------------------------------------------------- /src/components/Next.module.css: -------------------------------------------------------------------------------- 1 | .matrix { 2 | display: table; 3 | background-color: #ffffff; 4 | padding: 1px; 5 | float: left; 6 | } 7 | 8 | .row { 9 | display: table-row; 10 | width: auto; 11 | clear: both; 12 | } -------------------------------------------------------------------------------- /src/components/Score.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | 3 | export const Score = (props) => { 4 | const [score, setScore] = useState(props.score); 5 | 6 | useEffect(() => { 7 | setScore(props.score); 8 | }, [props.score]) 9 | 10 | return ( 11 |
12 | Score 13 |

{score}

14 |
15 | ) 16 | } 17 | 18 | export default Score; 19 | -------------------------------------------------------------------------------- /src/helpers/Generators.js: -------------------------------------------------------------------------------- 1 | export function sequence(max) { 2 | var items = []; 3 | for(var i = 0; i < max; i ++){ 4 | items.push(i); 5 | } 6 | return items; 7 | } -------------------------------------------------------------------------------- /src/helpers/Helpers.js: -------------------------------------------------------------------------------- 1 | import Colors from "../tetris/Colors"; 2 | import Cell from "../components/Cell"; 3 | const tetromino2Class = (tetromino, styles) => { 4 | let clazz = styles.empty; 5 | if(tetromino == null){ 6 | return clazz; 7 | } else if(tetromino.isFilled()){ 8 | if(Colors.BLUE === tetromino.color){ 9 | return styles.blue; 10 | } else if(Colors.RED === tetromino.color){ 11 | return styles.red; 12 | } else if(Colors.GREEN === tetromino.color){ 13 | return styles.green; 14 | } else if(Colors.YELLOW === tetromino.color){ 15 | return styles.yellow; 16 | } else { 17 | return styles.empty; 18 | } 19 | } else { 20 | return styles.empty; 21 | } 22 | }; 23 | 24 | const createTetrominoRow = (tetraminos, rowIndex, styles) => { 25 | const cols = tetraminos.map( (tetramino, colIndex) => 26 | 27 | ); 28 | const rowComponent =
{cols}
; 29 | return rowComponent; 30 | }; 31 | 32 | const createMatrix = (state, styles) => { 33 | if(state == null){ 34 | return null; 35 | } 36 | const matrix = state.map( (tetraminos , rowIndex) => { 37 | return createTetrominoRow(tetraminos, rowIndex, state, styles); 38 | }); 39 | return matrix; 40 | } 41 | 42 | export {tetromino2Class, createMatrix} 43 | 44 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .title { 16 | font-size: 28px; 17 | color: dimgray; 18 | } 19 | 20 | .gameText { 21 | font-size: 18px; 22 | color: #0a07bc; 23 | margin: 10px; 24 | } 25 | 26 | .infoText { 27 | font-size: 12px; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import Game from './components/Game'; 5 | import {createTetris} from "./tetris/Tetris"; 6 | 7 | const tetris = createTetris(); 8 | tetris.start(); 9 | document.addEventListener("keydown", (event) => { 10 | const keyName = event.key; 11 | if (tetris.isRunning()) { 12 | if (keyName === "ArrowUp") { 13 | tetris.rotateCurrentPiece(); 14 | } else if (keyName === "ArrowDown") { 15 | tetris.tick(); 16 | } 17 | else if (keyName === "ArrowLeft") { 18 | tetris.moveLeft(); 19 | } else if (keyName === "ArrowRight") { 20 | tetris.moveRight(); 21 | } else if (keyName === " ") { 22 | tetris.pause(); 23 | } 24 | } else { 25 | if(keyName === " ") { 26 | if (tetris.isPaused()) { 27 | tetris.resume(); 28 | } else { 29 | tetris.start(); 30 | } 31 | } 32 | } 33 | }); 34 | ReactDOM.render( 35 | 36 | 37 | , 38 | document.getElementById('root') 39 | ); 40 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/tetris/Colors.js: -------------------------------------------------------------------------------- 1 | const Colors = { 2 | RED: 1, 3 | BLUE: 2, 4 | YELLOW: 3, 5 | GREEN: 4, 6 | }; 7 | 8 | const randomColor = () => { 9 | const rand = Math.floor(Math.random() * Object.keys(Colors).length); 10 | const randColorValue = Colors[Object.keys(Colors)[rand]]; 11 | return randColorValue; 12 | } 13 | 14 | export default Colors; 15 | export {randomColor} 16 | -------------------------------------------------------------------------------- /src/tetris/Piece.js: -------------------------------------------------------------------------------- 1 | import {Tetromino, TetrominoStates} from "./Tetromino"; 2 | import {randomColor} from "./Colors"; 3 | 4 | const Piece = ({x = 0, y = 0, color = randomColor(), state}) => { 5 | const FILLED = 1; 6 | let _x = x; 7 | let _y = y; 8 | let _color = color; 9 | let _state = state; 10 | let width = _state[0].length; 11 | let height = _state.length; 12 | const rotate = (matrixState) => { 13 | let newState = []; 14 | for(let i = 0; i < width; i ++){ 15 | newState.push([]); 16 | for(let j = 0; j < height; j ++){ 17 | newState[i].push(_state[height - j - 1][i]); 18 | } 19 | } 20 | 21 | const nextPieceState = Piece({x: _x, y: _y, color: _color, state: newState}); 22 | const leftMost = Math.min(...nextPieceState.matrixCoordinates().map(coordinate => coordinate.x)); 23 | let rightMost = Math.max(...nextPieceState.matrixCoordinates().map(coordinate => coordinate.x)); 24 | 25 | let newX = _x; 26 | let newY = _y; 27 | 28 | if(leftMost < 0){ 29 | newX = _x + Math.abs(leftMost); 30 | } 31 | 32 | if(rightMost >= matrixState.width - 1){ 33 | newX = _x - (rightMost - (matrixState.width - 1)); 34 | } 35 | 36 | return Piece({x: newX, y: newY, color: _color, state: newState}); 37 | 38 | }; 39 | 40 | const move = (x, y) => { 41 | return Piece(x, y, _color, _state); 42 | }; 43 | 44 | const moveInitCenter = (matrixWidth) => { 45 | const x = Math.floor((matrixWidth - width) / 2); 46 | const upMostPoint = Math.min(...matrixCoordinates().map(coordinate => coordinate.y)); 47 | const y = 0 - upMostPoint; 48 | return Piece({x: x, y: y, color: _color, state: _state}) 49 | } 50 | 51 | const moveLeft = () => { 52 | let newX = _x - 1; 53 | let newY = _y; 54 | 55 | const leftMost = Math.min(...matrixCoordinates().map(coordinate => coordinate.x)); 56 | //if piece out of matrix after rotation, auto align coordinates. 57 | if(leftMost <= 0){ 58 | newX = _x; 59 | } 60 | 61 | return Piece({x: newX, y: newY, color: _color, state:_state}); 62 | }; 63 | 64 | const moveRight = (matrixState) => { 65 | let newX = _x + 1; 66 | let newY = _y; 67 | 68 | let rightMost = Math.max(...matrixCoordinates().map(coordinate => coordinate.x)); 69 | if(rightMost >= matrixState.width - 1){ 70 | newX = _x; 71 | } 72 | return Piece({x: newX, y: newY, color: _color, state:_state}); 73 | }; 74 | 75 | const matrixCoordinates = () => { 76 | const coordinates = []; 77 | for(let i = 0; i < height; i ++){ 78 | for(let j = 0; j < width; j++){ 79 | if(_state[i][j] === FILLED){ 80 | const x = j + _x; 81 | const y = i + _y; 82 | //if piece is rotated at initial position (especially for I piece), 83 | // some cells can go to out of matrix 84 | if(x < 0 || y < 0){ 85 | continue; 86 | } 87 | coordinates.push({x: x, y: y}); 88 | } 89 | } 90 | } 91 | return coordinates; 92 | }; 93 | 94 | const moveDown = (gameState) => { 95 | const newY = _y + 1; 96 | const piece = Piece({x: _x, y: newY, color, state}); 97 | const matrixCoordinates = piece.matrixCoordinates(); 98 | const canMoveDown = matrixCoordinates.every(coordinate => { 99 | const canMove = (coordinate.y < gameState.height) && !gameState.isFilled(coordinate); 100 | return canMove; 101 | }); 102 | 103 | if(canMoveDown){ 104 | return piece; 105 | } else { 106 | const samePiece = Piece({x: _x, y: _y, color, state}); 107 | return samePiece; 108 | } 109 | }; 110 | 111 | const isOnSamePosition = (piece) =>{ 112 | return _x === piece.x && _y === piece.y; 113 | }; 114 | 115 | const toTetrominos = (state, color) => { 116 | const tetrominos = []; 117 | for(let i = 0; i < state.length; i ++){ 118 | const row = []; 119 | for(let j = 0; j < state[0].length; j ++){ 120 | let tetromino = Tetromino(); 121 | if(state[i][j] === FILLED){ 122 | tetromino = Tetromino(TetrominoStates.FILLED, color); 123 | } 124 | row.push(tetromino); 125 | } 126 | tetrominos.push(row); 127 | } 128 | return tetrominos; 129 | }; 130 | 131 | const tetrominos = () => { 132 | return toTetrominos(_state, _color); 133 | }; 134 | 135 | return { 136 | rotate, 137 | move, 138 | moveLeft, 139 | moveRight, 140 | moveDown, 141 | x: _x, 142 | y: _y, 143 | isOnSamePosition, 144 | matrixCoordinates, 145 | color: _color, 146 | tetrominos, 147 | moveInitCenter 148 | }; 149 | } 150 | 151 | const I_Piece = () => { 152 | let initalState = [ 153 | [0,0,0,0], 154 | [1,1,1,1], 155 | [0,0,0,0], 156 | [0,0,0,0], 157 | ]; 158 | 159 | const piece = Piece({state: initalState}); 160 | return piece; 161 | } 162 | 163 | const O_Piece = () => { 164 | let initalState = [ 165 | [1,1], 166 | [1,1], 167 | 168 | ]; 169 | 170 | const piece = Piece({state: initalState}); 171 | return piece; 172 | } 173 | 174 | const Z_Piece = () => { 175 | let initalState = [ 176 | [1,1,0], 177 | [0,1,1], 178 | [0,0,0] 179 | ]; 180 | 181 | const piece = Piece({state: initalState}); 182 | return piece; 183 | } 184 | 185 | const S_Piece = () => { 186 | let initalState = [ 187 | [0,1,1], 188 | [1,1,0], 189 | [0,0,0] 190 | ]; 191 | 192 | const piece = Piece({state: initalState}); 193 | return piece; 194 | } 195 | 196 | const J_Piece = () => { 197 | let initalState = [ 198 | [1,0,0], 199 | [1,1,1], 200 | [0,0,0], 201 | 202 | ]; 203 | 204 | const piece = Piece({state: initalState}); 205 | return piece; 206 | } 207 | 208 | const L_Piece = () => { 209 | let initalState = [ 210 | [0,0,1], 211 | [1,1,1], 212 | [0,0,0], 213 | 214 | ]; 215 | 216 | const piece = Piece({state: initalState}); 217 | return piece; 218 | } 219 | 220 | const M_Piece = () => { 221 | let initalState = [ 222 | [1,1,1], 223 | [0,1,0], 224 | [0,0,0], 225 | 226 | ]; 227 | 228 | const piece = Piece({state: initalState}); 229 | return piece; 230 | } 231 | 232 | const X_Piece = () => { 233 | let initalState = [ 234 | [1,0,1], 235 | [0,1,0], 236 | [1,0,1], 237 | 238 | ]; 239 | 240 | const piece = Piece({state: initalState}); 241 | return piece; 242 | } 243 | 244 | 245 | const empty_Piece = () => { 246 | let initalState = [ 247 | [0,0,0], 248 | [0,0,0], 249 | [0,0,0], 250 | 251 | ]; 252 | 253 | const piece = Piece({state: initalState}); 254 | return piece; 255 | } 256 | 257 | const randomPiece = () => { 258 | const i = I_Piece(); 259 | const o = O_Piece(); 260 | const z = Z_Piece(); 261 | const s = S_Piece(); 262 | const j = J_Piece(); 263 | const l = L_Piece(); 264 | const m = M_Piece(); 265 | 266 | //eslint-disable-next-line no-unused-vars 267 | const x = X_Piece(); 268 | const pieces = [ 269 | i, 270 | o, 271 | z, 272 | s, 273 | j, 274 | l, 275 | m, 276 | //x --> experimental X piece. 277 | ]; 278 | 279 | const rnd = Math.floor(Math.random() * pieces.length); 280 | const randomPiece = pieces[rnd]; 281 | return randomPiece; 282 | } 283 | 284 | const emtpyPiece = () => { 285 | return empty_Piece(); 286 | } 287 | 288 | export {randomPiece, emtpyPiece}; -------------------------------------------------------------------------------- /src/tetris/State.js: -------------------------------------------------------------------------------- 1 | import {Tetromino, TetrominoStates} from "./Tetromino"; 2 | import {randomPiece} from "./Piece"; 3 | 4 | const MAX_ROW_CLEAR_PER_LEVEL = 10; 5 | 6 | const createState = (height, width) => { 7 | let _height = height; 8 | let _width = width; 9 | let paused = false; 10 | let started = false; 11 | let gameOver = false; 12 | let _level = 1; 13 | let _score = 0; 14 | let _nextPiece = null; 15 | let _currentPiece = null; 16 | let totalNumberOfClearedRows = 0; 17 | let onLevelChangeAction = null; 18 | 19 | const isRunning = () => { 20 | return started && !paused; 21 | }; 22 | 23 | const isStarted = () => { 24 | return started; 25 | }; 26 | 27 | const isGameOver = () => { 28 | return gameOver; 29 | }; 30 | 31 | const isPaused = () => { 32 | return paused; 33 | }; 34 | 35 | const initMatrix = (height, width) => { 36 | let _matrix = []; 37 | for (let i = 0; i < height; i++) { 38 | _matrix[i] = []; 39 | for (let j = 0; j < width; j++) { 40 | _matrix[i][j] = Tetromino(); 41 | } 42 | } 43 | return _matrix; 44 | }; 45 | 46 | let matrix = initMatrix(height, width); 47 | let _visibleMatrix = [...matrix]; 48 | 49 | 50 | const start = () => { 51 | paused = false; 52 | started = true; 53 | gameOver = false; 54 | _level = 1; 55 | _score = 0; 56 | matrix = initMatrix(_height, _width); 57 | sendNextPiece(); 58 | _visibleMatrix = createVisibleMatrix(matrix); 59 | }; 60 | 61 | const pause = () => { 62 | paused = true; 63 | } 64 | 65 | const resume = () => { 66 | paused = false; 67 | } 68 | 69 | const sendNextPiece = () => { 70 | if(_nextPiece == null){ 71 | _nextPiece = randomPiece(); 72 | } 73 | _currentPiece = _nextPiece; 74 | _nextPiece = randomPiece(); 75 | _currentPiece = _currentPiece.moveInitCenter(width); 76 | } 77 | 78 | const isFilled = (coordinate) => { 79 | return matrix[coordinate.y][coordinate.x].isFilled(); 80 | }; 81 | 82 | const increaseLevel = () => { 83 | if (totalNumberOfClearedRows > MAX_ROW_CLEAR_PER_LEVEL) { 84 | _level++; 85 | totalNumberOfClearedRows = 0; 86 | notifyLevelChangeListener(_level); 87 | } 88 | } 89 | 90 | const moveCurrentPieceDown = () => { 91 | if(_currentPiece == null){ 92 | return; 93 | } 94 | 95 | if(overlaps(_currentPiece)){ 96 | setGameOver(); 97 | return; 98 | } 99 | const newPosition = _currentPiece.moveDown({isFilled, height}); 100 | //piece cannot move down. 101 | //merge piece into matrix and start a new piece. 102 | if(newPosition.isOnSamePosition(_currentPiece)){ 103 | mergePieceIntoMatrix(newPosition); 104 | sendNextPiece(); 105 | } else { 106 | _currentPiece = newPosition; 107 | } 108 | const numberOfClearedRows = clearRows(); 109 | totalNumberOfClearedRows+=numberOfClearedRows; 110 | increaseLevel(); 111 | _score = calculateScore(_score, numberOfClearedRows, _level); 112 | _visibleMatrix = createVisibleMatrix(matrix); 113 | }; 114 | 115 | const calculateScore = (_score, numberOfClearedRows, _level) => { 116 | return _score + Math.pow(numberOfClearedRows, 2) * _level 117 | } 118 | 119 | const createVisibleMatrix = (matrix) => { 120 | const visibleMatrix = matrix.map(row => row.slice()); 121 | if(_currentPiece != null){ 122 | _currentPiece.matrixCoordinates().forEach(coordinate => { 123 | const tetromino = Tetromino(TetrominoStates.FILLED, _currentPiece.color); 124 | visibleMatrix[coordinate.y][coordinate.x] = tetromino; 125 | }) 126 | } 127 | return visibleMatrix; 128 | }; 129 | 130 | const overlaps = (piece) => { 131 | const overlap = piece.matrixCoordinates().some(coordinate => { 132 | return matrix[coordinate.y][coordinate.x].isFilled() 133 | }); 134 | 135 | return overlap; 136 | }; 137 | 138 | const setGameOver = () => { 139 | started = false; 140 | paused = false; 141 | gameOver = true; 142 | }; 143 | 144 | const mergePieceIntoMatrix = (piece) => { 145 | piece.matrixCoordinates().forEach( coordinate => { 146 | matrix[coordinate.y][coordinate.x] = Tetromino(TetrominoStates.FILLED, piece.color) 147 | }); 148 | }; 149 | 150 | const rotateCurrentPiece = () => { 151 | move(() => _currentPiece.rotate({width: _width})); 152 | } 153 | 154 | const moveLeft = () => { 155 | move(() => _currentPiece.moveLeft()); 156 | } 157 | 158 | const moveRight = () => { 159 | move(() => _currentPiece.moveRight({width: _width})); 160 | } 161 | 162 | const move = (action) => { 163 | if(_currentPiece == null){ 164 | return; 165 | } 166 | let nextPosition = action(); 167 | if(nextPosition != null && !overlaps(nextPosition)){ 168 | _currentPiece = nextPosition; 169 | _visibleMatrix = createVisibleMatrix(matrix); 170 | } 171 | } 172 | 173 | const clearRows = () => { 174 | let numberOfClearedRows = 0; 175 | for(let i = height - 1; i >= 0 ; ){ 176 | const allEmpty = matrix[i].every(cell => cell.isEmpty()); 177 | if(allEmpty){ 178 | break; 179 | } 180 | 181 | const allFilled = matrix[i].every(cell => cell.isFilled()); 182 | if(allFilled){ 183 | matrix.splice(i, 1); 184 | const emtyRow = []; 185 | for(let j = 0; j < width; j++){ 186 | emtyRow.push(Tetromino()); 187 | } 188 | matrix.unshift(emtyRow); 189 | numberOfClearedRows++; 190 | } else { 191 | i--; 192 | } 193 | } 194 | return numberOfClearedRows; 195 | }; 196 | 197 | const visibleMatrix = () => { 198 | return _visibleMatrix; 199 | } 200 | 201 | const score = () => { 202 | return _score; 203 | }; 204 | 205 | const level = () => { 206 | return _level; 207 | }; 208 | 209 | const nextPiece = () => { 210 | return _nextPiece; 211 | }; 212 | 213 | const onLevelChange = (action) => { 214 | onLevelChangeAction = action; 215 | }; 216 | 217 | const notifyLevelChangeListener = (level) => { 218 | if(onLevelChangeAction){ 219 | onLevelChangeAction(level); 220 | } 221 | }; 222 | 223 | return { 224 | isRunning, 225 | isStarted, 226 | isPaused, 227 | start, 228 | pause, 229 | resume, 230 | level, 231 | score, 232 | isFilled, 233 | isGameOver, 234 | moveCurrentPieceDown, 235 | visibleMatrix, 236 | rotateCurrentPiece, 237 | moveLeft, 238 | moveRight, 239 | nextPiece, 240 | onLevelChange 241 | }; 242 | }; 243 | 244 | export {createState}; 245 | -------------------------------------------------------------------------------- /src/tetris/Tetris.js: -------------------------------------------------------------------------------- 1 | import {createState} from "./State.js"; 2 | 3 | 4 | const createTetris = ({height, width} = {height: 20, width:10}) => { 5 | const _height = height; 6 | const _width = width; 7 | const state = createState(_height, _width); 8 | let timer = null; 9 | const stateChangeListeners = []; 10 | 11 | state.onLevelChange((level) => startTimer(timer, calculateSpeedForCurrentLevel(level))) 12 | 13 | const startTimer = (timer, delay) => { 14 | if (timer != null) { 15 | clearInterval(timer); 16 | } 17 | 18 | const newTimer = setInterval(tick, delay) 19 | return newTimer; 20 | }; 21 | 22 | const stopTimer = (timer) => { 23 | if (timer != null) { 24 | clearInterval(timer); 25 | } 26 | }; 27 | 28 | const calculateSpeedForCurrentLevel = (level) => { 29 | return Math.floor(900 * (Math.pow(0.9, level - 1))); 30 | }; 31 | 32 | 33 | //eslint-disable-next-line no-unused-vars 34 | const toString = () => { 35 | let str = state.visibleMatrix().map(row => { 36 | return row.map(tetromino => { 37 | if (tetromino.isFilled()) { 38 | return tetromino.color; 39 | } else { 40 | return tetromino.state; 41 | } 42 | }).join(","); 43 | }).join("\n"); 44 | 45 | return str; 46 | }; 47 | 48 | const tick = () => { 49 | if (!state.isRunning()) { 50 | return; 51 | } 52 | 53 | state.moveCurrentPieceDown(); 54 | if(state.isGameOver()){ 55 | stopTimer(timer); 56 | } 57 | notifyOnStateChangeListeners(); 58 | }; 59 | 60 | const notifyOnStateChangeListeners = () => { 61 | stateChangeListeners.forEach(fn => fn({...state})); 62 | }; 63 | 64 | const resume = () => { 65 | state.resume(); 66 | notifyOnStateChangeListeners(); 67 | timer = startTimer(timer, calculateSpeedForCurrentLevel(state.level())); 68 | 69 | }; 70 | 71 | const start = () => { 72 | if (state.isStarted()) { 73 | return; 74 | } 75 | state.start(); 76 | notifyOnStateChangeListeners(); 77 | timer = startTimer(timer, calculateSpeedForCurrentLevel(state.level())); 78 | }; 79 | 80 | const pause = () => { 81 | if (state.isPaused()) { 82 | return; 83 | } 84 | if (timer != null) { 85 | clearInterval(timer); 86 | } 87 | state.pause(); 88 | stopTimer(timer); 89 | notifyOnStateChangeListeners(); 90 | }; 91 | 92 | const isPaused = () => { 93 | return state.isPaused(); 94 | } 95 | 96 | const moveLeft = () => { 97 | state.moveLeft(); 98 | notifyOnStateChangeListeners(); 99 | }; 100 | 101 | const moveRight = () => { 102 | state.moveRight(); 103 | notifyOnStateChangeListeners(); 104 | } 105 | 106 | const rotateCurrentPiece = () => { 107 | state.rotateCurrentPiece(); 108 | notifyOnStateChangeListeners(); 109 | } 110 | 111 | 112 | 113 | return { 114 | onStateChange(fn) { 115 | stateChangeListeners.push(fn); 116 | }, 117 | 118 | isGameOver() { 119 | return state.gameOver; 120 | }, 121 | 122 | isRunning() { 123 | return state.isRunning(); 124 | }, 125 | 126 | isStarted() { 127 | return state.isStarted(); 128 | }, 129 | state, 130 | rotateCurrentPiece, 131 | moveLeft, 132 | moveRight, 133 | start, 134 | pause, 135 | isPaused, 136 | resume, 137 | tick, 138 | height: _height, 139 | width: _width 140 | }; 141 | 142 | }; 143 | 144 | export {createTetris} 145 | -------------------------------------------------------------------------------- /src/tetris/Tetromino.js: -------------------------------------------------------------------------------- 1 | const TetrominoStates = { 2 | EMPTY: 0, 3 | FILLED: 1, 4 | SHADOW: 2 5 | }; 6 | const Tetromino = (state, color) => { 7 | 8 | let _state = state == null ? TetrominoStates.EMPTY : state; 9 | let _color = color; 10 | 11 | return { 12 | isEmpty() { 13 | return _state === TetrominoStates.EMPTY || _state === TetrominoStates.SHADOW; 14 | }, 15 | isShadow() { 16 | return _state === TetrominoStates.SHADOW; 17 | }, 18 | isFilled() { 19 | return _state === TetrominoStates.FILLED; 20 | }, 21 | color: _color, 22 | state: _state 23 | 24 | } 25 | } 26 | 27 | export {Tetromino, TetrominoStates}; 28 | -------------------------------------------------------------------------------- /src/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.css$/i, 6 | use: ["style-loader", "css-loader"], 7 | options: { 8 | modules: true, 9 | }, 10 | }, 11 | ], 12 | }, 13 | }; --------------------------------------------------------------------------------