Skip to main content

shuyi_core/bots/
order.rs

1//! A candidate order made of ranked groups: the few commands worth trying
2//! first, out of hundreds of legal ones.
3//!
4//! A bot that plans ([`TurnPlan`](super::TurnPlan), [`Rollout`](super::Rollout))
5//! tries only the first `candidates` commands of its [`Order`](super::Order).
6//! The legal order cut at ten holds one piece's attack in every flavour
7//! and nothing else, and a planner that never tries its other pieces, a
8//! step towards the goal or the end of its turn stalls (game ADR-0095).
9//! [`ranked_order`] is the generic cure: the game tags each legal command
10//! with a [`Ranked`] group, value and key (or leaves it out), and gets
11//! back
12//!
13//! 1. the groups in ascending order;
14//! 2. in a group, the best value first (a stable sort: ties keep the
15//!    game's order);
16//! 3. in a group, one per key first: the first command of each key comes
17//!    before any second command of a key already listed (a round-robin:
18//!    all firsts, then all seconds…); a command with no key is never held
19//!    back;
20//! 4. the `last` command (the end of the turn), if any, kept within the
21//!    cap: it takes the place of the last of the first `cap` commands, so
22//!    the cap never cuts it.
23//!
24//! A deck-builder's order is this alone: damage plays best first, one per
25//! target first, then draws, then the rest, ending the turn always kept.
26//! A grid game's is [`TacticsOrder`](crate::threat::TacticsOrder), built on
27//! it.
28//!
29//! ```
30//! use shuyi_core::bots::{Ranked, ranked_order};
31//!
32//! let plays = vec![
33//!     Ranked::new("strike a", 0, (5, 0), Some('a')),
34//!     Ranked::new("draw", 1, (0, 0), None),
35//!     Ranked::new("burn a", 0, (4, 0), Some('a')),
36//!     Ranked::new("strike b", 0, (3, 0), Some('b')),
37//! ];
38//! let order = ranked_order(plays.clone(), Some("end turn"), 10);
39//! assert_eq!(order, ["strike a", "strike b", "burn a", "draw", "end turn"]);
40//! assert_eq!(ranked_order(plays, Some("end turn"), 3), ["strike a", "strike b", "end turn"]);
41//! ```
42
43use alloc::vec::Vec;
44use core::cmp::Reverse;
45
46/// One command of a candidate order, as the game ranks it.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Ranked<C, K> {
49    /// The command.
50    pub command: C,
51    /// Its group: lower groups come first.
52    pub group: u8,
53    /// Its value in its group: higher comes first.
54    pub value: (i64, i64),
55    /// What it is one of, for one per key first (the target it strikes,
56    /// say); `None` is never held back.
57    pub key: Option<K>,
58}
59
60impl<C, K> Ranked<C, K> {
61    /// `command` in `group`, worth `value`, one of `key`.
62    pub const fn new(command: C, group: u8, value: (i64, i64), key: Option<K>) -> Self {
63        Self {
64            command,
65            group,
66            value,
67            key,
68        }
69    }
70}
71
72/// The first `cap` commands of `items` in the order the module's docs
73/// give: groups ascending, the best value first in a group, one per key
74/// first, and `last`, if any, in place of the last of them. `cap` 0
75/// counts as 1.
76pub fn ranked_order<C, K: Eq>(mut items: Vec<Ranked<C, K>>, last: Option<C>, cap: usize) -> Vec<C> {
77    items.sort_by_key(|item| (item.group, Reverse(item.value)));
78    let rounds = rounds(&items);
79    let mut ranked: Vec<(u8, usize, C)> = items
80        .into_iter()
81        .zip(rounds)
82        .map(|(item, round)| (item.group, round, item.command))
83        .collect();
84    ranked.sort_by_key(|&(group, round, _)| (group, round));
85    keep_last(
86        ranked.into_iter().map(|(_, _, command)| command).collect(),
87        last,
88        cap,
89    )
90}
91
92/// Each item's round in its group: how many items of its group and key
93/// come before it (0 with no key).
94fn rounds<C, K: Eq>(items: &[Ranked<C, K>]) -> Vec<usize> {
95    let mut seen: Vec<(u8, &K, usize)> = Vec::new();
96    items
97        .iter()
98        .map(|item| {
99            let Some(key) = &item.key else {
100                return 0;
101            };
102            if let Some(entry) = seen
103                .iter_mut()
104                .find(|(group, seen, _)| *group == item.group && *seen == key)
105            {
106                entry.2 += 1;
107                entry.2
108            } else {
109                seen.push((item.group, key, 0));
110                0
111            }
112        })
113        .collect()
114}
115
116/// The first `cap` of `order` (0 counts as 1), with `last`, if any, in
117/// place of the last of them.
118pub(crate) fn keep_last<C>(mut order: Vec<C>, last: Option<C>, cap: usize) -> Vec<C> {
119    let cap = cap.max(1);
120    match last {
121        Some(last) => {
122            order.truncate(cap - 1);
123            order.push(last);
124        }
125        None => order.truncate(cap),
126    }
127    order
128}
129
130#[cfg(test)]
131mod tests;