mirror of
https://github.com/ghndrx/k8s-game-2048.git
synced 2026-02-10 06:45:07 +00:00
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:
76
.github/workflows/deploy-dev.yml
vendored
Normal file
76
.github/workflows/deploy-dev.yml
vendored
Normal 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
103
.github/workflows/deploy-prod.yml
vendored
Normal 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
81
.github/workflows/deploy-staging.yml
vendored
Normal 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
50
.gitignore
vendored
Normal 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
159
CONTRIBUTING.md
Normal 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
17
Dockerfile
Normal 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
21
LICENSE
Normal 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
121
README.md
Normal 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
236
docs/SETUP.md
Normal 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
|
||||||
13
manifests/dev/domain-mapping.yml
Normal file
13
manifests/dev/domain-mapping.yml
Normal 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
|
||||||
7
manifests/dev/namespace.yml
Normal file
7
manifests/dev/namespace.yml
Normal 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
60
manifests/dev/service.yml
Normal 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
|
||||||
13
manifests/prod/domain-mapping.yml
Normal file
13
manifests/prod/domain-mapping.yml
Normal 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
|
||||||
7
manifests/prod/namespace.yml
Normal file
7
manifests/prod/namespace.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: game-2048-prod
|
||||||
|
labels:
|
||||||
|
environment: production
|
||||||
|
app: game-2048
|
||||||
60
manifests/prod/service.yml
Normal file
60
manifests/prod/service.yml
Normal 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
|
||||||
13
manifests/staging/domain-mapping.yml
Normal file
13
manifests/staging/domain-mapping.yml
Normal 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
|
||||||
7
manifests/staging/namespace.yml
Normal file
7
manifests/staging/namespace.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: game-2048-staging
|
||||||
|
labels:
|
||||||
|
environment: staging
|
||||||
|
app: game-2048
|
||||||
60
manifests/staging/service.yml
Normal file
60
manifests/staging/service.yml
Normal 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
38
nginx.conf
Normal 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
36
package.json
Normal 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
87
scripts/deploy.sh
Executable 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
58
scripts/setup-knative.sh
Executable 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
109
scripts/setup-kourier.sh
Executable 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
82
src/index.html
Normal 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
348
src/script.js
Normal 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
382
src/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user