Rust中合作桌游机器人的约定集(Convention Set)建模方案咨询
Rust中合作桌游机器人的约定集(Convention Set)建模方案咨询
作为同样在Rust里折腾桌游AI的开发者,我特别理解你从OOP转过来建模这类规则时的纠结——既要贴合Rust的 idiom,又要兼顾性能和可维护性。咱们一步步拆解你的问题,结合你的需求(无堆开销、最小化动态分发)给出具体的方案建议。
核心需求回顾
首先明确我们要建模的核心实体:
- GameState:当前游戏的全局状态(玩家手牌数量、场上牌堆、已执行动作等)
- ConventionTech:单条约定规则(比如“手牌满5张时必须弃白卡(如果有)”),需要支持3个核心能力:
- 给定状态返回该规则允许的合法动作
- 判断其他玩家的动作是否符合该规则的触发条件
- 根据符合规则的动作返回推断出的信息
- ConventionSet:一组ConventionTech的集合
方案一:枚举(Enum)方案(推荐,更Rustacean+高性能)
Rust的枚举天生适合处理有限的、已知的变体集合,正好匹配你当前的约定规则场景。它完全没有动态分发开销,也不需要堆分配(因为约定规则都是无状态的,用单元变体即可),性能最优,而且逻辑集中,对新手更友好。
1. 先定义基础类型
// 玩家ID类型,用新类型避免混淆 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct PlayerId(u32); // 卡牌类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Card { White, Black, } // 游戏动作类型 #[derive(Debug, Clone, PartialEq, Eq)] enum Action { Draw(PlayerId), Play(PlayerId, Card), Discard(PlayerId, Card), } // 推断出的信息类型 #[derive(Debug, Clone, PartialEq, Eq)] enum InferredInfo { OnlyBlackCardsInHand(PlayerId), HasWhiteCardInHand(PlayerId), // 后续可扩展其他推断类型 } // 玩家可见状态(公共状态+私有手牌) #[derive(Debug, Clone)] struct PlayerState { id: PlayerId, hand_size: usize, // 自己的手牌是完全可见的,其他玩家的手牌只能通过推断 own_hand: Vec<Card>, inferred_hand: Option<Vec<Card>>, // 或者更精细的分布推断,比如黑白卡数量 } // 全局游戏状态 #[derive(Debug, Clone)] struct GameState { players: Vec<PlayerState>, center_cards: Vec<Card>, discard_pile: Vec<Card>, } impl GameState { // 基础规则允许的动作(不考虑约定) fn base_allowed_actions(&self, player_id: PlayerId) -> Vec<Action> { let mut actions = Vec::new(); let player = self.players.iter().find(|p| p.id == player_id).unwrap(); // 基础规则:只要手牌不满5张就能抽卡 if player.hand_size < 5 { actions.push(Action::Draw(player_id)); } // 基础规则:可以打出任意手牌 for &card in &player.own_hand { actions.push(Action::Play(player_id, card)); } // 基础规则:可以丢弃任意手牌 for &card in &player.own_hand { actions.push(Action::Discard(player_id, card)); } actions } // 根据动作获取执行动作的玩家状态 fn get_player_by_action(&self, action: &Action) -> Option<&PlayerState> { let player_id = match action { Action::Draw(id) | Action::Play(id, _) | Action::Discard(id, _) => id, }; self.players.iter().find(|p| p.id == *player_id) } }
2. 约定规则枚举与核心方法实现
// 约定规则枚举(所有规则都在这里定义) #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ConventionTech { /// 手牌满5张时,有白卡必须弃白卡 OnlyDiscardWhiteWhenFullHand, /// 手牌只剩1张时,只有白卡才能抽卡 OnlyDrawWhenSingleWhiteCard, } impl ConventionTech { // 1. 返回当前规则允许的合法动作(过滤基础动作) fn allowed_actions(&self, game_state: &GameState, player_id: PlayerId) -> Vec<Action> { let base_actions = game_state.base_allowed_actions(player_id); let player = game_state.players.iter().find(|p| p.id == player_id).unwrap(); match self { ConventionTech::OnlyDiscardWhiteWhenFullHand => { if player.hand_size != 5 { return base_actions; } // 有白卡的话,过滤掉弃黑卡的动作 let has_white = player.own_hand.contains(&Card::White); if has_white { base_actions.into_iter() .filter(|a| match a { Action::Discard(_, Card::Black) => false, _ => true, }) .collect() } else { base_actions } } ConventionTech::OnlyDrawWhenSingleWhiteCard => { if player.hand_size != 1 { return base_actions; } // 只有白卡才能抽卡,否则过滤抽卡动作 let has_white = player.own_hand.contains(&Card::White); if !has_white { base_actions.into_iter() .filter(|a| !matches!(a, Action::Draw(_))) .collect() } else { base_actions } } } } // 2. 判断动作是否符合当前规则的触发条件(用于推断信息) fn matched(&self, game_state: &GameState, action: &Action) -> bool { let Some(player) = game_state.get_player_by_action(action) else { return false; }; match self { ConventionTech::OnlyDiscardWhiteWhenFullHand => { // 触发条件:手牌满5张且弃了黑卡(说明没有白卡) player.hand_size == 5 && matches!(action, Action::Discard(_, Card::Black)) } ConventionTech::OnlyDrawWhenSingleWhiteCard => { // 触发条件:手牌只剩1张且选择抽卡(说明是白卡) player.hand_size == 1 && matches!(action, Action::Draw(_)) } } } // 3. 返回动作对应的推断信息 fn inferred_information(&self, game_state: &GameState, action: &Action) -> Option<InferredInfo> { if self.matched(game_state, action) { let player_id = match action { Action::Draw(id) | Action::Play(id, _) | Action::Discard(id, _) => id, }; match self { ConventionTech::OnlyDiscardWhiteWhenFullHand => { Some(InferredInfo::OnlyBlackCardsInHand(*player_id)) } ConventionTech::OnlyDrawWhenSingleWhiteCard => { Some(InferredInfo::HasWhiteCardInHand(*player_id)) } } } else { None } } } // 约定集合类型 #[derive(Debug, Clone)] struct ConventionSet { techs: Vec<ConventionTech>, } impl ConventionSet { // 批量获取所有规则允许的合法动作(取交集) fn all_allowed_actions(&self, game_state: &GameState, player_id: PlayerId) -> Vec<Action> { let mut allowed_actions = game_state.base_allowed_actions(player_id); for tech in &self.techs { allowed_actions = tech.allowed_actions(game_state, player_id) .into_iter() .filter(|a| allowed_actions.contains(a)) .collect(); } allowed_actions } // 批量处理其他玩家的动作,获取所有推断信息 fn infer_all(&self, game_state: &GameState, action: &Action) -> Vec<InferredInfo> { self.techs.iter() .filter_map(|tech| tech.inferred_information(game_state, action)) .collect() } }
方案二:Trait方案(适合频繁扩展规则的场景)
如果你预计后续会频繁添加新的约定规则,且希望每个规则的逻辑完全独立,那么Trait方案更符合开闭原则。不过我们可以通过静态单例避免堆分配,减少动态分发的开销。
1. 定义Trait与实现
// 约定规则Trait trait ConventionTech { fn allowed_actions(&self, game_state: &GameState, player_id: PlayerId) -> Vec<Action>; fn matched(&self, game_state: &GameState, action: &Action) -> bool; fn inferred_information(&self, game_state: &GameState, action: &Action) -> Option<InferredInfo>; } // 具体规则实现:手牌满5张必须弃白卡 struct OnlyDiscardWhiteWhenFullHand; impl ConventionTech for OnlyDiscardWhiteWhenFullHand { fn allowed_actions(&self, game_state: &GameState, player_id: PlayerId) -> Vec<Action> { // 逻辑和Enum方案中对应的分支完全一致 let base_actions = game_state.base_allowed_actions(player_id); let player = game_state.players.iter().find(|p| p.id == player_id).unwrap(); if player.hand_size != 5 { return base_actions; } let has_white = player.own_hand.contains(&Card::White); if has_white { base_actions.into_iter() .filter(|a| !matches!(a, Action::Discard(_, Card::Black))) .collect() } else { base_actions } } fn matched(&self, game_state: &GameState, action: &Action) -> bool { let Some(player) = game_state.get_player_by_action(action) else { return false; }; player.hand_size == 5 && matches!(action, Action::Discard(_, Card::Black)) } fn inferred_information(&self, game_state: &GameState, action: &Action) -> Option<InferredInfo> { if self.matched(game_state, action) { let player_id = match action { Action::Draw(id) | Action::Play(id, _) | Action::Discard(id, _) => id, }; Some(InferredInfo::OnlyBlackCardsInHand(*player_id)) } else { None } } } // 具体规则实现:手牌1张时只有白卡才能抽卡 struct OnlyDrawWhenSingleWhiteCard; impl ConventionTech for OnlyDrawWhenSingleWhiteCard { // 实现逻辑同Enum方案对应分支 fn allowed_actions(&self, game_state: &GameState, player_id: PlayerId) -> Vec<Action> { let base_actions = game_state.base_allowed_actions(player_id); let player = game_state.players.iter().find(|p| p.id == player_id).unwrap(); if player.hand_size != 1 { return base_actions; } let has_white = player.own_hand.contains(&Card::White); if !has_white { base_actions.into_iter() .filter(|a| !matches!(a, Action::Draw(_))) .collect() } else { base_actions } } fn matched(&self, game_state: &GameState, action: &Action) -> bool { let Some(player) = game_state.get_player_by_action(action) else { return false; }; player.hand_size == 1 && matches!(action, Action::Draw(_)) } fn inferred_information(&self, game_state: &GameState, action: &Action) -> Option<InferredInfo> { if self.matched(game_state, action) { let player_id = match action { Action::Draw(id) | Action::Play(id, _) | Action::Discard(id, _) => id, }; Some(InferredInfo::HasWhiteCardInHand(*player_id)) } else { None } } } // 静态单例,避免堆分配 static ONLY_DISCARD_WHITE: &dyn ConventionTech = &OnlyDiscardWhiteWhenFullHand; static ONLY_DRAW_WHITE: &dyn ConventionTech = &OnlyDrawWhenSingleWhiteCard; // 约定集合类型 struct ConventionSet { techs: Vec<&'static dyn ConventionTech>, } impl ConventionSet { // 实现方法同Enum方案,遍历所有Trait对象调用方法 fn all_allowed_actions(&self, game_state: &GameState, player_id: PlayerId) -> Vec<Action> { let mut allowed_actions = game_state.base_allowed_actions(player_id); for tech in &self.techs { allowed_actions = tech.allowed_actions(game_state, player_id) .into_iter() .filter(|a| allowed_actions.contains(a)) .collect(); } allowed_actions } fn infer_all(&self, game_state: &GameState, action: &Action) -> Vec<InferredInfo> { self.techs.iter() .filter_map(|tech| tech.inferred_information(game_state, action)) .collect() } }
方案对比与选择建议
| 维度 | Enum方案 | Trait方案 |
|---|---|---|
| 性能 | 静态调度,无动态分发,无堆开销 | 动态分发(虚函数调用),用&'static可避免堆开销 |
| 可维护性 | 规则集中管理,调试方便 | 规则分散在不同类型,扩展无需修改现有代码 |
| Rust适配性 | 完全符合Rust设计哲学(枚举处理有限变体) | 符合OOP直觉,但需要理解Trait对象 |
| 新手友好度 | 更高,逻辑集中,无需理解Trait对象 | 稍低,需要掌握Trait对象、动态分发等概念 |
最终建议
如果你是Rust新手,且你的约定规则集合不会频繁大幅扩展,优先选择Enum方案:它性能最优,代码更集中,更容易调试,完全贴合Rust的设计思路。
如果后续需要频繁添加新的约定规则,再考虑切换到Trait方案,且尽量用&'static dyn ConventionTech来避免不必要的堆分配。
额外小贴士
- GameState的建模要严格区分公共可见状态(其他玩家能看到的信息,比如手牌数量、已执行动作)和私有状态(自己的手牌),推断逻辑只能基于公共状态和动作。
- 推断信息可以设计得更精细,比如用
HandInference { white_count: Option<usize>, black_count: Option<usize> }来表示部分推断的手牌分布,而不是直接存储完整手牌。 - 可以为ConventionSet实现
Defaulttrait,提供默认的常用约定规则集合,方便快速初始化。
内容来源于stack exchange
相关产品推荐
相关产品推荐

