kukicha-style-guide

star 0

This project is written in Kukicha, a near-superset of Go. Most Go syntax compiles unchanged, but Kukicha-idiomatic forms are required — reviewers reject PRs that use raw Go operators, type syntax, or nil when a Kukicha form exists

duber000 By duber000 schedule Updated 6/4/2026

name: "kukicha-style-guide" description: "This project is written in Kukicha, a near-superset of Go. Most Go syntax compiles unchanged, but Kukicha-idiomatic forms are required — reviewers reject PRs that use raw Go operators, type syntax, or nil when a Kukicha form exists"

Kukicha Style Guide for race-to-the-crystal

This project is written in Kukicha, a near-superset of Go. Most Go syntax compiles unchanged, but Kukicha-idiomatic forms are required — reviewers reject PRs that use raw Go operators, type syntax, or nil when a Kukicha form exists.

Anti-patterns observed in this codebase

These are the recurring mistakes found in the generated .kuki files here. Fix them proactively; don't wait for a lint warning.

1. == and != — the most common mistake

equals and isnt replace every == and !=, not just nil/empty checks.

# WRONG — Go operators leak into non-empty comparisons constantly in this codebase
if gs.phase == enums.PLAYING
if gs.turn_phase != enums.MOVEMENT
if dc.cell_type != enums.GENERATOR
if m == dest
if start == end
if len(parts) != 3

# CORRECT
if gs.phase equals enums.PLAYING
if gs.turn_phase isnt enums.MOVEMENT
if dc.cell_type isnt enums.GENERATOR
if m equals dest
if start equals end
if len(parts) isnt 3

equals/isnt with empty is the one context where this codebase gets it right — apply the same keyword to all other comparisons too.

2. Explicit type in list literal elements

When element type is inferrable from the outer literal, omit it.

# WRONG
var DIRECTIONS = list of DirOffset{
    DirOffset{dx: -1, dy: -1},
    DirOffset{dx: 0,  dy: -1},
}

# CORRECT — inner type is inferred from the outer `list of DirOffset`
var DIRECTIONS = list of DirOffset{
    {dx: -1, dy: -1},
    {dx: 0,  dy: -1},
}

Same rule applies to append calls and function arguments where the type is fixed by the parameter signature.

3. panic — use errors instead

# WRONG — triggers the Kukicha lint warning
panic "health must be >= 0"

# CORRECT — return an error or use onerr
func NewToken(...) (Token, error)
    if health < 0
        return Token{}, error "health must be >= 0"

4. Fallback return for list of byte (and similar slice types)

# WRONG — string-to-slice cast is a Go-ism
result := decode(s) onerr return "" as list of byte

# CORRECT
result := decode(s) onerr return empty list of byte

Quick reference — Kukicha vs Go

Kukicha (write this) Go equivalent (never write in .kuki)
equals ==
isnt !=
and, or, not &&, ||, !
empty nil
list of T []T
map of K to V map[K]V
reference T *T (statically non-empty)
optional reference T *T (may be nil; guard before dereference)
reference of x &x
dereference x *x
name: Type in params name Type (accepted but deprecated)
for x in items for _, x := range items
for i from 0 to N for i := 0; i < N; i++
empty list of T nil / []T(nil)
{field: val} (type inferrable) T{field: val}

Enum patterns used in this project

Variant enums (plain integer-backed, no string value)

enum GamePhase
    SETUP
    PLAYING
    ENDED

Reference variants with the bare name inside the same package (PLAYING) or qualified from another package (enums.PLAYING). Switch with when:

switch gs.phase
    when enums.PLAYING
        ...
    when enums.ENDED
        ...

String-backed enums

enum PlayerColor: string
    CYAN = "cyan"
    MAGENTA = "magenta"

The compiler auto-generates:

  • String() — returns the raw string value (PlayerColor.CYAN.String()"cyan")
  • ParsePlayerColor(s string) (PlayerColor, bool) — parse at boundaries

Prefer switch + when over a chain of if color equals ... for 3+ arms.

onerr patterns

# Propagate — most common
data := fetch.Get(url) onerr return empty, error "{error}"

# Fallback value on expected absence
token := crypto.RandomToken(32) onerr return ""

# Discard (test code only)
doSomething() onerr discard

# Named error for wrapping
data := parse(raw) onerr as e
    return empty, error "parse failed: {e}"

onerr is for genuinely fallible operations (I/O, parse, network, required env var). For lookups with a sensible default, use *Or functions instead: maps.GetOr, slice.GetOr, slice.FirstOr, env.GetOr.

optional reference narrowing

# Always narrow before dereference
cell := brd.get_cell(nx, ny)
if cell equals empty
    continue
dc := dereference cell   # safe: narrowed above

The compiler rejects dereference x unless x is narrowed in the current branch by if x isnt empty or if x equals empty: return/continue/break.

Porting from Python — concept mapping

This codebase is a port of a Python project, and the most damaging mistakes aren't syntax — they're Python concepts carried over intact. The transpiler accepts them, so nothing complains, but they throw away everything Kukicha gives you over Python. Translate the idea, not just the line.

1. Python dict is not map of string to any — it's a struct (or enum)

This is the big one. In Python a dict[str, Any] is the universal container for records, payloads, and kwargs. In Kukicha that role belongs to structs and variant enums. A map of string to any should be rare — reserved for genuinely dynamic, schema-less data (e.g. arbitrary decoded JSON you immediately re-shape).

# WRONG — Python dict carried straight over (the whole project does this)
func best_attack(attacks: list of map of string to any) ChosenAction
    att_id, _ := chosen["attacker_id"]        # stringly-typed key
    dmg := cast.SmartInt(chosen["damage"]) onerr 0   # re-asserting type at use

# CORRECT — the schema already exists in game/schemas.kuki; use it
func best_attack(attacks: list of AttackActionResponse) ChosenAction
    att_id := chosen.attacker_id              # typed field, no key lookup
    dmg := chosen.damage                      # already int, no cast

Tell-tale symptom: a cloud of cast.SmartInt / cast.SmartString / cast.SmartBool and x["key"] lookups. Each one is a type you threw away at the boundary and are now paying to recover. Decode any into a struct once, at the boundary (JSON in, request in), then pass the struct. game/schemas.kuki already defines MoveActionSchema, AttackActionResponse, etc. — the bug is that the live code routes around them through dicts.

2. Stringly-typed dispatch → variant or string-backed enum

# WRONG — Python string-tag dispatch
if action["type"] == "MOVE"
    ...
else if action["type"] == "ATTACK"
    ...

# CORRECT — string-backed enum (parse at the boundary), switch + when
switch action.type
    when ActionType.MOVE
        ...
    when ActionType.ATTACK
        ...

You already do this right for AIStrategyName: string — extend the same instinct to action types, phases, and any other "magic string" set.

3. Python Noneoptional reference T, and prefer non-null by default

Python lets every value be None, so you guard defensively everywhere. Kukicha makes nullability part of the type. A plain reference T is statically guaranteed non-empty — no guard needed. Reach for optional reference T only when absence is real, and narrow once (if x equals empty: return) before dereference. Don't reflexively make everything nullable "to be safe" — that's the Python habit, and it forces guards the type system would otherwise spare you.

4. raise / try/exceptonerr + error returns

Python signals failure by raising. Kukicha returns errors and handles them at the call site with onerr. A bare panic is the carried-over raise — replace it with return ..., error "..." (or onerr at the call site). Reserve panic for truly unrecoverable startup in main/init.

5. Python truthiness → explicit checks

if mylist: and if not s: have no Kukicha equivalent. Write the predicate: if len(mylist) > 0, if s equals "", if ptr isnt empty. (This codebase mostly gets this right — keep it up.)

6. List comprehensions → slice.Map / slice.Filter + pipes

[f(x) for x in xs if p(x)] becomes a pipe of slice.Filter then slice.Map with lambdas — readable and already used here (slice.Filter(effects, (e: ...) => ...)).

Rule of spirit: if a piece of ported code still "thinks in dicts and strings," it isn't finished. The destination is named types — structs, enums, typed fields — checked by the compiler instead of re-validated by hand.

Module layout

shared/enums/   — CellType, GamePhase, TurnPhase, PlayerColor, Direction, …
shared/types/   — TokenID, PlayerID, Position, Pos()
shared/constants/ — board dimensions and tunable constants
shared/errs/    — project error values
game/           — board, token, player, combat, movement, crystal, AI logic
server/         — HTTP/WebSocket handlers, auth, lobby, coordinator

Import cross-package types with an alias to avoid name collisions:

import "race-to-the-crystal/shared/enums" as enums
import "race-to-the-crystal/shared/types" as types
Install via CLI
npx skills add https://github.com/duber000/race-to-the-crystal --skill kukicha-style-guide
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator