From c3b227b7d75cc697ec75364e685b120607262039 Mon Sep 17 00:00:00 2001 From: greg Date: Mon, 30 Jun 2025 20:43:19 -0700 Subject: [PATCH] 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 --- .github/workflows/deploy-dev.yml | 76 ++++++ .github/workflows/deploy-prod.yml | 103 ++++++++ .github/workflows/deploy-staging.yml | 81 ++++++ .gitignore | 50 ++++ CONTRIBUTING.md | 159 +++++++++++ Dockerfile | 17 ++ LICENSE | 21 ++ README.md | 121 +++++++++ docs/SETUP.md | 236 +++++++++++++++++ manifests/dev/domain-mapping.yml | 13 + manifests/dev/namespace.yml | 7 + manifests/dev/service.yml | 60 +++++ manifests/prod/domain-mapping.yml | 13 + manifests/prod/namespace.yml | 7 + manifests/prod/service.yml | 60 +++++ manifests/staging/domain-mapping.yml | 13 + manifests/staging/namespace.yml | 7 + manifests/staging/service.yml | 60 +++++ nginx.conf | 38 +++ package.json | 36 +++ scripts/deploy.sh | 87 ++++++ scripts/setup-knative.sh | 58 ++++ scripts/setup-kourier.sh | 109 ++++++++ src/index.html | 82 ++++++ src/script.js | 348 ++++++++++++++++++++++++ src/style.css | 382 +++++++++++++++++++++++++++ 26 files changed, 2244 insertions(+) create mode 100644 .github/workflows/deploy-dev.yml create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 .github/workflows/deploy-staging.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/SETUP.md create mode 100644 manifests/dev/domain-mapping.yml create mode 100644 manifests/dev/namespace.yml create mode 100644 manifests/dev/service.yml create mode 100644 manifests/prod/domain-mapping.yml create mode 100644 manifests/prod/namespace.yml create mode 100644 manifests/prod/service.yml create mode 100644 manifests/staging/domain-mapping.yml create mode 100644 manifests/staging/namespace.yml create mode 100644 manifests/staging/service.yml create mode 100644 nginx.conf create mode 100644 package.json create mode 100755 scripts/deploy.sh create mode 100755 scripts/setup-knative.sh create mode 100755 scripts/setup-kourier.sh create mode 100644 src/index.html create mode 100644 src/script.js create mode 100644 src/style.css diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..faf7d04 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -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}' diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..e0d88ef --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -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}' diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..d38969e --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -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}' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36fdcba --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ab4c140 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..358d4e2 --- /dev/null +++ b/Dockerfile @@ -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;"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a1a6d2d --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd525e2 --- /dev/null +++ b/README.md @@ -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. diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..45b6609 --- /dev/null +++ b/docs/SETUP.md @@ -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 - < 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 -l serving.knative.dev/service= +``` + +## 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 diff --git a/manifests/dev/domain-mapping.yml b/manifests/dev/domain-mapping.yml new file mode 100644 index 0000000..aa4cf9d --- /dev/null +++ b/manifests/dev/domain-mapping.yml @@ -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 diff --git a/manifests/dev/namespace.yml b/manifests/dev/namespace.yml new file mode 100644 index 0000000..54e4953 --- /dev/null +++ b/manifests/dev/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: game-2048-dev + labels: + environment: development + app: game-2048 diff --git a/manifests/dev/service.yml b/manifests/dev/service.yml new file mode 100644 index 0000000..0cbc12e --- /dev/null +++ b/manifests/dev/service.yml @@ -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 diff --git a/manifests/prod/domain-mapping.yml b/manifests/prod/domain-mapping.yml new file mode 100644 index 0000000..049c519 --- /dev/null +++ b/manifests/prod/domain-mapping.yml @@ -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 diff --git a/manifests/prod/namespace.yml b/manifests/prod/namespace.yml new file mode 100644 index 0000000..465c0b6 --- /dev/null +++ b/manifests/prod/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: game-2048-prod + labels: + environment: production + app: game-2048 diff --git a/manifests/prod/service.yml b/manifests/prod/service.yml new file mode 100644 index 0000000..e94a3b0 --- /dev/null +++ b/manifests/prod/service.yml @@ -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 diff --git a/manifests/staging/domain-mapping.yml b/manifests/staging/domain-mapping.yml new file mode 100644 index 0000000..9a75183 --- /dev/null +++ b/manifests/staging/domain-mapping.yml @@ -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 diff --git a/manifests/staging/namespace.yml b/manifests/staging/namespace.yml new file mode 100644 index 0000000..11a7855 --- /dev/null +++ b/manifests/staging/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: game-2048-staging + labels: + environment: staging + app: game-2048 diff --git a/manifests/staging/service.yml b/manifests/staging/service.yml new file mode 100644 index 0000000..1ecebb0 --- /dev/null +++ b/manifests/staging/service.yml @@ -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 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d29470a --- /dev/null +++ b/nginx.conf @@ -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; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..909faf0 --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..67040a4 --- /dev/null +++ b/scripts/deploy.sh @@ -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." diff --git a/scripts/setup-knative.sh b/scripts/setup-knative.sh new file mode 100755 index 0000000..5437520 --- /dev/null +++ b/scripts/setup-knative.sh @@ -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" diff --git a/scripts/setup-kourier.sh b/scripts/setup-kourier.sh new file mode 100755 index 0000000..e3c1a47 --- /dev/null +++ b/scripts/setup-kourier.sh @@ -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 < + + + + + 2048 Game - Knative Edition + + + + +
+
+

2048

+
+
+
+
SCORE
+
0
+
+
+
BEST
+
0
+
+
+
+ +
+

+ HOW TO PLAY: Use your arrow keys to move the tiles. + When two tiles with the same number touch, they merge into one! +

+ +
+ +
+
+

+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+

Knative Edition: This game is deployed using Knative Serving with scale-to-zero capabilities on Kubernetes!

+

Environment: Production

+
+
+ + + + diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..73ff3df --- /dev/null +++ b/src/script.js @@ -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(); +}); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..4434194 --- /dev/null +++ b/src/style.css @@ -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; + } +}