Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Game Security (Must match between frontend and backend)
GAME_SECRET=
VITE_GAME_SECRET=

# Backend Config
PORT=8080
DB_PATH=game.db

# Frontend Config
VITE_API_URL=http://localhost:8080/api
VITE_GA_ID=
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ dist-ssr
*.njsproj
*.sln
*.sw?
package-lock.json
package-lock.json

*.db
*.db-shm
*.db-wal

.env
.env.local
.env.*.local
19 changes: 19 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Makefile for Rick Stack Backend

BUILD_DIR=bin
BINARY_NAME=rick-server

build:
go build -o $(BUILD_DIR)/$(BINARY_NAME) main.go

run:
go run main.go

test:
go test ./...

clean:
rm -rf $(BUILD_DIR)
rm -f game.db game.db-shm game.db-wal

all: build
111 changes: 111 additions & 0 deletions backend/database/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package database

import (
"database/sql"
_ "modernc.org/sqlite"
"log"
"time"
)

var DB *sql.DB

func InitDB(dbPath string) (*sql.DB, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, err
}

// Enable WAL mode for performance
if _, err := db.Exec("PRAGMA journal_mode=WAL;"); err != nil {
log.Printf("failed to enable WAL: %v", err)
}

// Create tables
schema := `
CREATE TABLE IF NOT EXISTS players (
id TEXT PRIMARY KEY,
nickname TEXT DEFAULT 'Rick',
game_state BLOB,
seeds INTEGER GENERATED ALWAYS AS (json_extract(game_state, '$.seeds')) VIRTUAL,
max_dimension INTEGER GENERATED ALWAYS AS (json_extract(game_state, '$.maxDimensionLevel')) VIRTUAL,
inventory_count INTEGER GENERATED ALWAYS AS (json_array_length(json_extract(game_state, '$.inventory'))) VIRTUAL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_players_seeds ON players(seeds);
CREATE INDEX IF NOT EXISTS idx_players_max_dimension ON players(max_dimension);
CREATE INDEX IF NOT EXISTS idx_players_inventory_count ON players(inventory_count);
`
_, err = db.Exec(schema)
if err != nil {
return nil, err
}

DB = db
return db, nil
}

func SavePlayer(id string, nickname string, gameStateJSON []byte) error {
_, err := DB.Exec(`
INSERT INTO players (id, nickname, game_state, updated_at)
VALUES (?, ?, jsonb(?), CURRENT_TIMESTAMP)
ON CONFLICT(id) DO UPDATE SET
nickname=?,
game_state=jsonb(?),
updated_at=CURRENT_TIMESTAMP
`, id, nickname, gameStateJSON, nickname, gameStateJSON)
return err
}

func GetPlayer(id string) (string, []byte, time.Time, error) {
var playerId string
var gameState []byte
var updatedAt time.Time
err := DB.QueryRow("SELECT id, json(game_state), updated_at FROM players WHERE id = ?", id).Scan(&playerId, &gameState, &updatedAt)
if err != nil {
return "", nil, time.Time{}, err
}
return playerId, gameState, updatedAt, nil
}

type LeaderboardEntry struct {
ID string `json:"id"`
Nickname string `json:"nickname"`
Value int64 `json:"value"`
}

func GetLeaderboard(category string, limit int) ([]LeaderboardEntry, error) {
var query string
switch category {
case "seeds":
query = "SELECT id, nickname, seeds FROM players ORDER BY seeds DESC LIMIT ?"
case "dimension":
query = "SELECT id, nickname, max_dimension FROM players ORDER BY max_dimension DESC LIMIT ?"
case "inventory":
query = "SELECT id, nickname, inventory_count FROM players ORDER BY inventory_count DESC LIMIT ?"
case "discovery":
query = `
SELECT id, nickname, (SELECT count(*) FROM json_each(json_extract(game_state, '$.discoveredCards'))) as discovery_count
FROM players
ORDER BY discovery_count DESC
LIMIT ?`
default:
query = "SELECT id, nickname, seeds FROM players ORDER BY seeds DESC LIMIT ?"
}

rows, err := DB.Query(query, limit)
if err != nil {
return nil, err
}
defer rows.Close()

var entries []LeaderboardEntry
for rows.Next() {
var e LeaderboardEntry
if err := rows.Scan(&e.ID, &e.Nickname, &e.Value); err != nil {
return nil, err
}
entries = append(entries, e)
}
return entries, nil
}
48 changes: 48 additions & 0 deletions backend/game/logic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package game

import (
"backend/models"
"fmt"
"time"
)

// Constants for logical verification
const (
MaxSeedsPerSecond = 500.0 // Adjusted for "Seeds"
MaxNewCardsPerSave = 200
)

func ValidateProgress(oldState *models.GameState, newState *models.GameState, lastUpdate time.Time) error {
// If no old state, it's a new player.
if oldState == nil {
return nil
}

deltaSeconds := time.Since(lastUpdate).Seconds()
if deltaSeconds < 0 {
return fmt.Errorf("invalid time delta")
}

// 1. Seeds Verification
seedsDiff := newState.Seeds - oldState.Seeds
if seedsDiff > 0 {
// Calculate max possible based on some game logic
maxPossibleSeeds := (MaxSeedsPerSecond * deltaSeconds) + float64(newState.MaxDimensionLevel*1000)
if float64(seedsDiff) > maxPossibleSeeds {
return fmt.Errorf("impossible seeds gain: %d in %.1fs", seedsDiff, deltaSeconds)
}
}

// 2. Inventory Count Verification
inventoryDiff := len(newState.Inventory) - len(oldState.Inventory)
if inventoryDiff > MaxNewCardsPerSave {
return fmt.Errorf("impossible inventory growth: %d", inventoryDiff)
}

// 3. Dimension Level check
if newState.MaxDimensionLevel < oldState.MaxDimensionLevel {
return fmt.Errorf("max dimension level cannot decrease")
}

return nil
}
21 changes: 21 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module backend

go 1.25.4

require (
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
modernc.org/sqlite v1.47.0
)

require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.42.0 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
55 changes: 55 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
Loading
Loading