flatland_client_lib/
bot.rs1use std::time::Duration;
2use std::time::Instant;
3
4use flatland_protocol::{Intent, Seq};
5use rand::Rng;
6use rand::SeedableRng;
7use rand::rngs::StdRng;
8use tracing::debug;
9
10use crate::session::{PlayConnection, SessionEvent};
11
12#[derive(Debug, Clone)]
13pub struct BotConfig {
14 pub name: String,
15 pub think_interval: Duration,
16}
17
18impl BotConfig {
19 pub fn new(name: impl Into<String>) -> Self {
20 Self {
21 name: name.into(),
22 think_interval: Duration::from_millis(100),
23 }
24 }
25}
26
27#[derive(Debug, Default, Clone)]
28pub struct BotStats {
29 pub ticks_received: u64,
30 pub intents_sent: u64,
31 pub intent_acks: u64,
32 pub last_tick: u64,
33 pub last_entity_count: usize,
34 pub intent_latency_p99_ms: f64,
35}
36
37struct PendingIntent {
38 sent_at: Instant,
39 seq: Seq,
40}
41
42pub struct BotClient<S: PlayConnection> {
43 config: BotConfig,
44 session: S,
45 seq: Seq,
46 stats: BotStats,
47 pending: Vec<PendingIntent>,
48 rng: StdRng,
49}
50
51impl<S: PlayConnection> BotClient<S> {
52 pub fn new(config: BotConfig, session: S) -> Self {
53 let seed = session.entity_id() ^ session.session_id().rotate_left(17);
54 Self {
55 config,
56 session,
57 seq: 0,
58 stats: BotStats::default(),
59 pending: Vec::new(),
60 rng: StdRng::seed_from_u64(seed),
61 }
62 }
63
64 pub fn entity_id(&self) -> u64 {
65 self.session.entity_id()
66 }
67
68 pub fn stats(&self) -> &BotStats {
69 &self.stats
70 }
71
72 pub async fn run_until(&mut self, deadline: Instant) -> anyhow::Result<()> {
73 let mut next_think = tokio::time::Instant::now();
74
75 while Instant::now() < deadline {
76 tokio::select! {
77 _ = tokio::time::sleep_until(next_think) => {
78 self.send_random_intent().await?;
79 next_think = tokio::time::Instant::now() + self.config.think_interval;
80 }
81 event = self.session.next_event() => {
82 match event {
83 Some(ev) => self.handle_event(ev).await?,
84 None => break,
85 }
86 }
87 }
88 }
89
90 self.disconnect();
91 Ok(())
92 }
93
94 pub fn disconnect(&self) {
95 self.session.disconnect();
96 }
97
98 async fn send_random_intent(&mut self) -> anyhow::Result<()> {
99 self.seq += 1;
100 let forward = self.rng.gen_range(-1.0..=1.0);
101 let strafe = self.rng.gen_range(-1.0..=1.0);
102 let seq = self.seq;
103
104 self.session
105 .submit_intent(Intent::Move {
106 entity_id: self.session.entity_id(),
107 forward,
108 strafe,
109 seq,
110 })
111 .await?;
112
113 self.pending.push(PendingIntent {
114 sent_at: Instant::now(),
115 seq,
116 });
117 self.stats.intents_sent += 1;
118 Ok(())
119 }
120
121 async fn handle_event(&mut self, event: SessionEvent) -> anyhow::Result<()> {
122 match event {
123 SessionEvent::Welcome { entity_id, .. } => {
124 debug!(bot = %self.config.name, entity_id, "welcome");
125 }
126 SessionEvent::Tick(delta) => {
127 self.stats.ticks_received += 1;
128 self.stats.last_tick = delta.tick;
129 self.stats.last_entity_count = delta.entities.len();
130 }
131 SessionEvent::IntentAck { seq, .. } => {
132 self.stats.intent_acks += 1;
133 if let Some(idx) = self.pending.iter().position(|p| p.seq == seq) {
134 let pending = self.pending.remove(idx);
135 let ms = pending.sent_at.elapsed().as_secs_f64() * 1000.0;
136 self.stats.intent_latency_p99_ms =
137 self.stats.intent_latency_p99_ms.max(ms);
138 }
139 }
140 SessionEvent::Disconnected => {
141 anyhow::bail!("disconnected");
142 }
143 }
144 Ok(())
145 }
146}