Add visual smoothing for snake game and systemd deployment
CSS animations for smoother 60fps feel despite 7Hz game ticks: - 130ms transitions on cell background/box-shadow - Head pop-in animation on direction changes - Food pulse animation - Smooth death state fade with grayscale Per-snake colored glow on head cells. Make server port configurable via PORT env var (default 8080). Add deploy/ with systemd service and scripts: - setup.sh: create games user, /opt/c4, install unit - deploy.sh: build and install binary, restart service - package.sh: cross-compile, tarball, base64 split for transfer - reassemble.sh: decode and extract on target server
This commit is contained in:
@@ -75,13 +75,30 @@
|
|||||||
.snake-cell {
|
.snake-cell {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
border: 1px solid rgba(255,255,255,0.03);
|
border: 1px solid rgba(255,255,255,0.03);
|
||||||
transition: background 0.05s;
|
transition: background 130ms ease-in-out, box-shadow 130ms ease-out;
|
||||||
|
}
|
||||||
|
.snake-cell.snake-head {
|
||||||
|
border-radius: 4px;
|
||||||
|
animation: head-pop 130ms ease-out;
|
||||||
|
}
|
||||||
|
@keyframes head-pop {
|
||||||
|
0% { transform: scale(0.6); }
|
||||||
|
50% { transform: scale(1.08); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
}
|
}
|
||||||
.snake-cell.snake-food {
|
.snake-cell.snake-food {
|
||||||
background: #ff6b6b;
|
background: #ff6b6b;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 6px rgba(255,107,107,0.6);
|
box-shadow: 0 0 6px rgba(255,107,107,0.6);
|
||||||
|
animation: food-pulse 1.2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes food-pulse {
|
||||||
|
from { box-shadow: 0 0 6px rgba(255,107,107,0.4); transform: scale(0.85); }
|
||||||
|
to { box-shadow: 0 0 12px rgba(255,107,107,0.9); transform: scale(1); }
|
||||||
|
}
|
||||||
|
.snake-cell.snake-dead {
|
||||||
|
opacity: 0.35;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
transition: opacity 400ms ease-out, background 130ms ease-in-out;
|
||||||
}
|
}
|
||||||
.snake-cell.snake-head { border-radius: 4px; }
|
|
||||||
.snake-cell.snake-dead { opacity: 0.35; }
|
|
||||||
.snake-wrapper:focus { outline: none; }
|
.snake-wrapper:focus { outline: none; }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
24
deploy/c4.service
Normal file
24
deploy/c4.service
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=C4 Game Lobby
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=games
|
||||||
|
Group=games
|
||||||
|
WorkingDirectory=/opt/c4
|
||||||
|
ExecStart=/opt/c4/c4
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
Environment=PORT=8080
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/opt/c4
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
31
deploy/deploy.sh
Executable file
31
deploy/deploy.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy the c4 binary to /opt/c4, then restart the service.
|
||||||
|
# Works from the repo (builds first) or from an extracted tarball (pre-built binary).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
INSTALL_DIR="/opt/c4"
|
||||||
|
BINARY="$ROOT_DIR/c4"
|
||||||
|
|
||||||
|
# If Go is available and we have source, build fresh
|
||||||
|
if [[ -f "$ROOT_DIR/go.mod" ]] && command -v go &>/dev/null; then
|
||||||
|
echo "Building CSS..."
|
||||||
|
(cd "$ROOT_DIR" && go tool gotailwind -i assets/css/input.css -o assets/css/output.css --minify)
|
||||||
|
|
||||||
|
echo "Building binary..."
|
||||||
|
(cd "$ROOT_DIR" && CGO_ENABLED=0 go build -o c4 .)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$BINARY" ]]; then
|
||||||
|
echo "ERROR: Binary not found at $BINARY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing to $INSTALL_DIR..."
|
||||||
|
install -o games -g games -m 755 "$BINARY" "$INSTALL_DIR/c4"
|
||||||
|
|
||||||
|
echo "Restarting service..."
|
||||||
|
systemctl restart c4.service
|
||||||
|
|
||||||
|
echo "Done. Status:"
|
||||||
|
systemctl status c4.service --no-pager
|
||||||
87
deploy/package.sh
Executable file
87
deploy/package.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the c4 binary, bundle it with deploy files into a tarball,
|
||||||
|
# base64-encode it, and split into 25MB chunks for transfer.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
|
TARBALL="c4-deploy-${TIMESTAMP}.tar.gz"
|
||||||
|
BASE64_FILE="c4-deploy-${TIMESTAMP}_b64.txt"
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Clean previous artifacts
|
||||||
|
#==============================================================================
|
||||||
|
echo "--- Cleaning old artifacts ---"
|
||||||
|
rm -f ./c4 c4-deploy-*.tar.gz c4-deploy-*_b64*.txt
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Build
|
||||||
|
#==============================================================================
|
||||||
|
echo "--- Building CSS ---"
|
||||||
|
go tool gotailwind -i assets/css/input.css -o assets/css/output.css --minify
|
||||||
|
|
||||||
|
echo "--- Building binary (linux/amd64) ---"
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o c4 .
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Verify required files
|
||||||
|
#==============================================================================
|
||||||
|
echo "--- Verifying files ---"
|
||||||
|
REQUIRED_FILES=(
|
||||||
|
c4
|
||||||
|
deploy/setup.sh
|
||||||
|
deploy/deploy.sh
|
||||||
|
deploy/reassemble.sh
|
||||||
|
deploy/c4.service
|
||||||
|
)
|
||||||
|
for f in "${REQUIRED_FILES[@]}"; do
|
||||||
|
if [[ ! -f "$f" ]]; then
|
||||||
|
echo "ERROR: Missing required file: $f" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Create tarball
|
||||||
|
#==============================================================================
|
||||||
|
echo "--- Creating tarball ---"
|
||||||
|
tar -czf "/tmp/${TARBALL}" --transform 's,^,c4/,' \
|
||||||
|
c4 \
|
||||||
|
deploy/setup.sh \
|
||||||
|
deploy/deploy.sh \
|
||||||
|
deploy/reassemble.sh \
|
||||||
|
deploy/c4.service
|
||||||
|
|
||||||
|
mv "/tmp/${TARBALL}" .
|
||||||
|
echo " -> ${TARBALL} ($(du -h "${TARBALL}" | cut -f1))"
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Base64 encode and split
|
||||||
|
#==============================================================================
|
||||||
|
echo "--- Base64 encoding ---"
|
||||||
|
base64 "${TARBALL}" > "${BASE64_FILE}"
|
||||||
|
echo " -> ${BASE64_FILE} ($(du -h "${BASE64_FILE}" | cut -f1))"
|
||||||
|
|
||||||
|
echo "--- Splitting into 25MB chunks ---"
|
||||||
|
split -b 25M -d --additional-suffix=.txt "${BASE64_FILE}" "c4-deploy-${TIMESTAMP}_b64_part"
|
||||||
|
rm -f "${BASE64_FILE}"
|
||||||
|
|
||||||
|
CHUNKS=(c4-deploy-${TIMESTAMP}_b64_part*.txt)
|
||||||
|
echo " -> ${#CHUNKS[@]} chunk(s):"
|
||||||
|
for chunk in "${CHUNKS[@]}"; do
|
||||||
|
echo " $chunk ($(du -h "$chunk" | cut -f1))"
|
||||||
|
done
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Done
|
||||||
|
#==============================================================================
|
||||||
|
echo ""
|
||||||
|
echo "=== Package Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Transfer the chunk files to the target server, then run:"
|
||||||
|
echo " ./reassemble.sh"
|
||||||
|
echo " cd ~/c4 && sudo ./deploy/setup.sh # first time only"
|
||||||
|
echo " cd ~/c4 && sudo ./deploy/deploy.sh"
|
||||||
96
deploy/reassemble.sh
Executable file
96
deploy/reassemble.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Reassembles base64 chunks and extracts the c4 deployment tarball.
|
||||||
|
# Expects chunk files in the current directory.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$HOME"
|
||||||
|
|
||||||
|
echo "=== C4 Deployment Reassembler ==="
|
||||||
|
echo "Working directory: $HOME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Find and validate chunk files
|
||||||
|
#==============================================================================
|
||||||
|
echo "--- Finding chunk files ---"
|
||||||
|
|
||||||
|
CHUNKS=($(ls -1 c4-deploy-*_b64_part*.txt 2>/dev/null | sort))
|
||||||
|
|
||||||
|
if [[ ${#CHUNKS[@]} -eq 0 ]]; then
|
||||||
|
echo "ERROR: No chunk files found matching c4-deploy-*_b64_part*.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found ${#CHUNKS[@]} chunks:"
|
||||||
|
for chunk in "${CHUNKS[@]}"; do
|
||||||
|
echo " - $chunk ($(du -h "$chunk" | cut -f1))"
|
||||||
|
done
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Reassemble and decode
|
||||||
|
#==============================================================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Reassembling chunks ---"
|
||||||
|
|
||||||
|
TIMESTAMP=$(echo "${CHUNKS[0]}" | sed -E 's/c4-deploy-([0-9]+-[0-9]+)_b64_part.*/\1/')
|
||||||
|
TARBALL="c4-deploy-${TIMESTAMP}.tar.gz"
|
||||||
|
COMBINED="combined_b64.txt"
|
||||||
|
|
||||||
|
echo "Concatenating chunks..."
|
||||||
|
cat "${CHUNKS[@]}" > "$COMBINED"
|
||||||
|
echo " -> Created $COMBINED ($(du -h "$COMBINED" | cut -f1))"
|
||||||
|
|
||||||
|
echo "Decoding base64..."
|
||||||
|
base64 -d "$COMBINED" > "$TARBALL"
|
||||||
|
echo " -> Created $TARBALL ($(du -h "$TARBALL" | cut -f1))"
|
||||||
|
|
||||||
|
echo "Verifying tarball..."
|
||||||
|
if tar -tzf "$TARBALL" > /dev/null 2>&1; then
|
||||||
|
echo " -> Tarball is valid"
|
||||||
|
else
|
||||||
|
echo "ERROR: Tarball verification failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Archive existing source
|
||||||
|
#==============================================================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Archiving existing source ---"
|
||||||
|
|
||||||
|
if [[ -d c4 ]]; then
|
||||||
|
rm -rf c4.bak
|
||||||
|
mv c4 c4.bak
|
||||||
|
echo " -> Moved c4 -> c4.bak"
|
||||||
|
else
|
||||||
|
echo " -> No existing c4 directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Extract
|
||||||
|
#==============================================================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Extracting tarball ---"
|
||||||
|
|
||||||
|
tar -xzf "$TARBALL"
|
||||||
|
echo " -> Extracted to ~/c4"
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Cleanup
|
||||||
|
#==============================================================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Cleaning up ---"
|
||||||
|
|
||||||
|
rm -f "$COMBINED" "$TARBALL" "${CHUNKS[@]}"
|
||||||
|
echo " -> Removed chunks, combined base64, and tarball"
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Next steps
|
||||||
|
#==============================================================================
|
||||||
|
echo ""
|
||||||
|
echo "=== Reassembly Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " cd ~/c4"
|
||||||
|
echo " sudo ./deploy/setup.sh # first time only"
|
||||||
|
echo " sudo ./deploy/deploy.sh"
|
||||||
29
deploy/setup.sh
Executable file
29
deploy/setup.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One-time setup: create the games user, install directory, and systemd unit.
|
||||||
|
# Run as root (or with sudo).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Run as root: sudo $0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create system user if it doesn't exist
|
||||||
|
if ! id -u games &>/dev/null; then
|
||||||
|
useradd --system --shell /usr/sbin/nologin --home-dir /opt/c4 --create-home games
|
||||||
|
echo "Created system user: games"
|
||||||
|
else
|
||||||
|
echo "User 'games' already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure install directory exists with correct ownership
|
||||||
|
install -d -o games -g games -m 755 /opt/c4
|
||||||
|
install -d -o games -g games -m 755 /opt/c4/data
|
||||||
|
|
||||||
|
# Install systemd unit
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cp "$SCRIPT_DIR/c4.service" /etc/systemd/system/c4.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable c4.service
|
||||||
|
|
||||||
|
echo "Setup complete. Run deploy.sh to build and start the service."
|
||||||
18
main.go
18
main.go
@@ -6,6 +6,7 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
@@ -20,9 +21,11 @@ import (
|
|||||||
"github.com/ryanhamamura/via/vianats"
|
"github.com/ryanhamamura/via/vianats"
|
||||||
)
|
)
|
||||||
|
|
||||||
var store = game.NewGameStore()
|
var (
|
||||||
var snakeStore = snake.NewSnakeStore()
|
store = game.NewGameStore()
|
||||||
var queries *gen.Queries
|
snakeStore = snake.NewSnakeStore()
|
||||||
|
queries *gen.Queries
|
||||||
|
)
|
||||||
|
|
||||||
//go:embed assets/css/output.css
|
//go:embed assets/css/output.css
|
||||||
var daisyUICSS []byte
|
var daisyUICSS []byte
|
||||||
@@ -35,6 +38,13 @@ func DaisyUIPlugin(v *via.V) {
|
|||||||
v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("/_plugins/daisyui/style.css")))
|
v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("/_plugins/daisyui/style.css")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func port() string {
|
||||||
|
if p := os.Getenv("PORT"); p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return "7331"
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := db.Init("c4.db"); err != nil {
|
if err := db.Init("c4.db"); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -60,7 +70,7 @@ func main() {
|
|||||||
v.Config(via.Options{
|
v.Config(via.Options{
|
||||||
LogLevel: via.LogLevelDebug,
|
LogLevel: via.LogLevelDebug,
|
||||||
DocumentTitle: "Game Lobby",
|
DocumentTitle: "Game Lobby",
|
||||||
ServerAddress: ":7331",
|
ServerAddress: ":" + port(),
|
||||||
SessionManager: sessionManager,
|
SessionManager: sessionManager,
|
||||||
PubSub: ns,
|
PubSub: ns,
|
||||||
Plugins: []via.Plugin{DaisyUIPlugin},
|
Plugins: []via.Plugin{DaisyUIPlugin},
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ func SnakeBoard(sg *snake.SnakeGame) h.H {
|
|||||||
case ci.snakeIdx >= 0:
|
case ci.snakeIdx >= 0:
|
||||||
s := state.Snakes[ci.snakeIdx]
|
s := state.Snakes[ci.snakeIdx]
|
||||||
colorIdx := ci.snakeIdx
|
colorIdx := ci.snakeIdx
|
||||||
|
bg := ""
|
||||||
if colorIdx < len(snake.SnakeColors) {
|
if colorIdx < len(snake.SnakeColors) {
|
||||||
bg := snake.SnakeColors[colorIdx]
|
bg = snake.SnakeColors[colorIdx]
|
||||||
style += fmt.Sprintf("background:%s;", bg)
|
style += fmt.Sprintf("background:%s;", bg)
|
||||||
}
|
}
|
||||||
if !s.Alive {
|
if !s.Alive {
|
||||||
@@ -73,6 +74,9 @@ func SnakeBoard(sg *snake.SnakeGame) h.H {
|
|||||||
}
|
}
|
||||||
if ci.isHead {
|
if ci.isHead {
|
||||||
class += " snake-head"
|
class += " snake-head"
|
||||||
|
if bg != "" {
|
||||||
|
style += fmt.Sprintf("box-shadow:0 0 8px %s;", bg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user