terminity/games/
sttt.rs

1//! A "super tic tac toe". TODO: Explan rules
2
3#![allow(missing_docs)]
4
5use core::slice;
6use std::fmt::Write as FmtWrite;
7use std::ops::{Index, IndexMut};
8use std::time::Duration;
9use std::{
10	fmt::{self, Display, Formatter},
11	io,
12};
13
14use super::Game;
15use crossterm::event::{self, KeyModifiers};
16use crossterm::style::{Color, ContentStyle, Stylize};
17use crossterm::terminal::Clear;
18use crossterm::{cursor, QueueableCommand};
19use terminity_widgets::widgets::frame::Frame;
20use terminity_widgets::widgets::text::{Align, Text};
21use terminity_widgets::{frame, Widget};
22use Tile::*;
23
24#[derive(Debug)]
25pub struct SuperTTT();
26
27impl Game for SuperTTT {
28	fn run(&self, out: &mut dyn io::Write) -> io::Result<()> {
29		GameState::new(out).run()
30	}
31}
32
33type Player = u8;
34
35struct GameState<'a> {
36	pub out: &'a mut dyn io::Write,
37	pub area: Frame<(u8, u8), Zone, GameArea>,
38	pub selected: Selection,
39	pub player: u8,
40	pub text: Text<7>,
41}
42
43#[derive(Debug, Copy, Clone)]
44struct Selection {
45	ty: SelectType,
46	x: u8,
47	y: u8,
48}
49
50#[derive(Debug, Copy, Clone, PartialEq, Eq)]
51enum SelectType {
52	SelCell(u8, u8),
53	Zone,
54}
55
56#[derive(Debug)]
57struct Zone {
58	pub values: [Tile; 9],
59	pub winner: Option<Tile>,
60	pub selected: bool,
61}
62
63impl Widget for Zone {
64	fn displ_line(&self, f: &mut Formatter<'_>, line: usize) -> std::fmt::Result {
65		let mut style = ContentStyle::new();
66		if let Some(winner) = self.winner {
67			style.background_color = Some(winner.get_color());
68			style.foreground_color = Some(Color::Black);
69		}
70		if self.selected {
71			style.background_color = Some(Color::Grey);
72			style.foreground_color = style.background_color;
73		}
74		for cell_x in 0..3 {
75			f.write_fmt(format_args!("{}", &style.apply(' ').to_string()))?;
76			let cell = self[(cell_x, line as u8)];
77			let mut styled_cell = style.apply(cell).bold();
78			if style.foreground_color == None {
79				styled_cell = styled_cell.with(cell.get_color()).bold();
80			}
81			f.write_fmt(format_args!("{}", styled_cell))?;
82		}
83		f.write_fmt(format_args!("{}", &style.apply(' ').to_string()))?;
84		Ok(())
85	}
86	fn size(&self) -> (usize, usize) {
87		(7, 3)
88	}
89}
90
91#[derive(Debug, Default)]
92struct GameArea([Zone; 9]);
93
94impl Index<(u8, u8)> for GameArea {
95	type Output = Zone;
96	fn index(&self, (x, y): (u8, u8)) -> &Self::Output {
97		&self.0[(x + 3 * y) as usize]
98	}
99}
100impl IndexMut<(u8, u8)> for GameArea {
101	fn index_mut(&mut self, (x, y): (u8, u8)) -> &mut Self::Output {
102		&mut self.0[(x + 3 * y) as usize]
103	}
104}
105
106impl GameArea {
107	fn iter(&self) -> slice::Iter<Zone> {
108		self.0.iter()
109	}
110}
111
112#[derive(Debug, Copy, Clone, Eq, PartialEq)]
113#[repr(u8)]
114enum Tile {
115	X = 'x' as u8,
116	O = 'o' as u8,
117	Empty = ' ' as u8,
118}
119
120impl Tile {
121	fn from_player(player: Player) -> Self {
122		if player == 0 {
123			X
124		} else if player == 1 {
125			O
126		} else {
127			panic!("Whut?")
128		}
129	}
130	fn get_color(&self) -> Color {
131		match self {
132			X => Color::Red,
133			O => Color::Blue,
134			Empty => Color::White,
135		}
136	}
137}
138
139impl Display for Tile {
140	fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
141		fmt.write_char(*self as u8 as char)
142	}
143}
144
145impl Default for Zone {
146	fn default() -> Self {
147		Self { values: [Empty; 9], winner: None, selected: false }
148	}
149}
150
151impl Index<(u8, u8)> for Zone {
152	type Output = Tile;
153	fn index(&self, (x, y): (u8, u8)) -> &Self::Output {
154		&self.values[(x + 3 * y) as usize]
155	}
156}
157impl IndexMut<(u8, u8)> for Zone {
158	fn index_mut(&mut self, (x, y): (u8, u8)) -> &mut Self::Output {
159		&mut self.values[(x + 3 * y) as usize]
160	}
161}
162
163impl<'a> GameState<'a> {
164	fn new(out: &'a mut dyn io::Write) -> Self {
165		let mut area: GameArea = Default::default();
166		area[(1, 1)].selected = true;
167		Self {
168			out,
169			selected: Selection { ty: SelectType::Zone, x: 1, y: 1 },
170			player: 0,
171			area: frame!(
172				area => {
173					'0': (0, 0), '1': (1, 0), '2': (2, 0),
174					'3': (0, 1), '4': (1, 1), '5': (2, 1),
175					'6': (0, 2), '7': (1, 2), '8': (2, 2),
176				}
177				"                      #-------#-------#-------#                      "
178				"                      |0000000|1111111|2222222|                      "
179				"                      |0000000|1111111|2222222|                      "
180				"                      |0000000|1111111|2222222|                      "
181				"                      #-------#-------#-------#                      "
182				"                      |3333333|4444444|5555555|                      "
183				"                      |3333333|4444444|5555555|                      "
184				"                      |3333333|4444444|5555555|                      "
185				"                      #-------#-------#-------#                      "
186				"                      |6666666|7777777|8888888|                      "
187				"                      |6666666|7777777|8888888|                      "
188				"                      |6666666|7777777|8888888|                      "
189				"                      #-------#-------#-------#                      "
190			),
191			text: Text {
192				content: [
193					"".to_owned(),
194					"".to_owned(),
195					"Welcome to Super tic tac toe!".to_owned(),
196					"Choose in which zone you will play first. You won't be able to cancel!"
197						.to_owned(),
198					"".to_owned(),
199					"".to_owned(),
200					"".to_owned(),
201				],
202				align: Align::Center,
203				padding: ' ',
204				width: 70,
205			},
206		}
207	}
208
209	fn run(&mut self) -> crossterm::Result<()> {
210		use event::{Event::Key, KeyCode::*, KeyEvent, KeyEventKind::*};
211		self.disp()?;
212		let winner = loop {
213			let coords = (self.selected.x, self.selected.y);
214			self.area[coords].selected = false;
215			match event::read()? {
216				Key(KeyEvent { code: Left, kind: Press, .. }) => {
217					if self.selected.x > 0 {
218						self.selected.x -= 1
219					}
220				}
221				Key(KeyEvent { code: Right, kind: Press, .. }) => {
222					if self.selected.x < 2 {
223						self.selected.x += 1
224					}
225				}
226				Key(KeyEvent { code: Up, kind: Press, .. }) => {
227					if self.selected.y > 0 {
228						self.selected.y -= 1
229					}
230				}
231				Key(KeyEvent { code: Down, kind: Press, .. }) => {
232					if self.selected.y < 2 {
233						self.selected.y += 1
234					}
235				}
236				Key(KeyEvent { code: Enter, kind: Press, .. }) => match self.selected.ty {
237					SelectType::Zone => {
238						self.text.clear();
239						if let Some(winner) = self.area[(self.selected.x, self.selected.y)].winner {
240							self.text[2] = if winner == Empty {
241								format!("Nope, no more free tile over here.")
242							} else {
243								format!("Nope, you can't! The zone is already won by {}.", winner)
244							};
245							self.text[3] = "Choose in which zone you will play.".to_string();
246						} else {
247							self.selected.ty =
248								SelectType::SelCell(self.selected.x, self.selected.y);
249							self.selected.x = 1;
250							self.selected.y = 1;
251							self.text[2] = "Right.".to_owned();
252							self.text[3] = "Which tile?".to_owned();
253						}
254					}
255					SelectType::SelCell(zone_x, zone_y) => {
256						match self.play(zone_x, zone_y, self.selected.x, self.selected.y) {
257							Ok(None) => {
258								self.text.clear();
259								self.text[2] = "Really guys? That's a draw.".to_owned();
260								self.text[3] = "Well played though, that was intense!".to_owned();
261								break Ok(None);
262							}
263							Ok(Some(winner)) => {
264								self.text.clear();
265								self.text[2] =
266									"WOOOOOHOOOOO!!!! Seems like we have a winner!".to_owned();
267								self.text[3] = format!("Well done player {}!", self.player + 1);
268								self.text[4] = format!(
269									"Player {}, maybe you wanna ask a rematch?",
270									(self.player + 1) % 2 + 1
271								);
272								break Ok(Some(winner));
273							}
274							Err(true) => {
275								self.text.clear();
276								self.text[2] = "Done.".to_owned();
277								self.text[3] = "Where to play now?".to_owned();
278								if self.area[(self.selected.x, self.selected.y)].winner == None {
279									self.selected.ty =
280										SelectType::SelCell(self.selected.x, self.selected.y);
281									self.selected.x = 1;
282									self.selected.y = 1;
283								} else {
284									self.selected.ty = SelectType::Zone;
285									self.selected.x = 1;
286									self.selected.y = 1;
287								}
288								self.player = (1 + self.player) % 2;
289							}
290							Err(false) => {
291								self.text.clear();
292								self.text[2] =
293									"Sneaky one, but you can't play where someone already played!"
294										.to_owned();
295								self.text[3] = "Choose on which tile you'll play.".to_string();
296							}
297						}
298					}
299				},
300				Key(KeyEvent { code: Char('c'), kind: Press, modifiers, .. }) => {
301					if modifiers.contains(KeyModifiers::CONTROL) {
302						self.text.clear();
303						self.text[2] = "Exiting the game....".to_owned();
304						break Err(());
305					}
306				}
307				_ => (),
308			}
309			if self.selected.ty == SelectType::Zone {
310				let coords = (self.selected.x, self.selected.y);
311				self.area[coords].selected = true;
312			}
313			self.disp()?;
314		};
315		if winner == Err(()) {
316			return Ok(());
317		}
318		let texts = [
319			"Press any key to exit   ",
320			"Press any key to exit.  ",
321			"Press any key to exit.. ",
322			"Press any key to exit...",
323		];
324		let mut i = 0;
325		loop {
326			self.text[6] = texts[i].to_owned();
327			i = (i + 1) % texts.len();
328			self.disp()?;
329			self.out.queue(crossterm::cursor::Hide)?;
330			self.out.flush()?;
331			if event::poll(Duration::from_millis(600))? {
332				break;
333			}
334		}
335		Ok(())
336	}
337
338	fn play(&mut self, z_x: u8, z_y: u8, cx: u8, cy: u8) -> Result<Option<Player>, bool> {
339		let cell_type = Tile::from_player(self.player);
340
341		let cell = &mut self.area[(z_x, z_y)][(cx, cy)];
342		if *cell != Empty {
343			return Err(false);
344		}
345		*cell = cell_type;
346
347		// Line is the same
348		if cell_type == self.area[(z_x, z_y)][((cx + 1) % 3, cy)]
349			&& cell_type == self.area[(z_x, z_y)][((cx + 2) % 3, cy)]
350		// Column is the same
351		||     cell_type == self.area[(z_x, z_y)][(cx, (cy + 1) % 3)]
352			&& cell_type == self.area[(z_x, z_y)][(cx, (cy + 2) % 3)]
353		// On the first diagonal and same as all on the diagonal
354		||     cx == cy
355			&& cell_type == self.area[(z_x, z_y)][((cx + 1) % 3, (cy + 1) % 3)]
356			&& cell_type == self.area[(z_x, z_y)][((cx + 2) % 3, (cy + 2) % 3)]
357		// On the second diagonal and same as all on the diagonal
358		||     cx + cy == 2
359			&& cell_type == self.area[(z_x, z_y)][((cx + 1) % 3, (cy + 2) % 3)]
360			&& cell_type == self.area[(z_x, z_y)][((cx + 2) % 3, (cy + 1) % 3)]
361		{
362			// Mark zone as winned
363			self.area[(z_x, z_y)].winner = Some(cell_type);
364
365			// If line is the same
366			if Some(cell_type) == self.area[((z_x + 1) % 3, z_y)].winner
367				&& Some(cell_type) == self.area[((z_x + 2) % 3, z_y)].winner
368			// column is the same
369			||     Some(cell_type) == self.area[(z_x, (z_y + 1) % 3)].winner
370				&& Some(cell_type) == self.area[(z_x, (z_y + 2) % 3)].winner
371			// on the first diagonal and same as all on the diagonal
372			||     z_x == z_y
373				&& Some(cell_type) == self.area[((z_x + 1) % 3, (z_y + 1) % 3)].winner
374				&& Some(cell_type) == self.area[((z_x + 2) % 3, (z_y + 2) % 3)].winner
375			// on the second diagonal and same as all on the diagonal
376			||     z_x + z_y == 2
377				&& Some(cell_type) == self.area[((z_x + 1) % 3, (z_y + 2) % 3)].winner
378				&& Some(cell_type) == self.area[((z_x + 2) % 3, (z_y + 1) % 3)].winner
379			{
380				return Ok(Some(self.player));
381			}
382		} else if self.area[(z_x, z_y)].values.iter().all(|c| *c != Empty) {
383			self.area[(z_x, z_y)].winner = Some(Empty);
384		}
385
386		if self.area[(z_x, z_y)].winner != None && self.area.iter().all(|z| z.winner != None) {
387			Ok(None)
388		} else {
389			Err(true)
390		}
391	}
392
393	fn disp(&mut self) -> io::Result<()> {
394		self.text[0] = format!(
395			"Turn to player {} ({})",
396			self.player + 1,
397			Tile::from_player(self.player)
398				.to_string()
399				.with(Tile::from_player(self.player).get_color())
400				.bold()
401		);
402
403		self.out.queue(cursor::MoveTo(0, 0))?;
404		write!(self.out, "{}", self.area)?;
405		self.out.queue(cursor::MoveTo(0, 13))?;
406		write!(self.out, "{}", self.text)?;
407		//.queue(PrintSt(self.text.clone().stylize()))?
408		self.out.queue(Clear(crossterm::terminal::ClearType::FromCursorDown))?;
409
410		if let Selection { ty: SelectType::SelCell(zx, zy), x, y } = self.selected {
411			let (mut x_index, mut y_index) = self.area.find_pos(&(zx, zy)).unwrap();
412			y_index += y as usize;
413			x_index += 1 + 2 * x as usize;
414			self.out.queue(cursor::MoveTo(x_index as u16, y_index as u16))?.queue(cursor::Show)?;
415		} else {
416			self.out.queue(cursor::Hide)?;
417		}
418		self.out.flush()?;
419		Ok(())
420	}
421}