一键导入
cairo-contract
Develop Cairo contracts for Eternum with strict TDD workflow - plan, test first, then implement
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Develop Cairo contracts for Eternum with strict TDD workflow - plan, test first, then implement
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Use when authenticating, running, or managing Eternum game agents via the axis CLI, especially on headless servers or VPS.
Emergency defense against early aggression. Use when scouting reveals enemy military units moving toward your base in the early game. Covers unit production priority, positioning, and counterattack timing.
Standard opening strategy for the first 50-100 ticks. Use when the game has just started and you need to establish economy and scouting before making strategic commitments. Covers worker production, initial scouting, and expansion timing.
基于 SOC 职业分类
| name | cairo-contract |
| description | Develop Cairo contracts for Eternum with strict TDD workflow - plan, test first, then implement |
| allowed-tools | ["Bash","Read","Write","Edit","Glob","Grep","TodoWrite"] |
Develop Cairo contracts for the Eternum project using Test-Driven Development. This skill enforces a strict workflow: Plan -> Test First -> Implement -> Review.
NO IMPLEMENTATION CODE WITHOUT A FAILING TEST FIRST.
If you write implementation before tests, delete it and start over.
Before writing any code, understand:
Create a design:
Models Needed:
[Model Name]
- #[key] field_name: type <- what uniquely identifies this?
- data_field: type
Systems Needed:
[System Name]
- function_name(params) -> what it does
Relationships:
System X reads/writes Model A, B
CRITICAL: Before implementing anything, study how similar features are built in the codebase. Don't invent new patterns when established ones exist.
# Find similar models
Grep("similar_concept", "contracts/game/src/models")
# Find similar systems
Grep("similar_action", "contracts/game/src/systems")
# Find config patterns (if your feature needs config)
Grep("set_.*_config", "contracts/game/src/systems/config")
Grep("Config", "contracts/game/src/models/config.cairo")
# Find deployer integration patterns
Grep("set.*Config", "config/deployer/config.ts")
Copy existing patterns exactly before adapting. If set_world_config exists, your set_feature_config should
follow the same structure.
Track each piece with TodoWrite:
TodoWrite([
{content: "Write tests for Model A", status: "pending", activeForm: "Writing tests for Model A"},
{content: "Implement Model A (make tests pass)", status: "pending", activeForm: "Implementing Model A"},
{content: "Write tests for System X", status: "pending", activeForm: "Writing tests for System X"},
{content: "Implement System X (make tests pass)", status: "pending", activeForm: "Implementing System X"},
{content: "Review code", status: "pending", activeForm: "Reviewing code"},
])
contracts/game/src/systems/<feature>/tests/<test_name>.cairo
#[cfg(test)]
mod tests {
use dojo::model::{ModelStorage, ModelStorageTest};
use dojo::world::WorldStorageTrait;
use dojo_cairo_test::{ContractDef, ContractDefTrait, NamespaceDef, TestResource};
use crate::constants::{DEFAULT_NS, DEFAULT_NS_STR};
// Import models (use m_ prefix for TEST_CLASS_HASH)
use crate::models::your_feature::{YourModel, m_YourModel};
// Import systems
use crate::systems::your_feature::contracts::your_systems::{
IYourSystemsDispatcher, IYourSystemsDispatcherTrait, your_systems,
};
// Import testing helpers
use crate::utils::testing::helpers::{
MOCK_TICK_CONFIG, MOCK_CAPACITY_CONFIG, tspawn_world,
tstore_tick_config, tstore_capacity_config,
};
fn namespace_def() -> NamespaceDef {
NamespaceDef {
namespace: DEFAULT_NS_STR(),
resources: [
// Models (use m_ prefix for TEST_CLASS_HASH)
TestResource::Model(m_YourModel::TEST_CLASS_HASH),
// Contracts
TestResource::Contract(your_systems::TEST_CLASS_HASH),
].span(),
}
}
fn contract_defs() -> Span<ContractDef> {
[
ContractDefTrait::new(DEFAULT_NS(), @"your_systems")
.with_writer_of([dojo::utils::bytearray_hash(DEFAULT_NS())].span()),
].span()
}
fn setup() -> (WorldStorage, IYourSystemsDispatcher) {
let mut world = tspawn_world(namespace_def(), contract_defs());
// Store required configs
tstore_tick_config(ref world, MOCK_TICK_CONFIG());
tstore_capacity_config(ref world, MOCK_CAPACITY_CONFIG());
// Get system dispatcher
let (system_addr, _) = world.dns(@"your_systems").unwrap();
let dispatcher = IYourSystemsDispatcher { contract_address: system_addr };
(world, dispatcher)
}
#[test]
#[available_gas(3000000000000)]
fn test_happy_path() {
let (mut world, dispatcher) = setup();
// Arrange
let owner = starknet::contract_address_const::<'owner'>();
starknet::testing::set_contract_address(owner);
// Act
dispatcher.your_action(/* params */);
// Assert
let result: YourModel = world.read_model(/* key */);
assert!(result.field == expected_value, "wrong field value");
}
#[test]
#[available_gas(3000000000000)]
#[should_panic(expected: ('Expected error', 'ENTRYPOINT_FAILED'))]
fn test_fails_when_unauthorized() {
let (mut world, dispatcher) = setup();
let unauthorized = starknet::contract_address_const::<'unauthorized'>();
starknet::testing::set_contract_address(unauthorized);
// This should panic
dispatcher.your_action(/* params */);
}
}
Run the test to verify it fails:
cd contracts/game
scarb test test_your_feature
MANDATORY: Confirm the test fails because the feature is missing, not due to syntax errors.
use starknet::ContractAddress;
use crate::alias::ID;
#[derive(Introspect, Copy, Drop, Serde)]
#[dojo::model]
pub struct YourModel {
#[key]
pub entity_id: ID,
pub owner: ContractAddress,
pub value: u128,
}
#[generate_trait]
pub impl YourModelImpl of YourModelTrait {
fn some_helper(self: @YourModel) -> u128 {
*self.value * 2
}
fn assert_valid(self: @YourModel) {
assert!(*self.value > 0, "value must be positive");
}
}
use crate::alias::ID;
#[starknet::interface]
pub trait IYourSystems<T> {
fn your_action(ref self: T, entity_id: ID, param1: u128);
}
#[dojo::contract]
pub mod your_systems {
use dojo::model::ModelStorage;
use crate::alias::ID;
use crate::constants::DEFAULT_NS;
use crate::models::your_feature::{YourModel, YourModelTrait};
use crate::models::owner::OwnerAddressTrait;
use crate::models::structure::StructureOwnerStoreImpl;
#[abi(embed_v0)]
impl YourSystemsImpl of super::IYourSystems<ContractState> {
fn your_action(ref self: ContractState, entity_id: ID, param1: u128) {
let mut world = self.world(DEFAULT_NS());
// Verify ownership
let owner = StructureOwnerStoreImpl::retrieve(ref world, entity_id);
owner.assert_caller_owner();
// Read current state
let mut model: YourModel = world.read_model(entity_id);
// Validate
model.assert_valid();
// Modify state
model.value = param1;
// Write back
world.write_model(@model);
}
}
}
scarb test test_your_feature
All tests must pass before proceeding.
Clean up code while keeping tests green. Run tests after each change.
Models:
#[derive(Introspect, Copy, Drop, Serde)]#[dojo::model]Systems:
#[starknet::interface] trait#[dojo::contract] moduleassert! with clear error messagesTests:
#[should_panic]Check for:
A feature is not complete until the entire stack is wired up. Before marking done, verify:
contracts/game/src/models/contracts/game/src/systems/models/config.cairoset_<feature>_config() function (pattern: see set_world_config)packages/provider/src/index.tspackages/types/src/types/config/environments/_shared_.tsset<Feature>Config() call in config/deployer/config.tsSelf-Learning Principle: Before implementing a new pattern (like config setters), search for existing examples:
# Find existing config patterns
Grep("set_.*_config", "contracts/game/src/systems")
Grep("setWorldConfig", "config/deployer")
cd contracts/game
scarb test
Use the smallest type that fits the value range. Don't upsize unless aggregation requires it.
| Value Range | Type | Use Case |
|---|---|---|
| 0-255 | u8 | Percentages, small counts |
| 0-65,535 | u16 | Config values, rates per second |
| 0-4.29B | u32 | Aggregated rates, medium counts |
| 0-340 undecillion | u128 | Accumulated totals, balances |
Example: A config field for "points per second" that will never exceed 1000 should be u16, not u32. Only upsize
when values are aggregated (e.g., summing multiple u16 rates into a u32 total).
Scaling and precision belong in the client/config layer, not contracts.
| Layer | Responsibility |
|---|---|
config/environments/_shared_.ts | Apply precision multipliers, human-readable values |
config/deployer/config.ts | Pass scaled values to contracts |
contracts/ | Store and compute with pre-scaled integers |
Bad (precision in contracts):
const PRECISION: u32 = 10;
let scaled = config.rate * PRECISION;
Good (precision in client):
// _shared_.ts
const PRECISION = 10;
const RATE = 50 * PRECISION; // 500
Prefer simple data structures over complex index-based patterns.
Bad (over-engineered):
struct Winners {
count: u32,
}
struct Winner {
index: u32,
wonder_id: ID,
}
// Requires managing count, iterating indices
Good (simple):
struct Winners {
wonder_ids: Array<ID>,
}
// Direct array operations
When ownership or state can change during operations, test all permutations:
#[test]
fn test_ownership_changes_during_operation() {
// Setup: entity owned by player_a
// Act: transfer to player_b mid-operation
// Assert: both states updated correctly
}
For reference on patterns:
# Model design patterns
Skill("book:dojo-model")
# System patterns
Skill("book:dojo-system")
# Test patterns
Skill("book:dojo-test")
# Code review
Skill("book:dojo-review")
contracts/game/src/
├── models/
│ └── your_feature.cairo # Models
├── systems/
│ └── your_feature/
│ ├── contracts.cairo # Systems
│ └── tests/
│ └── test_feature.cairo # Tests
└── utils/testing/helpers.cairo # Test helpers
# Run specific test
scarb test test_your_feature
# Run all tests
scarb test
# Build only
scarb build
// Models
use dojo::model::{ModelStorage, ModelStorageTest};
use crate::alias::ID;
// Systems
use dojo::model::ModelStorage;
use crate::constants::DEFAULT_NS;
// Tests
use dojo_cairo_test::{NamespaceDef, TestResource, ContractDef, ContractDefTrait};
use crate::utils::testing::helpers::{tspawn_world, MOCK_TICK_CONFIG};
From utils/testing/helpers.cairo:
tspawn_world(namespace_def, contract_defs) - Creates test worldMOCK_*_CONFIG() - Config factory functionststore_*_config() - Store config helperstgrant_resources() - Grant resources to entitytspawn_simple_realm() - Create realm for testingtspawn_explorer() - Create explorer for testing1. PLAN
- Understand requirements
- Design models/systems
- Create todo list
2. TEST FIRST (RED)
- Create test file
- Write failing tests
- Verify tests fail
3. IMPLEMENT (GREEN)
- Write minimal code
- Verify tests pass
4. REFACTOR
- Clean up code
- Keep tests green
5. REVIEW
- Code quality check
- Security review
- Full test suite
Remember: Delete any implementation code written before tests.
Don't create complex index-based storage when a simple Array<T> works. Question every level of indirection.
A contract change without deployer integration is incomplete. Always trace the full path: Model → System → Provider → Types → Config → Deployer.
Don't use u32 when u16 fits. Don't use u128 when u32 fits. Larger types cost more gas and obscure intent.
Before creating a new way to do something, search for how it's already done. The codebase is your teacher.