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
grim must not import crimson
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 isolated
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/availability independence
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
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
Parity Justification
Non-Obvious Behavior
Temporary Code
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
Explain surprising behavior: # RNG is consumed even when creature doesn't spawn
# This matches native behavior for determinism
self .rng.next_u32()
if not should_spawn:
return None
Mark temporary code clearly: # TODO : Replace with native parity version after session-25
# Current implementation is placeholder
def temp_collision_check ():
pass
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