Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/banteg/crimson/llms.txt

Use this file to discover all available pages before exploring further.

The Crimsonland project enforces strict code style to maintain consistency, readability, and correctness.

Automated Enforcement

Code style is enforced automatically:
# Check all style requirements
just check

# Or individually
uv run ruff check .              # Linting
uv run ty check src tests        # Type checking
uv run lint-imports              # Import contracts
sg scan                          # Structural rules

Linting (Ruff)

The project uses ruff for fast linting.

Configuration

From pyproject.toml:
[tool.ruff]
line-length = 120

[tool.ruff.lint]
extend-select = [
    "B007",   # Loop variable not used
    "B017",   # assertRaises(Exception) is too broad
    "B023",   # Function uses loop variable
    "B904",   # Raise from None
    "COM812", # Trailing comma missing
    "I001",   # Import sorting
    "PGH003", # Type ignore without code
    "PT017",  # pytest.raises() without match
    "RUF012", # Mutable class attributes
    "RUF100", # Unused noqa
    "S113",   # Timeout without timeout
    "S607",   # Starting process with partial path
    "TRY004", # Prefer TypeError for type errors
    "UP035",  # Import from collections.abc
]
extend-ignore = ["E402"]  # Module level import not at top

Auto-Fixing

Most issues can be auto-fixed:
uv run ruff check --fix .

Line Length

Maximum line length is 120 characters. Break long lines:
# Good
def create_weapon(
    weapon_type: str,
    damage: float,
    fire_rate: float,
    ammo_capacity: int,
) -> Weapon:
    return Weapon(weapon_type, damage, fire_rate, ammo_capacity)

# Bad - exceeds 120 characters
def create_weapon(weapon_type: str, damage: float, fire_rate: float, ammo_capacity: int) -> Weapon:
    return Weapon(weapon_type, damage, fire_rate, ammo_capacity)

Import Sorting

Imports are sorted automatically:
# Standard library
import math
import os
from pathlib import Path

# Third-party
import msgspec
import pytest
from construct import Struct

# Local
from crimson.gameplay import GameState
from grim.audio import AudioManager

Type Checking (ty)

The project uses ty for fast type checking.

Type Annotations Required

All functions must have type annotations:
# Good
def calculate_damage(base_damage: float, distance: float) -> float:
    return base_damage * (1.0 - distance / 1000.0)

# Bad - missing annotations
def calculate_damage(base_damage, distance):
    return base_damage * (1.0 - distance / 1000.0)

No Any Without Justification

Avoid Any. If ty complains, fix the schema/boundary/model, don’t cast to Any.
# Bad - dodges typing
from typing import Any

def process_data(data: Any) -> Any:
    return data.get("value")  # Unsafe!

# Good - proper typing
from msgspec import Struct

class GameData(Struct):
    value: float

def process_data(data: GameData) -> float:
    return data.value

Type Guards

Use type guards for conditional typing:
from typing import Union

def process_entity(entity: Union[Player, Creature]) -> None:
    if isinstance(entity, Player):
        # Type narrowed to Player
        entity.award_experience(100)
    else:
        # Type narrowed to Creature
        entity.apply_damage(50)

Import Contracts

Architectural boundaries are enforced by import-linter.

Layer Separation

The engine layer (grim) is independent of game logic (crimson):
# In grim/audio.py - GOOD
from grim.formats import load_wav

# In grim/audio.py - BAD
from crimson.gameplay import GameState  # ❌ Violates boundary
Perk implementations must not import selection/availability logic:
# In crimson/perks/impl/berserk.py - GOOD
from crimson.perks.runtime import PerkEffect

# In crimson/perks/impl/berserk.py - BAD
from crimson.perks.selection import PerkSelector  # ❌ Violates isolation
Selection and availability must not import implementations directly:
# In crimson/perks/selection.py - GOOD
from crimson.perks.registry import get_perk_by_id

# In crimson/perks/selection.py - BAD
from crimson.perks.impl.berserk import BerserkPerk  # ❌ Violates independence

Verify Contracts

uv run lint-imports

Code Patterns

Validate at Boundaries

Validate/parse at boundaries (IO/CLI/JSON/msgpack). Inside the domain, assume typed objects are correct.
# Boundary - parse and validate
def load_config(path: Path) -> Config:
    data = msgspec.json.decode(path.read_bytes(), type=Config)
    # Validation happens here via msgspec
    return data

# Domain - trust typed inputs
def apply_config(config: Config) -> None:
    # No .get(), no isinstance(), no try/except
    # Just use the typed object
    screen_width = config.screen_width
    screen_height = config.screen_height

Fail Fast on Invalid State

# Good - fail fast
def get_weapon(weapon_id: int) -> Weapon:
    if weapon_id not in WEAPON_TABLE:
        raise ValueError(f"Invalid weapon_id: {weapon_id}")
    return WEAPON_TABLE[weapon_id]

# Bad - hides errors with defaults
def get_weapon(weapon_id: int) -> Weapon:
    return WEAPON_TABLE.get(weapon_id, DEFAULT_WEAPON)  # ❌ Silent failure

No Defensive Coding in Domain

Avoid defensive checks deep in gameplay code:
# Bad - defensive checks in domain
def update_creature(creature: Creature, dt: float) -> None:
    if not isinstance(creature, Creature):  # ❌ Unnecessary
        return
    if not hasattr(creature, "health"):     # ❌ Dodge typing
        return
    try:
        creature.move(dt)                   # ❌ Broad exception
    except ValueError:
        pass

# Good - trust types, fail fast
def update_creature(creature: Creature, dt: float) -> None:
    creature.move(dt)  # Type system ensures creature has move()

Typed Domain, Dict at Edges

# Domain - typed objects
class GameState(Struct):
    score: int
    wave: int
    player_health: float

# Edge - convert to dict for serialization
def save_game(state: GameState, path: Path) -> None:
    data = msgspec.to_builtins(state)  # Convert to dict
    path.write_bytes(msgspec.json.encode(data))

Naming Conventions

Variables and Functions

# Snake case for variables and functions
player_health = 100.0
weapon_damage = 75.0

def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
    return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

Classes and Types

# Pascal case for classes
class WeaponSystem:
    pass

class GameState(Struct):
    pass

Constants

# UPPER_SNAKE_CASE for module-level constants
MAX_CREATURES = 100
DEFAULT_DIFFICULTY = "normal"
PI = 3.1415927  # Native f32 precision

Private Members

# Single underscore for internal implementation
class Weapon:
    def __init__(self):
        self._ammo_count = 0  # Internal state
    
    def _reload_internal(self) -> None:  # Internal method
        self._ammo_count = self.max_ammo

Documentation

Docstrings

Use docstrings for public APIs:
def calculate_damage(base_damage: float, distance: float) -> float:
    """Calculate damage with distance falloff.
    
    Args:
        base_damage: Base weapon damage
        distance: Distance to target in pixels
    
    Returns:
        Effective damage after distance falloff
    
    Evidence:
        analysis/ghidra/raw/crimsonland.exe_decompiled.c:5432
    """
    return base_damage * (1.0 - distance / 1000.0)

Parity Evidence

For parity-critical code, document evidence:
def angle_approach(current: float, target: float, rate: float) -> float:
    """Approach target angle at given rate (native parity).
    
    Native behavior: x87 intermediate with f32 storage.
    Evidence: analysis/ghidra/raw/crimsonland.exe_decompiled.c:21767
    Differential session: docs/frida/differential-sessions/session-18.md
    
    Args:
        current: Current angle in radians
        target: Target angle in radians
        rate: Approach rate
    
    Returns:
        New angle after approach
    """
    # Implementation

Comments

When to Comment

Explain why code looks “wrong” due to native parity:
# Native constant - do not normalize to 0.6
# Evidence: analysis/ghidra/raw/crimsonland.exe_decompiled.c:8765
WEAPON_COOLDOWN = 0.6000000238418579

When Not to Comment

Don’t comment obvious code. Let the code speak:
# Bad - comments state the obvious
# Increment score by 100
score += 100

# Good - code is self-documenting
score += ENEMY_KILL_POINTS

Structural Rules (ast-grep)

Structural patterns are enforced via ast-grep:
sg scan                          # Run configured rules
sg test                          # Test rules
Rules are defined in .ast-grep/ (if present) or via CLI.

Next Steps

Float Parity Policy

Master float32 precision requirements

Commit Guidelines

Write meaningful commit messages

Testing Guide

Write well-styled tests

Verification Process

Ensure code passes all checks

Build docs developers (and LLMs) love