thedes_app/root/
new_game.rs

1use std::fmt;
2
3use thedes_tui::{
4    cancellability::Cancellable,
5    core::App,
6    info::{self, Info},
7    input::{self, Input},
8    menu::{self, Menu},
9};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13#[error("Truncated {} characters from input", .0)]
14pub struct SetNameError(usize);
15
16#[derive(Debug, Error)]
17pub enum InitError {
18    #[error("Failed to initialize menu")]
19    Menu(
20        #[source]
21        #[from]
22        menu::Error,
23    ),
24    #[error("Failed to create name input")]
25    Name(#[source] input::Error),
26    #[error("Failed to create seed input")]
27    Seed(#[source] input::Error),
28}
29
30#[derive(Debug, Error)]
31pub enum Error {
32    #[error("Failed to run menu")]
33    RunMenu(
34        #[source]
35        #[from]
36        menu::Error,
37    ),
38    #[error("Failed to run name input")]
39    RunName(#[source] input::Error),
40    #[error("Failed to run seed input")]
41    RunSeed(#[source] input::Error),
42    #[error("Failed to display information regarding name input")]
43    EmptyNameInfo(#[source] info::Error),
44    #[error("Failed to display information regarding seed input")]
45    EmptySeedInfo(#[source] info::Error),
46}
47
48#[derive(Debug, Clone, PartialEq, Default)]
49pub struct Form {
50    pub name: String,
51    pub seed: Seed,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum NewGameMenuItem {
56    Create,
57    SetName,
58    SetSeed,
59}
60
61impl fmt::Display for NewGameMenuItem {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.write_str(match self {
64            Self::Create => "Create",
65            Self::SetName => "Set Name",
66            Self::SetSeed => "Set Seed",
67        })
68    }
69}
70
71pub type Seed = u32;
72
73#[derive(Debug, Clone)]
74pub struct Component {
75    form: Form,
76    menu: Menu<NewGameMenuItem, Cancellable>,
77    name_input: Input<fn(char) -> bool, Cancellable>,
78    seed_input: Input<fn(char) -> bool, Cancellable>,
79    empty_name_info: Info,
80    empty_seed_info: Info,
81}
82
83impl Component {
84    pub fn new() -> Result<Self, InitError> {
85        let menu = Menu::from_cancellation(
86            "New Game",
87            [
88                NewGameMenuItem::Create,
89                NewGameMenuItem::SetName,
90                NewGameMenuItem::SetSeed,
91            ],
92            Cancellable::new(false),
93        )?;
94
95        let form = Form::default();
96
97        let result = Input::from_cancellation(
98            input::Config {
99                max: 32,
100                title: "New Game's Name",
101                filter: (|ch| {
102                    ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'
103                }) as fn(char) -> bool,
104            },
105            Cancellable::new(false),
106        );
107        let name_input = result.map_err(InitError::Name)?;
108
109        let result = Input::from_cancellation(
110            input::Config {
111                max: 8,
112                title: "New Game's Seed",
113                filter: (|ch| ch.is_ascii_hexdigit()) as fn(char) -> bool,
114            },
115            Cancellable::new(false),
116        );
117        let seed_input = result.map_err(InitError::Seed)?;
118
119        let empty_name_info = Info::new("Error!", "Game name cannot be empty");
120        let empty_seed_info = Info::new("Error!", "Game seed cannot be empty");
121
122        Ok(Self {
123            menu,
124            form,
125            name_input,
126            seed_input,
127            empty_name_info,
128            empty_seed_info,
129        })
130    }
131
132    pub fn set_seed(&mut self, seed: Seed) -> &mut Self {
133        self.form.seed = seed;
134        self
135    }
136
137    pub fn set_name(
138        &mut self,
139        name: impl Into<String>,
140    ) -> Result<&mut Self, SetNameError> {
141        let name = name.into();
142        let input_max = usize::from(self.name_input.max());
143        if name.len() > input_max {
144            Err(SetNameError(name.len() - input_max))?
145        }
146        self.form.name = name;
147        Ok(self)
148    }
149
150    pub fn form(&self) -> &Form {
151        &self.form
152    }
153
154    pub fn is_cancelling(&self) -> bool {
155        self.menu.is_cancelling() || self.form.name.is_empty()
156    }
157
158    pub fn output(&self) -> Option<&Form> {
159        if self.is_cancelling() {
160            None?
161        }
162        Some(self.form())
163    }
164
165    fn load_form(&mut self) {
166        let seed_digits = format!("{:x}", self.form.seed);
167        let _ = self.seed_input.set_buffer(seed_digits.chars());
168        let _ = self.name_input.set_buffer(self.form.name.chars());
169    }
170
171    pub async fn run(&mut self, app: &mut App) -> Result<(), Error> {
172        self.load_form();
173        self.read_name(app).await?;
174        if self.name_input.is_cancelling() {
175            self.menu.set_cancelling(true);
176        } else {
177            self.menu.set_selected(0)?;
178            self.menu.set_cancelling(false);
179
180            loop {
181                self.menu.run(app).await?;
182                match self.menu.output() {
183                    Some(NewGameMenuItem::Create) => break,
184                    Some(NewGameMenuItem::SetName) => {
185                        self.read_name(app).await?;
186                    },
187                    Some(NewGameMenuItem::SetSeed) => {
188                        self.read_seed(app).await?;
189                    },
190                    None => break,
191                }
192            }
193        }
194        Ok(())
195    }
196
197    async fn read_name(&mut self, app: &mut App) -> Result<(), Error> {
198        loop {
199            self.name_input.run(app).await.map_err(Error::RunName)?;
200            let name = self.name_input.output();
201
202            match name {
203                Some(name) if name.is_empty() => {
204                    self.empty_name_info
205                        .run(app)
206                        .await
207                        .map_err(Error::EmptyNameInfo)?;
208                },
209                Some(name) => {
210                    self.form.name = name;
211                    break;
212                },
213                None => break,
214            }
215        }
216        Ok(())
217    }
218
219    async fn read_seed(&mut self, app: &mut App) -> Result<(), Error> {
220        loop {
221            self.seed_input.run(app).await.map_err(Error::RunSeed)?;
222            let seed = self.seed_input.output();
223
224            match seed.map(|s| Seed::from_str_radix(&s, 16)) {
225                Some(Err(_)) => {
226                    self.empty_seed_info
227                        .run(app)
228                        .await
229                        .map_err(Error::EmptySeedInfo)?;
230                },
231                Some(Ok(seed)) => {
232                    self.form.seed = seed;
233                    break;
234                },
235                None => break,
236            }
237        }
238        Ok(())
239    }
240}
241
242#[cfg(test)]
243mod test {
244    use std::time::Duration;
245
246    use thedes_tui::core::{
247        App,
248        event::Key,
249        geometry::CoordPair,
250        runtime::{Config, device::mock::RuntimeDeviceMock},
251        screen,
252    };
253    use thiserror::Error;
254    use tokio::{task, time::timeout};
255
256    #[derive(Debug, Error)]
257    enum Error {
258        #[error(transparent)]
259        Init(#[from] super::InitError),
260        #[error(transparent)]
261        Run(#[from] super::Error),
262    }
263
264    async fn tui_main(mut app: App) -> Result<Option<super::Form>, Error> {
265        let mut component = super::Component::new()?;
266        component.run(&mut app).await?;
267        Ok(component.output().cloned())
268    }
269
270    #[tokio::test(flavor = "multi_thread")]
271    async fn cancel_first_name_input() {
272        let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
273        let device = device_mock.open();
274        let config = Config::new()
275            .with_screen(
276                screen::Config::new()
277                    .with_canvas_size(CoordPair { y: 22, x: 78 }),
278            )
279            .with_device(device);
280
281        device_mock.input().publish_ok([Key::Esc]);
282
283        let runtime_future = task::spawn(config.run(tui_main));
284        let output = timeout(Duration::from_millis(200), runtime_future)
285            .await
286            .unwrap()
287            .unwrap()
288            .unwrap()
289            .unwrap();
290        assert_eq!(output, None);
291    }
292
293    #[tokio::test(flavor = "multi_thread")]
294    async fn confirm_default_seed() {
295        let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
296        let device = device_mock.open();
297        let config = Config::new()
298            .with_screen(
299                screen::Config::new()
300                    .with_canvas_size(CoordPair { y: 22, x: 78 }),
301            )
302            .with_device(device);
303
304        device_mock.input().publish_ok([
305            Key::Char('w'),
306            Key::Char('0'),
307            Key::Enter,
308        ]);
309
310        let runtime_future = task::spawn(config.run(tui_main));
311        tokio::time::sleep(Duration::from_millis(50)).await;
312        device_mock.input().publish_ok([Key::Enter]);
313
314        let output = timeout(Duration::from_millis(200), runtime_future)
315            .await
316            .unwrap()
317            .unwrap()
318            .unwrap()
319            .unwrap();
320        assert_eq!(
321            output,
322            Some(super::Form { name: "w0".to_owned(), seed: 0 })
323        );
324    }
325
326    #[tokio::test(flavor = "multi_thread")]
327    async fn cancel_menu() {
328        let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
329        let device = device_mock.open();
330        let config = Config::new()
331            .with_screen(
332                screen::Config::new()
333                    .with_canvas_size(CoordPair { y: 22, x: 78 }),
334            )
335            .with_device(device);
336
337        device_mock.input().publish_ok([
338            Key::Char('w'),
339            Key::Char('0'),
340            Key::Enter,
341        ]);
342
343        let runtime_future = task::spawn(config.run(tui_main));
344        tokio::time::sleep(Duration::from_millis(50)).await;
345        device_mock.input().publish_ok([Key::Esc]);
346
347        let output = timeout(Duration::from_millis(200), runtime_future)
348            .await
349            .unwrap()
350            .unwrap()
351            .unwrap()
352            .unwrap();
353        assert_eq!(output, None);
354    }
355
356    #[tokio::test(flavor = "multi_thread")]
357    async fn confirm_custom_seed() {
358        let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
359        let device = device_mock.open();
360        let config = Config::new()
361            .with_screen(
362                screen::Config::new()
363                    .with_canvas_size(CoordPair { y: 22, x: 78 }),
364            )
365            .with_device(device);
366
367        device_mock.input().publish_ok([
368            Key::Char('w'),
369            Key::Char('0'),
370            Key::Enter,
371        ]);
372
373        let runtime_future = task::spawn(config.run(tui_main));
374
375        tokio::time::sleep(Duration::from_millis(50)).await;
376        device_mock.input().publish_ok([Key::Down, Key::Down, Key::Enter]);
377        tokio::time::sleep(Duration::from_millis(50)).await;
378        device_mock.input().publish_ok([
379            Key::Char('5'),
380            Key::Char('a'),
381            Key::Char('9'),
382            Key::Enter,
383        ]);
384
385        tokio::time::sleep(Duration::from_millis(50)).await;
386        device_mock.input().publish_ok([Key::Up, Key::Up, Key::Enter]);
387
388        let output = timeout(Duration::from_millis(200), runtime_future)
389            .await
390            .unwrap()
391            .unwrap()
392            .unwrap()
393            .unwrap();
394        assert_eq!(
395            output,
396            Some(super::Form { name: "w0".to_owned(), seed: 0x5a9 })
397        );
398    }
399
400    #[tokio::test(flavor = "multi_thread")]
401    async fn do_not_allow_empty_name() {
402        let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
403        let device = device_mock.open();
404        let config = Config::new()
405            .with_screen(
406                screen::Config::new()
407                    .with_canvas_size(CoordPair { y: 22, x: 78 }),
408            )
409            .with_device(device);
410
411        let runtime_future = task::spawn(config.run(tui_main));
412        tokio::time::sleep(Duration::from_millis(50)).await;
413
414        device_mock.input().publish_ok([Key::Enter]);
415        tokio::time::sleep(Duration::from_millis(50)).await;
416
417        device_mock.input().publish_ok([Key::Enter]);
418        tokio::time::sleep(Duration::from_millis(50)).await;
419
420        device_mock.input().publish_ok([
421            Key::Char('w'),
422            Key::Char('0'),
423            Key::Enter,
424        ]);
425        tokio::time::sleep(Duration::from_millis(50)).await;
426
427        device_mock.input().publish_ok([Key::Enter]);
428
429        let output = timeout(Duration::from_millis(200), runtime_future)
430            .await
431            .unwrap()
432            .unwrap()
433            .unwrap()
434            .unwrap();
435        assert_eq!(
436            output,
437            Some(super::Form { name: "w0".to_owned(), seed: 0 })
438        );
439    }
440}