You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中合作桌游机器人的约定集(Convention Set)建模方案咨询

Rust中合作桌游机器人的约定集(Convention Set)建模方案咨询

作为同样在Rust里折腾桌游AI的开发者,我特别理解你从OOP转过来建模这类规则时的纠结——既要贴合Rust的 idiom,又要兼顾性能和可维护性。咱们一步步拆解你的问题,结合你的需求(无堆开销、最小化动态分发)给出具体的方案建议。

核心需求回顾

首先明确我们要建模的核心实体:

  • GameState:当前游戏的全局状态(玩家手牌数量、场上牌堆、已执行动作等)
  • ConventionTech:单条约定规则(比如“手牌满5张时必须弃白卡(如果有)”),需要支持3个核心能力:
    1. 给定状态返回该规则允许的合法动作
    2. 判断其他玩家的动作是否符合该规则的触发条件
    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实现Default trait,提供默认的常用约定规则集合,方便快速初始化。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 10:40:27