Skip to main content

gpn_tron/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use tokio::{io::AsyncWriteExt as _, net::TcpStream};
4
5use tokio_stream::StreamExt as _;
6use tokio_util::codec::{FramedRead, LinesCodec};
7
8use crate::packets::{ClientPacket, MoveDirection, ServerPacket};
9
10pub mod demobot;
11pub mod errors;
12pub mod packets;
13
14pub struct Bot {
15    instruction_queue: Vec<ClientPacket>,
16    world: WorldMap,
17    username: String,
18    user_id: Option<usize>,
19    current_position: Option<(usize, usize)>,
20}
21
22/// Grid of the playing field with None being empty fields and Some if it's occupied with the occupant's user id.
23pub struct WorldMap {
24    cells: Vec<Vec<Option<usize>>>,
25    width: usize,
26    height: usize,
27}
28
29impl WorldMap {
30    fn new(width: usize, height: usize) -> Self {
31        Self {
32            cells: vec![vec![None; width]; height],
33            width,
34            height,
35        }
36    }
37
38    /// Returns (width, height).
39    pub fn size(&self) -> (usize, usize) {
40        (self.width, self.height)
41    }
42
43    pub fn width(&self) -> usize {
44        self.width
45    }
46
47    pub fn height(&self) -> usize {
48        self.height
49    }
50
51    /// Get returns either None if the field is empty or Some with the id of the user occupying it.
52    pub fn get(&self, x: usize, y: usize) -> Option<usize> {
53        if self.width == 0 || self.height == 0 {
54            return None;
55        }
56        let x = x % self.width;
57        let y = y % self.height;
58        self.cells[y][x]
59    }
60
61    pub fn set(&mut self, x: usize, y: usize, val: usize) {
62        if self.width == 0 || self.height == 0 {
63            return;
64        }
65        let x = x % self.width;
66        let y = y % self.height;
67        self.cells[y][x] = Some(val);
68    }
69
70    /// Clear every cell occupied by `player_id`.
71    pub fn clear_player(&mut self, player_id: usize) {
72        for row in &mut self.cells {
73            for cell in row.iter_mut() {
74                if *cell == Some(player_id) {
75                    *cell = None;
76                }
77            }
78        }
79    }
80
81    /// Returns the (x, y) coordinate after stepping once in a direction.
82    pub fn step(&self, x: usize, y: usize, direction: MoveDirection) -> (usize, usize) {
83        let (width, height) = (self.width, self.height);
84        match direction {
85            MoveDirection::Up => (x, (y + height - 1) % height),
86            MoveDirection::Down => (x, (y + 1) % height),
87            MoveDirection::Left => ((x + width - 1) % width, y),
88            MoveDirection::Right => ((x + 1) % width, y),
89        }
90    }
91
92    /// True if the cell in `direction` from `(x, y)` is occupied.
93    pub fn is_blocked(&self, x: usize, y: usize, direction: MoveDirection) -> bool {
94        let (nx, ny) = self.step(x, y, direction);
95        self.get(nx, ny).is_some()
96    }
97}
98
99impl Bot {
100    fn new(username: impl Into<String>) -> Self {
101        let username = username.into();
102        Self {
103            instruction_queue: Vec::new(),
104            world: WorldMap::new(0, 0),
105            username,
106            user_id: None,
107            current_position: None,
108        }
109    }
110
111    /// Read-only access to the world map.
112    pub fn world(&self) -> &WorldMap {
113        &self.world
114    }
115
116    /// The bot's player id, once received from the server.
117    pub fn user_id(&self) -> Option<usize> {
118        self.user_id
119    }
120
121    /// The bot's current position, once received from the server.
122    pub fn position(&self) -> Option<(usize, usize)> {
123        self.current_position
124    }
125
126    /// Returns every direction whose neighbouring cell is currently empty.
127    pub fn safe_directions(&self) -> Vec<MoveDirection> {
128        let Some((x, y)) = self.current_position else {
129            return Vec::new();
130        };
131        [
132            MoveDirection::Up,
133            MoveDirection::Down,
134            MoveDirection::Left,
135            MoveDirection::Right,
136        ]
137        .into_iter()
138        .filter(|d| !self.world.is_blocked(x, y, *d))
139        .collect()
140    }
141
142    /// True if moving in `direction` from the current position would hit a trail.
143    /// Returns `true` when the position is unknown.
144    pub fn is_blocked(&self, direction: MoveDirection) -> bool {
145        match self.current_position {
146            Some((x, y)) => self.world.is_blocked(x, y, direction),
147            None => true,
148        }
149    }
150
151    /// Write a raw packet to the server.
152    pub fn write_packet(&mut self, packet: ClientPacket) {
153        self.instruction_queue.push(packet);
154    }
155
156    /// Send a message in chat, make sure to not get rate limited.
157    pub fn chat(&mut self, message: impl Into<String>) {
158        self.write_packet(ClientPacket::Chat(message.into()));
159    }
160
161    /// Set a move direction. (I think you should only do one per tick I dir not test several)
162    pub fn do_move(&mut self, direction: MoveDirection) {
163        self.write_packet(ClientPacket::Move(direction));
164    }
165}
166
167pub trait GpnTronBot: Sized {
168    fn handle_packet(&mut self, bot: &mut Bot, packet: ServerPacket);
169
170    #[allow(async_fn_in_trait)]
171    async fn start(
172        &mut self,
173        url: impl std::fmt::Display,
174        port: u16,
175        username: impl Into<String>,
176        password: impl Into<String>,
177    ) -> crate::errors::Result<()> {
178        let addr = format!("{url}:{port}");
179        println!("connecting to {addr}");
180        let stream =
181            TcpStream::connect(&addr)
182                .await
183                .map_err(|source| crate::errors::Error::Connect {
184                    addr: addr.clone(),
185                    source: std::sync::Arc::new(source),
186                })?;
187        let (read, mut write) = stream.into_split();
188        let username = username.into();
189        let join = ClientPacket::join(username.clone(), password).to_string();
190        write.write_all(join.as_bytes()).await?;
191        let decoder = LinesCodec::new();
192        let mut read = FramedRead::new(read, decoder);
193        let mut bot = Bot::new(username);
194        loop {
195            while let Some(next) = read.next().await {
196                let string = next?;
197                let packet = ServerPacket::parse(string)?;
198                match &packet {
199                    ServerPacket::Game { width, height, .. } => {
200                        bot.world = WorldMap::new(*width, *height);
201                        bot.current_position = None;
202                    }
203                    ServerPacket::Position { player_id, x, y } => {
204                        bot.world.set(*x, *y, *player_id);
205                        if Some(*player_id) == bot.user_id {
206                            bot.current_position = Some((*x, *y));
207                        }
208                    }
209                    ServerPacket::Player { id, name } => {
210                        if name == &bot.username {
211                            bot.user_id.replace(*id);
212                        }
213                    }
214                    ServerPacket::Die { user_ids } => {
215                        for id in user_ids {
216                            bot.world.clear_player(*id);
217                            if Some(*id) == bot.user_id {
218                                bot.current_position = None;
219                            }
220                        }
221                    }
222                    _ => {}
223                }
224                self.handle_packet(&mut bot, packet);
225                for packet in &bot.instruction_queue {
226                    write.write_all(packet.to_string().as_bytes()).await?;
227                }
228                bot.instruction_queue.clear();
229            }
230        }
231    }
232}