Initial commit: 2048 game with Knative and Kourier deployment

- Complete 2048 game implementation with responsive design
- Knative Serving manifests for dev/staging/prod environments
- Scale-to-zero configuration with environment-specific settings
- Custom domain mapping for wa.darknex.us subdomains
- GitHub Actions workflows for CI/CD
- Docker container with nginx and health checks
- Setup scripts for Knative and Kourier installation
- GHCR integration for container registry
This commit is contained in:
greg
2025-06-30 20:43:19 -07:00
commit c3b227b7d7
26 changed files with 2244 additions and 0 deletions

76
.github/workflows/deploy-dev.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Deploy to Development
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ghndrx/k8s-game-2048
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Configure kubectl
run: |
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Update image in manifests
run: |
sed -i "s|ghcr.io/ghndrx/k8s-game-2048:latest|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}|g" manifests/dev/service.yml
- name: Deploy to development
run: |
export KUBECONFIG=kubeconfig
kubectl apply -f manifests/dev/
- name: Wait for deployment
run: |
export KUBECONFIG=kubeconfig
kubectl wait --for=condition=Ready ksvc/game-2048-dev -n game-2048-dev --timeout=300s
- name: Get service URL
run: |
export KUBECONFIG=kubeconfig
kubectl get ksvc game-2048-dev -n game-2048-dev -o jsonpath='{.status.url}'

103
.github/workflows/deploy-prod.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Deploy to Production
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Tag to deploy'
required: true
default: 'latest'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ghndrx/k8s-game-2048
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.event.inputs.tag }}
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Configure kubectl
run: |
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Update image in manifests
run: |
TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}"
sed -i "s|ghcr.io/ghndrx/k8s-game-2048:v1.0.0|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}|g" manifests/prod/service.yml
- name: Deploy to production with blue-green strategy
run: |
export KUBECONFIG=kubeconfig
# Deploy new revision with 0% traffic
kubectl apply -f manifests/prod/
# Wait for new revision to be ready
kubectl wait --for=condition=Ready ksvc/game-2048-prod -n game-2048-prod --timeout=300s
# Get the latest revision name
LATEST_REVISION=$(kubectl get ksvc game-2048-prod -n game-2048-prod -o jsonpath='{.status.latestReadyRevisionName}')
# Gradually shift traffic (10%, 50%, 100%)
kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"revisionName":"'$LATEST_REVISION'","percent":10},{"latestRevision":false,"percent":90}]}}'
sleep 60
kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"revisionName":"'$LATEST_REVISION'","percent":50},{"latestRevision":false,"percent":50}]}}'
sleep 60
kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"latestRevision":true,"percent":100}]}}'
- name: Run production health checks
run: |
# Wait for traffic to stabilize
sleep 60
# Test the production URL
curl -f https://2048.wa.darknex.us/ || exit 1
# Additional health checks can be added here
- name: Get service URL
run: |
export KUBECONFIG=kubeconfig
kubectl get ksvc game-2048-prod -n game-2048-prod -o jsonpath='{.status.url}'

81
.github/workflows/deploy-staging.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Deploy to Staging
on:
push:
branches: [ main ]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ghndrx/k8s-game-2048
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=staging-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Configure kubectl
run: |
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Update image in manifests
run: |
sed -i "s|ghcr.io/ghndrx/k8s-game-2048:staging|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ github.sha }}|g" manifests/staging/service.yml
- name: Deploy to staging
run: |
export KUBECONFIG=kubeconfig
kubectl apply -f manifests/staging/
- name: Wait for deployment
run: |
export KUBECONFIG=kubeconfig
kubectl wait --for=condition=Ready ksvc/game-2048-staging -n game-2048-staging --timeout=300s
- name: Run smoke tests
run: |
# Wait a bit for the service to be fully ready
sleep 30
# Test the staging URL
curl -f https://2048-staging.wa.darknex.us/ || exit 1
- name: Get service URL
run: |
export KUBECONFIG=kubeconfig
kubectl get ksvc game-2048-staging -n game-2048-staging -o jsonpath='{.status.url}'

50
.gitignore vendored Normal file
View File

@@ -0,0 +1,50 @@
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docker
.dockerignore
# Kubernetes
*.bak
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# Build outputs
dist/
build/

159
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,159 @@
# Contributing to K8s Game 2048
Thank you for considering contributing to this project! This guide will help you get started.
## Development Setup
1. **Clone the repository**
```bash
git clone https://github.com/your-username/k8s-game-2048.git
cd k8s-game-2048
```
2. **Local Development**
```bash
# Start local development server
npm start
# Or with Python
python3 -m http.server 8080 --directory src
```
3. **Build Docker Image**
```bash
npm run build
# Or
docker build -t k8s-game-2048 .
```
## Git Workflow
We use a GitFlow-inspired workflow:
- **`main`** - Production-ready code, deployed to staging automatically
- **`develop`** - Development branch, deployed to dev environment automatically
- **`feature/*`** - Feature branches, create PR to develop
- **`hotfix/*`** - Hotfix branches, create PR to main
- **`release/*`** - Release branches for production deployment
### Branch Protection Rules
- **`main`**: Requires PR review, all checks must pass
- **`develop`**: Requires PR review, all checks must pass
## Deployment Environments
| Environment | Branch | Domain | Auto-Deploy |
|-------------|--------|---------|------------|
| Development | `develop` | `2048-dev.wa.darknex.us` | ✅ |
| Staging | `main` | `2048-staging.wa.darknex.us` | ✅ |
| Production | `tags` | `2048.wa.darknex.us` | Manual |
## Making Changes
### For New Features
1. Create a feature branch from `develop`:
```bash
git checkout develop
git pull origin develop
git checkout -b feature/your-feature-name
```
2. Make your changes and commit:
```bash
git add .
git commit -m "feat: add your feature description"
```
3. Push and create a PR to `develop`:
```bash
git push origin feature/your-feature-name
```
### For Bug Fixes
1. Create a hotfix branch from `main`:
```bash
git checkout main
git pull origin main
git checkout -b hotfix/fix-description
```
2. Make your changes and create PR to `main`
## Commit Convention
We use [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` - New features
- `fix:` - Bug fixes
- `docs:` - Documentation changes
- `style:` - Code style changes (formatting, etc.)
- `refactor:` - Code refactoring
- `test:` - Adding or updating tests
- `chore:` - Maintenance tasks
## Testing
### Local Testing
```bash
# Test the game locally
npm start
open http://localhost:8080
```
### Kubernetes Testing
```bash
# Deploy to development environment
kubectl apply -f manifests/dev/
# Check deployment status
kubectl get ksvc -n game-2048-dev
# Test the deployed service
curl -f https://2048-dev.wa.darknex.us/
```
## Code Style
- Use 2 spaces for indentation
- Use meaningful variable and function names
- Add comments for complex logic
- Keep functions small and focused
## Pull Request Process
1. **Title**: Use conventional commit format
2. **Description**:
- What changes were made?
- Why were they made?
- How to test the changes?
3. **Testing**: Ensure all environments work correctly
4. **Documentation**: Update README if needed
## Release Process
1. Create a release branch from `main`:
```bash
git checkout main
git pull origin main
git checkout -b release/v1.1.0
```
2. Update version in `package.json`
3. Create PR to `main`
4. After merge, create a GitHub release with tag
5. Production deployment will trigger automatically
## Getting Help
- Open an issue for bugs or feature requests
- Start a discussion for questions
- Check existing issues before creating new ones
## Code of Conduct
Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM nginx:alpine
# Copy the 2048 game files
COPY src/ /usr/share/nginx/html/
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 8080 (Knative standard)
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/ || exit 1
# Run nginx
CMD ["nginx", "-g", "daemon off;"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 K8s Game 2048
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

121
README.md Normal file
View File

@@ -0,0 +1,121 @@
# K8s Game 2048
A Kubernetes deployment of the classic 2048 game using Knative Serving with Kourier ingress controller.
## Features
- **Knative Serving**: Serverless deployment with scale-to-zero capability
- **Kourier Gateway**: Lightweight ingress controller for Knative
- **Multi-environment**: Development, Staging, and Production deployments
- **Custom Domains**: Environment-specific domain configuration
- **GitOps Workflow**: Complete CI/CD pipeline with GitHub Actions
## Environments
- **Development**: `2048-dev.wa.darknex.us`
- **Staging**: `2048-staging.wa.darknex.us`
- **Production**: `2048.wa.darknex.us`
## Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Kourier │ │ Knative │ │ 2048 Game │
│ Gateway │───▶│ Service │───▶│ Container │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## Quick Start
### Prerequisites
- Kubernetes cluster (1.21+)
- Knative Serving installed
- Kourier as the networking layer
- kubectl configured
- Domain DNS configured to point to Kourier LoadBalancer
### Installation
1. Clone the repository:
```bash
git clone https://github.com/ghndrx/k8s-game-2048.git
cd k8s-game-2048
```
2. Deploy to development:
```bash
kubectl apply -f manifests/dev/
```
3. Deploy to staging:
```bash
kubectl apply -f manifests/staging/
```
4. Deploy to production:
```bash
kubectl apply -f manifests/prod/
```
## Project Structure
```
k8s-game-2048/
├── README.md
├── Dockerfile
├── .github/
│ └── workflows/
│ ├── deploy-dev.yml
│ ├── deploy-staging.yml
│ └── deploy-prod.yml
├── manifests/
│ ├── dev/
│ │ ├── namespace.yml
│ │ ├── service.yml
│ │ └── domain-mapping.yml
│ ├── staging/
│ │ ├── namespace.yml
│ │ ├── service.yml
│ │ └── domain-mapping.yml
│ └── prod/
│ ├── namespace.yml
│ ├── service.yml
│ └── domain-mapping.yml
├── scripts/
│ ├── setup-knative.sh
│ ├── setup-kourier.sh
│ └── deploy.sh
└── src/
└── (2048 game files)
```
## Deployment
The application uses Knative Serving with the following features:
- **Scale to Zero**: Automatically scales down to 0 when not in use
- **Auto-scaling**: Scales up based on incoming requests
- **Blue-Green Deployments**: Safe deployment strategy with traffic splitting
- **Custom Domains**: Environment-specific domain mapping
## Monitoring
Each environment includes:
- Knative Service status monitoring
- Request metrics via Knative
- Custom domain health checks
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

236
docs/SETUP.md Normal file
View File

@@ -0,0 +1,236 @@
# Knative & Kourier Setup Guide
This guide will help you set up Knative Serving with Kourier networking layer on your Kubernetes cluster.
## Prerequisites
- Kubernetes cluster (v1.21+)
- kubectl configured and working
- Cluster admin permissions
- LoadBalancer support (cloud provider or MetalLB)
## Quick Setup
Run the provided scripts in order:
```bash
# 1. Install Knative Serving
./scripts/setup-knative.sh
# 2. Install Kourier networking layer
./scripts/setup-kourier.sh
```
## Manual Setup
If you prefer to install manually:
### 1. Install Knative Serving
```bash
# Install CRDs
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-crds.yaml
# Install core components
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-core.yaml
# Install HPA autoscaler
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-hpa.yaml
```
### 2. Install Kourier
```bash
# Install Kourier
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.12.0/kourier.yaml
# Configure Knative to use Kourier
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
```
### 3. Configure Domain
```bash
# Set your custom domain
kubectl patch configmap/config-domain \
--namespace knative-serving \
--type merge \
--patch '{"data":{"wa.darknex.us":""}}'
```
### 4. Set up TLS (Optional but Recommended)
```bash
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# Install Knative cert-manager integration
kubectl apply -f https://github.com/knative/net-certmanager/releases/download/knative-v1.12.0/release.yaml
# Create Let's Encrypt ClusterIssuer
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@darknex.us
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: kourier.ingress.networking.knative.dev
EOF
# Configure Knative to use cert-manager
kubectl patch configmap/config-certmanager \
--namespace knative-serving \
--type merge \
--patch '{"data":{"issuerRef":"kind: ClusterIssuer\nname: letsencrypt-prod"}}'
# Enable auto-TLS
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"autoTLS":"Enabled","httpProtocol":"Redirected"}}'
```
## DNS Configuration
After installation, configure your DNS to point to the Kourier LoadBalancer:
1. **Get the LoadBalancer IP**:
```bash
kubectl get svc kourier -n kourier-system
```
2. **Create DNS records**:
```
2048-dev.wa.darknex.us -> LoadBalancer IP
2048-staging.wa.darknex.us -> LoadBalancer IP
2048.wa.darknex.us -> LoadBalancer IP
*.wa.darknex.us -> LoadBalancer IP (wildcard)
```
## Verification
Test your setup:
```bash
# Check Knative Serving
kubectl get pods -n knative-serving
# Check Kourier
kubectl get pods -n kourier-system
# Check cert-manager (if installed)
kubectl get pods -n cert-manager
# Deploy a test service
kubectl apply -f manifests/dev/
# Check service status
kubectl get ksvc -n game-2048-dev
```
## Troubleshooting
### Common Issues
1. **Pods stuck in Pending**:
- Check node resources: `kubectl describe nodes`
- Check PVC status: `kubectl get pvc -A`
2. **LoadBalancer IP not assigned**:
- Ensure your cluster supports LoadBalancer services
- For local clusters, consider using MetalLB
3. **TLS certificates not issued**:
- Check cert-manager logs: `kubectl logs -n cert-manager -l app=cert-manager`
- Verify DNS propagation: `dig 2048-dev.wa.darknex.us`
4. **Service not accessible**:
- Check Kourier gateway logs: `kubectl logs -n kourier-system -l app=3scale-kourier-gateway`
- Verify domain mapping: `kubectl get domainmapping -A`
### Useful Commands
```bash
# Check Knative service status
kubectl get ksvc -A
# Check revisions
kubectl get rev -A
# Check domain mappings
kubectl get domainmapping -A
# Check Kourier configuration
kubectl get svc kourier -n kourier-system -o yaml
# Check Knative configuration
kubectl get cm -n knative-serving
# Debug service logs
kubectl logs -n <namespace> -l serving.knative.dev/service=<service-name>
```
## Advanced Configuration
### Custom Autoscaling
```yaml
# Add to service annotations
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "100"
autoscaling.knative.dev/target: "70"
autoscaling.knative.dev/scaleDownDelay: "30s"
autoscaling.knative.dev/window: "60s"
```
### Traffic Splitting
```yaml
# In Knative Service spec
traffic:
- percent: 90
revisionName: myapp-00001
- percent: 10
revisionName: myapp-00002
```
### Custom Resource Limits
```yaml
# In container spec
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
```
## Monitoring
Consider installing these additional tools:
- **Knative Monitoring**: `kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/monitoring.yaml`
- **Prometheus**: For metrics collection
- **Grafana**: For visualization
- **Jaeger**: For distributed tracing
## Next Steps
1. Deploy the 2048 game: `kubectl apply -f manifests/dev/`
2. Set up monitoring and alerting
3. Configure backup and disaster recovery
4. Implement proper RBAC policies
5. Set up GitOps with ArgoCD or Flux

View File

@@ -0,0 +1,13 @@
apiVersion: serving.knative.dev/v1alpha1
kind: DomainMapping
metadata:
name: 2048-dev.wa.darknex.us
namespace: game-2048-dev
labels:
app: game-2048
environment: development
spec:
ref:
name: game-2048-dev
kind: Service
apiVersion: serving.knative.dev/v1

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: game-2048-dev
labels:
environment: development
app: game-2048

60
manifests/dev/service.yml Normal file
View File

@@ -0,0 +1,60 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: game-2048-dev
namespace: game-2048-dev
labels:
app: game-2048
environment: development
annotations:
# Scale to zero configuration
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "10"
# Scale down to zero after 30 seconds of no traffic
autoscaling.knative.dev/scaleDownDelay: "30s"
# Target concurrency per pod
autoscaling.knative.dev/target: "100"
spec:
template:
metadata:
labels:
app: game-2048
environment: development
annotations:
# Scale to zero configuration
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "10"
autoscaling.knative.dev/scaleDownDelay: "30s"
autoscaling.knative.dev/target: "100"
spec:
containers:
- name: game-2048
image: ghcr.io/ghndrx/k8s-game-2048:latest
ports:
- containerPort: 8080
protocol: TCP
env:
- name: ENVIRONMENT
value: "development"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
traffic:
- percent: 100
latestRevision: true

View File

@@ -0,0 +1,13 @@
apiVersion: serving.knative.dev/v1alpha1
kind: DomainMapping
metadata:
name: 2048.wa.darknex.us
namespace: game-2048-prod
labels:
app: game-2048
environment: production
spec:
ref:
name: game-2048-prod
kind: Service
apiVersion: serving.knative.dev/v1

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: game-2048-prod
labels:
environment: production
app: game-2048

View File

@@ -0,0 +1,60 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: game-2048-prod
namespace: game-2048-prod
labels:
app: game-2048
environment: production
annotations:
# Scale to zero configuration
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "50"
# Scale down to zero after 5 minutes of no traffic (longer for production)
autoscaling.knative.dev/scaleDownDelay: "300s"
# Target concurrency per pod
autoscaling.knative.dev/target: "100"
spec:
template:
metadata:
labels:
app: game-2048
environment: production
annotations:
# Scale to zero configuration
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "50"
autoscaling.knative.dev/scaleDownDelay: "300s"
autoscaling.knative.dev/target: "100"
spec:
containers:
- name: game-2048
image: ghcr.io/ghndrx/k8s-game-2048:v1.0.0
ports:
- containerPort: 8080
protocol: TCP
env:
- name: ENVIRONMENT
value: "production"
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 1Gi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
traffic:
- percent: 100
latestRevision: true

View File

@@ -0,0 +1,13 @@
apiVersion: serving.knative.dev/v1alpha1
kind: DomainMapping
metadata:
name: 2048-staging.wa.darknex.us
namespace: game-2048-staging
labels:
app: game-2048
environment: staging
spec:
ref:
name: game-2048-staging
kind: Service
apiVersion: serving.knative.dev/v1

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: game-2048-staging
labels:
environment: staging
app: game-2048

View File

@@ -0,0 +1,60 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: game-2048-staging
namespace: game-2048-staging
labels:
app: game-2048
environment: staging
annotations:
# Scale to zero configuration
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "20"
# Scale down to zero after 60 seconds of no traffic (longer for staging)
autoscaling.knative.dev/scaleDownDelay: "60s"
# Target concurrency per pod
autoscaling.knative.dev/target: "100"
spec:
template:
metadata:
labels:
app: game-2048
environment: staging
annotations:
# Scale to zero configuration
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "20"
autoscaling.knative.dev/scaleDownDelay: "60s"
autoscaling.knative.dev/target: "100"
spec:
containers:
- name: game-2048
image: ghcr.io/ghndrx/k8s-game-2048:staging
ports:
- containerPort: 8080
protocol: TCP
env:
- name: ENVIRONMENT
value: "staging"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
traffic:
- percent: 100
latestRevision: true

38
nginx.conf Normal file
View File

@@ -0,0 +1,38 @@
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Main location
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "k8s-game-2048",
"version": "1.0.0",
"description": "2048 game deployed on Kubernetes using Knative Serving with Kourier",
"main": "src/index.html",
"scripts": {
"start": "python3 -m http.server 8080 --directory src",
"build": "docker build -t k8s-game-2048 .",
"deploy:dev": "./scripts/deploy.sh dev",
"deploy:staging": "./scripts/deploy.sh staging",
"deploy:prod": "./scripts/deploy.sh prod",
"setup:knative": "./scripts/setup-knative.sh",
"setup:kourier": "./scripts/setup-kourier.sh"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ghndrx/k8s-game-2048.git"
},
"keywords": [
"2048",
"game",
"kubernetes",
"knative",
"kourier",
"serverless",
"scale-to-zero"
],
"author": "Your Name",
"license": "MIT",
"bugs": {
"url": "https://github.com/ghndrx/k8s-game-2048/issues"
},
"homepage": "https://github.com/ghndrx/k8s-game-2048#readme",
"devDependencies": {},
"dependencies": {}
}

87
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Deployment script for 2048 game environments
# Usage: ./deploy.sh [dev|staging|prod] [image-tag]
set -e
ENVIRONMENT=${1:-dev}
IMAGE_TAG=${2:-latest}
REGISTRY="ghcr.io/ghndrx/k8s-game-2048"
echo "🚀 Deploying 2048 game to $ENVIRONMENT environment..."
# Validate environment
case $ENVIRONMENT in
dev|staging|prod)
echo "✅ Valid environment: $ENVIRONMENT"
;;
*)
echo "❌ Invalid environment. Use: dev, staging, or prod"
exit 1
;;
esac
# Check if kubectl is available
if ! command -v kubectl &> /dev/null; then
echo "❌ kubectl is not installed. Please install kubectl first."
exit 1
fi
# Check if cluster is accessible
if ! kubectl cluster-info &> /dev/null; then
echo "❌ Cannot access Kubernetes cluster. Please check your kubeconfig."
exit 1
fi
# Update image tag in manifests
echo "🔧 Updating image tag to $IMAGE_TAG..."
if [ "$ENVIRONMENT" = "dev" ]; then
sed -i.bak "s|your-registry/game-2048:latest|$REGISTRY:$IMAGE_TAG|g" manifests/dev/service.yml
elif [ "$ENVIRONMENT" = "staging" ]; then
sed -i.bak "s|your-registry/game-2048:staging|$REGISTRY:$IMAGE_TAG|g" manifests/staging/service.yml
else
sed -i.bak "s|your-registry/game-2048:v1.0.0|$REGISTRY:$IMAGE_TAG|g" manifests/prod/service.yml
fi
# Deploy to the specified environment
echo "📦 Deploying to $ENVIRONMENT..."
kubectl apply -f manifests/$ENVIRONMENT/
# Wait for deployment to be ready
echo "⏳ Waiting for deployment to be ready..."
kubectl wait --for=condition=Ready ksvc/game-2048-$ENVIRONMENT -n game-2048-$ENVIRONMENT --timeout=300s
# Get service details
echo "✅ Deployment completed!"
echo ""
echo "🔍 Service details:"
kubectl get ksvc game-2048-$ENVIRONMENT -n game-2048-$ENVIRONMENT -o wide
echo ""
echo "🌐 Service URL:"
kubectl get ksvc game-2048-$ENVIRONMENT -n game-2048-$ENVIRONMENT -o jsonpath='{.status.url}'
echo ""
echo ""
echo "🎯 Custom domain:"
case $ENVIRONMENT in
dev)
echo "https://2048-dev.wa.darknex.us"
;;
staging)
echo "https://2048-staging.wa.darknex.us"
;;
prod)
echo "https://2048.wa.darknex.us"
;;
esac
# Restore original manifests
echo "🔄 Restoring original manifests..."
if [ -f "manifests/$ENVIRONMENT/service.yml.bak" ]; then
mv manifests/$ENVIRONMENT/service.yml.bak manifests/$ENVIRONMENT/service.yml
fi
echo ""
echo "🎮 Game deployed successfully! You can now access it at the custom domain."

58
scripts/setup-knative.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# Setup script for Knative Serving installation
# This script installs Knative Serving on a Kubernetes cluster
set -e
echo "🚀 Setting up Knative Serving..."
# Check if kubectl is available
if ! command -v kubectl &> /dev/null; then
echo "❌ kubectl is not installed. Please install kubectl first."
exit 1
fi
# Check if cluster is accessible
if ! kubectl cluster-info &> /dev/null; then
echo "❌ Cannot access Kubernetes cluster. Please check your kubeconfig."
exit 1
fi
# Install Knative Serving CRDs
echo "📦 Installing Knative Serving CRDs..."
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-crds.yaml
# Wait for CRDs to be established
echo "⏳ Waiting for CRDs to be established..."
kubectl wait --for condition=established --timeout=120s crd/configurations.serving.knative.dev
kubectl wait --for condition=established --timeout=120s crd/revisions.serving.knative.dev
kubectl wait --for condition=established --timeout=120s crd/routes.serving.knative.dev
kubectl wait --for condition=established --timeout=120s crd/services.serving.knative.dev
# Install Knative Serving core
echo "📦 Installing Knative Serving core..."
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-core.yaml
# Wait for Knative Serving to be ready
echo "⏳ Waiting for Knative Serving to be ready..."
kubectl wait --for=condition=Ready pod -l app=controller -n knative-serving --timeout=300s
kubectl wait --for=condition=Ready pod -l app=webhook -n knative-serving --timeout=300s
# Install Knative Serving HPA (Horizontal Pod Autoscaler)
echo "📦 Installing Knative Serving HPA..."
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.0/serving-hpa.yaml
# Configure domain
echo "🌐 Configuring domain..."
kubectl patch configmap/config-domain \
--namespace knative-serving \
--type merge \
--patch '{"data":{"wa.darknex.us":""}}'
echo "✅ Knative Serving installation completed!"
echo ""
echo "Next steps:"
echo "1. Install Kourier as the networking layer: ./setup-kourier.sh"
echo "2. Configure DNS to point your domain to the Kourier LoadBalancer"
echo "3. Deploy your applications using the manifests in this repository"

109
scripts/setup-kourier.sh Executable file
View File

@@ -0,0 +1,109 @@
#!/bin/bash
# Setup script for Kourier networking layer
# This script installs Kourier as the Knative networking layer
set -e
echo "🚀 Setting up Kourier networking layer..."
# Check if kubectl is available
if ! command -v kubectl &> /dev/null; then
echo "❌ kubectl is not installed. Please install kubectl first."
exit 1
fi
# Check if cluster is accessible
if ! kubectl cluster-info &> /dev/null; then
echo "❌ Cannot access Kubernetes cluster. Please check your kubeconfig."
exit 1
fi
# Check if Knative Serving is installed
if ! kubectl get namespace knative-serving &> /dev/null; then
echo "❌ Knative Serving is not installed. Please run ./setup-knative.sh first."
exit 1
fi
# Install Kourier
echo "📦 Installing Kourier..."
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.12.0/kourier.yaml
# Wait for Kourier to be ready
echo "⏳ Waiting for Kourier to be ready..."
kubectl wait --for=condition=Ready pod -l app=3scale-kourier-gateway -n kourier-system --timeout=300s
# Configure Knative to use Kourier
echo "🔧 Configuring Knative to use Kourier..."
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
# Get the external IP of Kourier
echo "🔍 Getting Kourier LoadBalancer details..."
kubectl get svc kourier -n kourier-system
# Configure auto-TLS (optional)
echo "🔐 Configuring auto-TLS..."
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"autoTLS":"Enabled","httpProtocol":"Redirected"}}'
# Install cert-manager for TLS (optional but recommended)
echo "📦 Installing cert-manager for TLS..."
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# Wait for cert-manager to be ready
echo "⏳ Waiting for cert-manager to be ready..."
kubectl wait --for=condition=Ready pod -l app=cert-manager -n cert-manager --timeout=300s
kubectl wait --for=condition=Ready pod -l app=cainjector -n cert-manager --timeout=300s
kubectl wait --for=condition=Ready pod -l app=webhook -n cert-manager --timeout=300s
# Install Knative cert-manager integration
echo "📦 Installing Knative cert-manager integration..."
kubectl apply -f https://github.com/knative/net-certmanager/releases/download/knative-v1.12.0/release.yaml
# Create ClusterIssuer for Let's Encrypt
echo "🔐 Creating Let's Encrypt ClusterIssuer..."
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@darknex.us
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: kourier.ingress.networking.knative.dev
EOF
# Configure Knative to use the ClusterIssuer
echo "🔧 Configuring Knative to use cert-manager..."
kubectl patch configmap/config-certmanager \
--namespace knative-serving \
--type merge \
--patch '{"data":{"issuerRef":"kind: ClusterIssuer\nname: letsencrypt-prod"}}'
echo "✅ Kourier setup completed!"
echo ""
echo "🔍 Kourier LoadBalancer service details:"
kubectl get svc kourier -n kourier-system -o wide
echo ""
echo "📋 Next steps:"
echo "1. Configure your DNS to point the following domains to the LoadBalancer IP:"
echo " - 2048-dev.wa.darknex.us"
echo " - 2048-staging.wa.darknex.us"
echo " - 2048.wa.darknex.us"
echo " - *.wa.darknex.us (wildcard)"
echo ""
echo "2. Deploy your applications:"
echo " kubectl apply -f manifests/dev/"
echo " kubectl apply -f manifests/staging/"
echo " kubectl apply -f manifests/prod/"

82
src/index.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048 Game - Knative Edition</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<div class="container">
<div class="header">
<h1>2048</h1>
<div class="environment-badge" id="env-badge"></div>
<div class="scores-container">
<div class="score-container">
<div class="score-title">SCORE</div>
<div class="score" id="score">0</div>
</div>
<div class="score-container">
<div class="score-title">BEST</div>
<div class="score" id="best">0</div>
</div>
</div>
</div>
<div class="above-game">
<p class="game-intro">
<strong>HOW TO PLAY:</strong> Use your <strong>arrow keys</strong> to move the tiles.
When two tiles with the same number touch, they <strong>merge into one!</strong>
</p>
<button class="restart-button" id="restart-button">New Game</button>
</div>
<div class="game-container">
<div class="game-message" id="game-message">
<p></p>
<div class="lower">
<button class="keep-playing-button" id="keep-playing-button">Keep going</button>
<button class="retry-button" id="retry-button">Try again</button>
</div>
</div>
<div class="grid-container">
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
</div>
<div class="tile-container" id="tile-container"></div>
</div>
<div class="game-explanation">
<p><strong>Knative Edition:</strong> This game is deployed using Knative Serving with scale-to-zero capabilities on Kubernetes!</p>
<p>Environment: <span id="environment">Production</span></p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

348
src/script.js Normal file
View File

@@ -0,0 +1,348 @@
// 2048 Game JavaScript - Knative Edition
class Game2048 {
constructor() {
this.grid = [];
this.score = 0;
this.best = localStorage.getItem('best2048') || 0;
this.gameWon = false;
this.gameOver = false;
this.keepPlaying = false;
this.init();
this.setupEventListeners();
this.setEnvironment();
}
init() {
this.grid = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
this.score = 0;
this.gameWon = false;
this.gameOver = false;
this.keepPlaying = false;
this.updateScore();
this.addRandomTile();
this.addRandomTile();
this.updateDisplay();
}
setEnvironment() {
const envElement = document.getElementById('environment');
const envBadge = document.getElementById('env-badge');
// Try to detect environment from hostname
const hostname = window.location.hostname;
let environment = 'production';
if (hostname.includes('dev')) {
environment = 'development';
} else if (hostname.includes('staging')) {
environment = 'staging';
}
envElement.textContent = environment.charAt(0).toUpperCase() + environment.slice(1);
envBadge.textContent = environment;
envBadge.className = `environment-badge ${environment}`;
}
setupEventListeners() {
document.addEventListener('keydown', (e) => this.handleKeyPress(e));
document.getElementById('restart-button').addEventListener('click', () => this.restart());
document.getElementById('keep-playing-button').addEventListener('click', () => this.keepPlayingGame());
document.getElementById('retry-button').addEventListener('click', () => this.restart());
// Touch/swipe support for mobile
let startX, startY;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
});
document.addEventListener('touchend', (e) => {
if (!startX || !startY) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const diffX = startX - endX;
const diffY = startY - endY;
if (Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 0) {
this.move('left');
} else {
this.move('right');
}
} else {
if (diffY > 0) {
this.move('up');
} else {
this.move('down');
}
}
});
}
handleKeyPress(e) {
if (this.gameOver && !this.keepPlaying) return;
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
this.move('up');
break;
case 'ArrowDown':
e.preventDefault();
this.move('down');
break;
case 'ArrowLeft':
e.preventDefault();
this.move('left');
break;
case 'ArrowRight':
e.preventDefault();
this.move('right');
break;
}
}
move(direction) {
const previousGrid = this.grid.map(row => [...row]);
let moved = false;
switch (direction) {
case 'left':
moved = this.moveLeft();
break;
case 'right':
moved = this.moveRight();
break;
case 'up':
moved = this.moveUp();
break;
case 'down':
moved = this.moveDown();
break;
}
if (moved) {
this.addRandomTile();
this.updateDisplay();
this.checkGameState();
}
}
moveLeft() {
let moved = false;
for (let row = 0; row < 4; row++) {
const newRow = this.slideArray(this.grid[row]);
if (!this.arraysEqual(this.grid[row], newRow)) {
moved = true;
this.grid[row] = newRow;
}
}
return moved;
}
moveRight() {
let moved = false;
for (let row = 0; row < 4; row++) {
const reversed = [...this.grid[row]].reverse();
const newRow = this.slideArray(reversed).reverse();
if (!this.arraysEqual(this.grid[row], newRow)) {
moved = true;
this.grid[row] = newRow;
}
}
return moved;
}
moveUp() {
let moved = false;
for (let col = 0; col < 4; col++) {
const column = [this.grid[0][col], this.grid[1][col], this.grid[2][col], this.grid[3][col]];
const newColumn = this.slideArray(column);
if (!this.arraysEqual(column, newColumn)) {
moved = true;
for (let row = 0; row < 4; row++) {
this.grid[row][col] = newColumn[row];
}
}
}
return moved;
}
moveDown() {
let moved = false;
for (let col = 0; col < 4; col++) {
const column = [this.grid[0][col], this.grid[1][col], this.grid[2][col], this.grid[3][col]];
const reversed = [...column].reverse();
const newColumn = this.slideArray(reversed).reverse();
if (!this.arraysEqual(column, newColumn)) {
moved = true;
for (let row = 0; row < 4; row++) {
this.grid[row][col] = newColumn[row];
}
}
}
return moved;
}
slideArray(arr) {
const filtered = arr.filter(val => val !== 0);
const missing = 4 - filtered.length;
const zeros = Array(missing).fill(0);
const newArray = filtered.concat(zeros);
for (let i = 0; i < 3; i++) {
if (newArray[i] !== 0 && newArray[i] === newArray[i + 1]) {
newArray[i] *= 2;
newArray[i + 1] = 0;
this.score += newArray[i];
}
}
const filtered2 = newArray.filter(val => val !== 0);
const missing2 = 4 - filtered2.length;
const zeros2 = Array(missing2).fill(0);
return filtered2.concat(zeros2);
}
arraysEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
addRandomTile() {
const emptyCells = [];
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] === 0) {
emptyCells.push({row, col});
}
}
}
if (emptyCells.length > 0) {
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
this.grid[randomCell.row][randomCell.col] = Math.random() < 0.9 ? 2 : 4;
}
}
updateDisplay() {
const container = document.getElementById('tile-container');
container.innerHTML = '';
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] !== 0) {
const tile = document.createElement('div');
tile.className = `tile tile-${this.grid[row][col]}`;
tile.textContent = this.grid[row][col];
tile.style.left = `${col * 121.25}px`;
tile.style.top = `${row * 121.25}px`;
if (this.grid[row][col] > 2048) {
tile.className = 'tile tile-super';
}
container.appendChild(tile);
}
}
}
}
updateScore() {
document.getElementById('score').textContent = this.score;
if (this.score > this.best) {
this.best = this.score;
localStorage.setItem('best2048', this.best);
}
document.getElementById('best').textContent = this.best;
}
checkGameState() {
this.updateScore();
// Check for 2048 tile (game won)
if (!this.gameWon && !this.keepPlaying) {
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] === 2048) {
this.gameWon = true;
this.showMessage('You Win!', 'game-won');
return;
}
}
}
}
// Check for game over
if (this.isGameOver()) {
this.gameOver = true;
this.showMessage('Game Over!', 'game-over');
}
}
isGameOver() {
// Check for empty cells
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] === 0) {
return false;
}
}
}
// Check for possible merges
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const current = this.grid[row][col];
if (
(row < 3 && current === this.grid[row + 1][col]) ||
(col < 3 && current === this.grid[row][col + 1])
) {
return false;
}
}
}
return true;
}
showMessage(text, className) {
const messageElement = document.getElementById('game-message');
messageElement.querySelector('p').textContent = text;
messageElement.className = `game-message ${className}`;
messageElement.style.display = 'block';
}
hideMessage() {
const messageElement = document.getElementById('game-message');
messageElement.style.display = 'none';
}
restart() {
this.hideMessage();
this.init();
}
keepPlayingGame() {
this.hideMessage();
this.keepPlaying = true;
}
}
// Initialize the game when the page loads
document.addEventListener('DOMContentLoaded', () => {
new Game2048();
});

382
src/style.css Normal file
View File

@@ -0,0 +1,382 @@
/* 2048 Game CSS - Knative Edition */
html, body {
margin: 0;
padding: 20px;
background: #faf8ef;
color: #776e65;
font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
font-size: 18px;
}
body {
margin: 80px 0;
}
.heading {
margin-bottom: 30px;
}
h1.title {
font-size: 80px;
font-weight: bold;
margin: 0;
display: inline-block;
}
.container {
width: 500px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h1 {
color: #776e65;
font-size: 80px;
font-weight: bold;
margin: 0;
}
.environment-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
color: white;
margin-left: 20px;
}
.environment-badge.development {
background: #ff6b6b;
}
.environment-badge.staging {
background: #ffa726;
}
.environment-badge.production {
background: #66bb6a;
}
.scores-container {
display: flex;
gap: 10px;
}
.score-container {
position: relative;
display: inline-block;
background: #bbada0;
padding: 10px 20px;
font-size: 25px;
height: 60px;
line-height: 47px;
font-weight: bold;
border-radius: 3px;
color: white;
text-align: center;
min-width: 80px;
}
.score-title {
position: absolute;
width: 100%;
top: 10px;
left: 0;
text-transform: uppercase;
font-size: 13px;
line-height: 13px;
text-align: center;
color: #eee4da;
}
.score {
font-size: 25px;
}
.above-game {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.game-intro {
line-height: 1.65;
margin: 0;
flex: 1;
margin-right: 20px;
}
.restart-button {
display: inline-block;
background: #8f7a66;
border-radius: 3px;
padding: 0 20px;
text-decoration: none;
color: #f9f6f2;
height: 40px;
line-height: 42px;
border: none;
cursor: pointer;
font-size: 18px;
}
.restart-button:hover {
background: #9f8a76;
}
.game-container {
position: relative;
padding: 15px;
cursor: default;
user-select: none;
touch-action: none;
background: #bbada0;
border-radius: 10px;
width: 500px;
height: 500px;
box-sizing: border-box;
}
.game-message {
display: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.73);
z-index: 100;
text-align: center;
border-radius: 10px;
}
.game-message p {
font-size: 60px;
font-weight: bold;
height: 60px;
line-height: 60px;
margin-top: 150px;
}
.game-message .lower {
display: block;
margin-top: 30px;
}
.game-message a {
display: inline-block;
background: #8f7a66;
border-radius: 3px;
padding: 0 20px;
text-decoration: none;
color: #f9f6f2;
height: 40px;
line-height: 42px;
margin-left: 9px;
}
.game-won {
background: rgba(237, 194, 46, 0.5);
color: #f9f6f2;
}
.game-won .game-message p {
color: #f9f6f2;
}
.game-over {
background: rgba(238, 228, 218, 0.73);
color: #776e65;
}
.game-over .game-message p {
color: #776e65;
}
.grid-container {
position: absolute;
z-index: 1;
}
.grid-row {
margin-bottom: 15px;
}
.grid-row:last-child {
margin-bottom: 0;
}
.grid-cell {
width: 106.25px;
height: 106.25px;
background: rgba(238, 228, 218, 0.35);
border-radius: 6px;
margin-right: 15px;
float: left;
}
.grid-cell:last-child {
margin-right: 0;
}
.tile-container {
position: absolute;
z-index: 2;
}
.tile {
width: 106.25px;
height: 106.25px;
background: #eee4da;
color: #776e65;
border-radius: 6px;
font-weight: bold;
text-align: center;
vertical-align: middle;
line-height: 106.25px;
font-size: 55px;
position: absolute;
transition: 0.15s ease-in-out;
transform-origin: center center;
}
.tile-2 { background: #eee4da; color: #776e65; }
.tile-4 { background: #ede0c8; color: #776e65; }
.tile-8 { color: #f9f6f2; background: #f2b179; }
.tile-16 { color: #f9f6f2; background: #f59563; }
.tile-32 { color: #f9f6f2; background: #f67c5f; }
.tile-64 { color: #f9f6f2; background: #f65e3b; }
.tile-128 { color: #f9f6f2; background: #edcf72; font-size: 45px; }
.tile-256 { color: #f9f6f2; background: #edcc61; font-size: 45px; }
.tile-512 { color: #f9f6f2; background: #edc850; font-size: 45px; }
.tile-1024 { color: #f9f6f2; background: #edc53f; font-size: 35px; }
.tile-2048 { color: #f9f6f2; background: #edc22e; font-size: 35px; }
.tile-super { color: #f9f6f2; background: #3c3a32; font-size: 30px; }
.tile-new {
animation: appear 200ms ease-in-out;
animation-fill-mode: backwards;
}
.tile-merged {
z-index: 20;
animation: pop 200ms ease-in-out;
animation-fill-mode: backwards;
}
@keyframes appear {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes pop {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.game-explanation {
margin-top: 30px;
text-align: center;
color: #776e65;
}
.game-explanation p {
margin: 10px 0;
}
.keep-playing-button, .retry-button {
display: inline-block;
background: #8f7a66;
border-radius: 3px;
padding: 0 20px;
text-decoration: none;
color: #f9f6f2;
height: 40px;
line-height: 42px;
border: none;
cursor: pointer;
font-size: 18px;
margin: 0 5px;
}
.keep-playing-button:hover, .retry-button:hover {
background: #9f8a76;
}
/* Responsive design */
@media screen and (max-width: 520px) {
.container {
width: 280px;
margin: 0 auto;
}
.header h1 {
font-size: 50px;
}
.scores-container {
flex-direction: column;
gap: 5px;
}
.above-game {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.game-container {
width: 280px;
height: 280px;
padding: 10px;
}
.grid-cell {
width: 60px;
height: 60px;
margin-right: 10px;
margin-bottom: 10px;
}
.tile {
width: 60px;
height: 60px;
line-height: 60px;
font-size: 35px;
}
.tile-128, .tile-256, .tile-512 {
font-size: 25px;
}
.tile-1024, .tile-2048 {
font-size: 20px;
}
.tile-super {
font-size: 18px;
}
}