Includes build/test commands, code style guidelines, naming conventions, error handling patterns, and Go/templ/Datastar patterns used in this repo.
5.9 KiB
5.9 KiB
AGENTS.md
Instructions for AI coding agents working in this repository.
Quick Reference
# Development
task live # Hot-reload dev server (templ + tailwind + air)
task build # Production build to bin/games
task run # Build and run server
# Quality
task test # Run all tests: go test ./...
task lint # Run linter: golangci-lint run
# Single test
go test -run TestName ./path/to/package
# Code generation
task build:templ # Compile .templ files
task build:styles # Build TailwindCSS
go generate ./... # Run sqlc for DB queries
Workflow Rules
- Never merge PRs without explicit user approval. Create the PR, push changes, then wait.
- Always use PRs via
teaCLI - never push directly to main. - Write semantic commit messages focusing on "why" not "what".
Project Structure
games/
├── connect4/, snake/ # Game logic packages (pure Go)
├── features/ # Feature modules (handlers, routes, templates)
│ ├── auth/ # Login/register
│ ├── c4game/ # Connect 4 UI
│ ├── snakegame/ # Snake UI
│ ├── lobby/ # Game lobby
│ └── common/ # Shared components, layouts
├── chat/ # Reusable chat room (NATS + persistence)
├── db/ # SQLite, migrations, sqlc queries
├── assets/ # Static files (embedded)
└── config/, logging/, nats/, sessions/, router/ # Infrastructure
Code Style
Imports
Organize in three groups: stdlib, third-party, local. The linter enforces this.
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
"github.com/ryanhamamura/games/connect4"
"github.com/ryanhamamura/games/db/repository"
)
Naming Conventions
| Type | Convention | Examples |
|---|---|---|
| Files | lowercase, underscores | config_dev.go, handlers.go |
| HTTP handlers | Handle prefix |
HandleGamePage, HandleLogin |
| Constructors | New prefix |
NewStore, NewRoom |
| Getters | Get prefix |
GetPlayerID, GetGame |
| Setup functions | Setup prefix |
SetupRoutes, SetupLogger |
| Types | PascalCase | Game, Player, Instance |
| Status enums | Status prefix |
StatusWaitingForPlayer, StatusInProgress |
| Session keys | Key prefix |
KeyPlayerID, KeyUserID |
Error Handling
-
Wrap errors with context:
return fmt.Errorf("loading game %s: %w", id, err) -
Return (result, error) tuples:
func loadGame(queries *repository.Queries, id string) (*Game, error) -
Best-effort operations - use nolint comment:
nc.Publish(subject, nil) //nolint:errcheck // best-effort notification -
HTTP errors:
http.Error(w, "game not found", http.StatusNotFound) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
Comments
- Focus on why, not how. Avoid superfluous comments.
- Package comments at top of primary file:
// Package connect4 implements Connect 4 game logic, state management, and persistence. package connect4 - Function comments for exported functions:
// DropPiece attempts to drop a piece in the given column. // Returns (row placed, success). func (g *Game) DropPiece(col, playerColor int) (int, bool)
Go Patterns
Dependency Injection via Closures
Handlers receive dependencies and return http.HandlerFunc:
func HandleGamePage(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// use store, sm here
}
}
Mutex for Concurrent Access
type Store struct {
games map[string]*Instance
gamesMu sync.RWMutex
}
func (s *Store) Get(id string) (*Instance, bool) {
s.gamesMu.RLock()
defer s.gamesMu.RUnlock()
inst, ok := s.games[id]
return inst, ok
}
Build Tags for Environment
//go:build dev
//go:build !dev
Embedded Filesystems
//go:embed assets
var assets embed.FS
//go:embed migrations/*.sql
var MigrationFS embed.FS
Graceful Shutdown
eg, egctx := errgroup.WithContext(ctx)
eg.Go(func() error { return server.ListenAndServe() })
eg.Go(func() error {
<-egctx.Done()
return server.Shutdown(context.Background())
})
return eg.Wait()
Templ + Datastar Patterns
SSE Connection with Disabled Cancellation
Datastar cancels SSE on user interaction by default. Disable for persistent connections:
data-init={ fmt.Sprintf("@get('/games/%s/events',{requestCancellation:'disabled'})", g.ID) }
Prevent Script Duplication on SSE Patches
Use templ.NewOnceHandle() for scripts in components that get patched:
var scriptHandle = templ.NewOnceHandle()
templ MyComponent() {
<div id="my-component">...</div>
@scriptHandle.Once() {
@myScript()
}
}
Conditional Classes with templ.KV
class={
"status status-sm",
templ.KV("status-success", isConnected),
templ.KV("status-error", !isConnected),
}
Datastar SSE Responses
sse := datastar.NewSSE(w, r)
sse.MergeFragmentTempl(components.GameBoard(game))
Tech Stack
| Layer | Technology |
|---|---|
| Templates | templ (type-safe HTML) |
| Reactivity | Datastar (SSE-driven) |
| CSS | TailwindCSS v4 + daisyUI |
| Router | chi/v5 |
| Sessions | scs/v2 |
| Database | SQLite (modernc.org/sqlite) |
| Migrations | goose |
| SQL codegen | sqlc |
| Pub/sub | Embedded NATS |
| Logging | zerolog |
Testing
# All tests
task test
# Single test
go test -run TestDropPiece ./connect4
# With verbose output
go test -v -run TestDropPiece ./connect4
# Test a package
go test ./connect4/...
Use testutil.SetupTestDB() for tests requiring database access.