严格游戏
启发式框架展示:LLM代理的类型安全游戏
艺术正在将混乱转化为形式,这就是游戏。游戏有规则。——斯蒂芬·桑德海姆
Strictly Games展示了 激励框架 在行动中——展示如何构建 类型安全操作语义 使代理行为无效 不可靠的 在类型层面。
为什么这很重要
传统方法:
Prompt: "Only make legal moves in tic-tac-toe"
Reality: Agent tries position 10 (doesn't exist) or places X on occupied square
Fix: Better prompts, few-shot examples, RLHF激发方法:
// Positions are an enum - position 10 doesn't exist
pub enum Position { TopLeft, TopCenter, ... }
// Moves validated by contracts before application
SquareIsEmpty::check(&action, &game)?;
PlayersTurn::check(&action, &game)?;
// Invalid moves don't compile, can't be represented我们没有建立更好的提示。我们正在构建使正确性不可避免的类型系统。
启发式架构
这个代码库展示了Elicitation框架的四个关键模式:
1. 类型状态机
游戏阶段以类型参数编码,使得非法状态转换成为不可能:
// Phase encoded as type parameter
let game: Game = Game::new();
// start() consumes Setup, returns InProgress
let game: Game = game.start(Player::X);
// make_move() consumes InProgress, returns InProgress or Finished
let result: MoveResult = game.make_move(action)?;无效转换不存在:您无法调用 make_move() 上 Game 或 restart() 上 Game.
2. 一级诉讼
域事件(移动)在应用程序之前独立验证:
// Actions are domain types with validation
let action = Move::new(Player::X, Position::Center);
// Contract-based validation (declarative, composable)
LegalMove::check(&action, &game)?;
// Apply validated action
let result = game.make_move(action)?;动作有自己的语义——它们不仅仅是数据 携带证明的代码.
3. 合同驱动的验证
规则是声明性契约,而不是强制性检查:
/// Precondition: Square must be empty
pub struct SquareIsEmpty;
impl SquareIsEmpty {
pub fn check(mov: &Move, game: &Game) -> Result {
if !game.board().is_empty(mov.position) {
Err(MoveError::SquareOccupied(mov.position))
} else {
Ok(())
}
}
}
// Compose contracts
pub struct LegalMove;
impl LegalMove {
pub fn check(mov: &Move, game: &Game) -> Result {
SquareIsEmpty::check(mov, game)?;
PlayersTurn::check(mov, game)?;
Ok(())
}
}合同包括:
- 声明式 -说明什么必须是真实的,而不是如何检查
- 可组合 -由简单规则构建的复杂规则
- 可验证的 -可以用Kani/Creusot正式证明
- 可重复使用的 -相同的合同适用于各种游戏变体
4. 干净的边界
域逻辑是纯粹的——没有表示,没有I/O,没有框架耦合:
// Domain types know nothing about:
// - How they're rendered (terminal? GUI? web?)
// - How moves arrive (MCP? HTTP? keyboard?)
// - Where state is stored (memory? database?)
// This makes them:
// - Testable in isolation
// - Reusable across contexts
// - Formally verifiable
// - Framework-agnostic游戏逻辑是 纯变换--从一个有效的状态到另一个有效状态,合同强制执行合法性。
教程:实现类型安全游戏
让我们通过tic-tac-toe实现来了解这些模式的实际应用。
步骤1:定义域类型
从不可约域概念开始:
// Players are an enum - only two exist
#[derive(Debug, Clone, Copy, PartialEq, Eq, Elicit)]
pub enum Player { X, O }
// Positions are bounded - only 9 valid squares
#[derive(Debug, Clone, Copy, PartialEq, Eq, Elicit)]
pub enum Position {
TopLeft, TopCenter, TopRight,
MiddleLeft, Center, MiddleRight,
BottomLeft, BottomCenter, BottomRight,
}
// Square state encodes occupancy
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Square {
Empty,
Occupied(Player),
}关键见解: 无效位置(如10或-1) 不存在--它们在类型中无法表示。
步骤2:构建复合类型
将图元组合成更高级别的结构:
// Board is array of squares
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Board {
squares: [Square; 9],
}
impl Board {
/// Check if position is empty
pub fn is_empty(&self, pos: Position) -> bool {
matches!(self.squares[pos as usize], Square::Empty)
}
/// Place a mark
pub fn place(&mut self, pos: Position, player: Player) {
self.squares[pos as usize] = Square::Occupied(player);
}
}步骤3:定义阶段标记
创建零大小的类型来编码游戏阶段:
/// Phase marker: Game is being set up
pub struct Setup;
/// Phase marker: Game is in progress
pub struct InProgress;
/// Phase marker: Game has finished
pub struct Finished;
/// Outcome of finished game
pub enum Outcome {
Winner(Player),
Draw,
}步骤4:实现类型状态游戏
游戏结构在阶段上是通用的:
/// Type-safe game with phase encoded as type parameter
pub struct Game
{
board: Board,
history: Vec,
phase_data: Phase, // Phase-specific data
}
// Setup phase: no current player yet
impl Game {
pub fn new() -> Self {
Self {
board: Board::empty(),
history: Vec::new(),
phase_data: Setup,
}
}
/// Transition: Setup → InProgress
pub fn start(self, first_player: Player) -> Game {
Game {
board: self.board,
history: self.history,
phase_data: InProgress { to_move: first_player },
}
}
}
// InProgress phase: has current player
impl Game {
/// Who moves next?
pub fn to_move(&self) -> Player {
self.phase_data.to_move
}
/// Attempt move - may transition to Finished
pub fn make_move(mut self, action: Move) -> Result {
// Validate via contracts (see Step 5)
LegalMove::check(&action, &self)?;
// Apply move
self.board.place(action.position, action.player);
self.history.push(action);
// Check for game end
if let Some(outcome) = self.check_outcome() {
Ok(MoveResult::Finished(Game {
board: self.board,
history: self.history,
phase_data: Finished { outcome },
}))
} else {
// Toggle player
self.phase_data.to_move = self.phase_data.to_move.opponent();
Ok(MoveResult::Continue(self))
}
}
}关键见解: make_move() 消耗 self--你不能意外地重用过时的游戏状态。
步骤5:定义合同
零大小结构类型的声明性规则:
/// Contract: Square must be empty
pub struct SquareIsEmpty;
impl SquareIsEmpty {
pub fn check(mov: &Move, game: &Game) -> Result {
if !game.board().is_empty(mov.position) {
Err(MoveError::SquareOccupied(mov.position))
} else {
Ok(())
}
}
}
/// Contract: Must be player's turn
pub struct PlayersTurn;
impl PlayersTurn {
pub fn check(mov: &Move, game: &Game) -> Result {
if mov.player != game.to_move() {
Err(MoveError::WrongPlayer { expected: game.to_move(), got: mov.player })
} else {
Ok(())
}
}
}
/// Composite contract: Move is legal
pub struct LegalMove;
impl LegalMove {
pub fn check(mov: &Move, game: &Game) -> Result {
SquareIsEmpty::check(mov, game)?;
PlayersTurn::check(mov, game)?;
Ok(())
}
}第6步:定义行动
移动是域事件,而不仅仅是数据:
/// A move action
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Move {
pub player: Player,
pub position: Position,
}
impl Move {
pub fn new(player: Player, position: Position) -> Self {
Self { player, position }
}
}
/// Move validation error
#[derive(Debug, Clone, Display, Error)]
pub enum MoveError {
#[display("Square {} is occupied", _0)]
SquareOccupied(Position),
#[display("Wrong player: expected {:?}, got {:?}", expected, got)]
WrongPlayer { expected: Player, got: Player },
}步骤7:正式验证(已经完成!)
你不写Kani证明。你已经通过作文进行了正式的验证。
通过使用 #[derive(Elicit)] 在您的域类型上,您继承了Elicitation Framework的 321份经过验证的合同:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Elicit)]
pub enum Position {
TopLeft, TopCenter, TopRight,
MiddleLeft, Center, MiddleRight,
BottomLeft, BottomCenter, BottomRight,
}这给了你什么(由Kani在启发式框架中证明):
✅ 选择返回值验证变量 -代理人只能退回9个职位中的一个\ ✅ 选择排气空间 -所有9个位置都是可枚举的\ ✅ 选择性喷油器 -职位→ 索引映射为1:1\ ✅ FiniteDomain -位置空间是有界的(正好有9个元素)\ ✅ 无无效状态 -位置10不存在,无法构建
组合验证:
// Position is verified (via Elicitation)
// Player is verified (via Elicitation)
// Therefore Move is verified (composition preserves properties)
pub struct Move {
pub player: Player, // ✅ Verified
pub position: Position, // ✅ Verified
}
// Contracts on verified types = verified system
impl LegalMove {
pub fn check(mov: &Move, game: &Game) -> Result {
SquareIsEmpty::check(mov, game)?; // Contract on verified type
PlayersTurn::check(mov, game)?; // Contract on verified type
Ok(())
}
}形式验证的温暖毯通过组合推理延伸到你的整个游戏中。
不需要Kani设置。没有可供书写的校对工具。无验证时间。通过使用框架的已验证原语,您可以免费获得它。
看 FORMAL_VERIFICATION.md 完整解释继承的验证保证。
安装
# Clone the repository
git clone https://github.com/crumplecup/strictly_games.git
cd strictly_games
# Build the server
cargo build --release运行服务器
服务器使用MCP协议通过stdin/stdout进行通信:
# Run directly
cargo run
# Or use the built binary
./target/release/strictly_games服务器将启动并等待stdin上的MCP消息。您将看到:
Starting Strictly Games MCP server
Server ready - connect via MCP protocol连接到克劳德桌面
添加到您的Claude Desktop MCP配置(claude_desktop_config.json):
{
"mcpServers": {
"strictly-games": {
"command": "/path/to/strictly_games/target/release/strictly_games"
}
}
}在macOS上:
# Edit config
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
# Restart Claude Desktop在Linux上:
# Edit config
code ~/.config/Claude/claude_desktop_config.json
# Restart Claude Desktop连接到GitHub Copilot CLI
GitHub Copilot CLI在以下位置使用配置文件 ~/.copilot/mcp-config.json:
# Create/edit the config file
cat > ~/.copilot/mcp-config.json bool {
if game.board[mov.pos] != Empty { return false; }
if mov.player != game.current_player { return false; }
if game.is_finished { return false; }
true
}
// ✅ Elicitation: Validation is declarative contracts
impl LegalMove {
fn check(mov: &Move, game: &Game) -> Result {
SquareIsEmpty::check(mov, game)?;
PlayersTurn::check(mov, game)?;
Ok(())
}
}3.类型是证据
// If you have Game, the game IS finished
// If you have Game, moves ARE legal
// If you have Move that passed LegalMove::check, it IS valid
// The type system is your proof system益处
对于代理商
- 更少的幻觉 -无效的动作不存在产生幻觉
- 更好的理解 -以类型编码的域结构
- 更清晰的错误 -带有上下文的类型安全错误
对于开发者
- 施工正确性 -无效状态无法表示
- 重塑信心 -编译器检查规则更改
- 形式验证 -Kani证明保证属性
- 可再用元件 -合同由游戏组成
用于系统设计
- 可测试性 -纯函数,确定性
- 可维护性 -类型签名是文档
- 可进化性 -在不破坏不变量的情况下添加特征
路线图
第一阶段:基础 (当前)
- ✅ 类型状态机(tic-tac-toe)
- ✅ 基于合同的验证
- ✅ 一级诉讼
- ✅ MCP集成
第二阶段:验证
- 为合同添加Kani证明
- 演示证明成分
- 文档验证模式
第三阶段:扩展游戏
- 二十一点(概率状态)
- Checkers(更大的状态空间)
- 国际象棋(复杂规则)
第四阶段:启发式深潜
- 通过启发进行互动游戏配置
- 锦标赛组织
- 战略启发与比较
代码结构
该实现展示了干净的分离:
src/games/tictactoe/
├── position.rs # Domain primitive: Position enum
├── types.rs # Core types: Player, Square, Board
├── phases.rs # Phase markers: Setup, InProgress, Finished
├── action.rs # Domain events: Move, MoveError
├── contracts.rs # Validation: SquareIsEmpty, PlayersTurn, LegalMove
├── typestate.rs # State machine: Game
with transitions
├── wrapper.rs # Type-erased wrapper for runtime polymorphism
└── mod.rs # Public API and documentation导航指南
- 从types.rs开始 -查看域图元(Player、Square、Board)
- 读取phases.rs -了解状态机阶段
- 研究合同 -请参阅声明性验证
- 探索typestate.rs -查看类型如何强制转换
- 检查包装器.rs -使用AnyGame学习运行时多态性
每个文件都包含大量文档。
许可证
根据以下任一方式获得许可:
- Apache许可证,版本2.0(特许通行证)
- MIT许可证(许可证-麻省理工学院)
由您选择。
致谢
与 引出 框架,证明类型安全代理交互在当今是实用且可实现的。
______________________________________________________________________
“我们正在为代理构建类型安全的操作语义,而不是更好的提示。”
关键见解: 防止代理人犯错的最好方法就是犯错 无法代表 在类型系统中。这就是启发式框架。
