diff --git a/Readme.md b/Readme.md index 17556a9..0791661 100644 --- a/Readme.md +++ b/Readme.md @@ -56,7 +56,7 @@ Check out the other [examples](https://github.com/gyscos/Cursive/tree/master/exa lorem.rs example menubar.rs example select.rs example -list_view.rs example +mines example theme.rs example diff --git a/doc/examples/mines.png b/doc/examples/mines.png new file mode 100644 index 0000000..2328819 Binary files /dev/null and b/doc/examples/mines.png differ diff --git a/examples/mines/Readme.md b/examples/mines/Readme.md new file mode 100644 index 0000000..c05977b --- /dev/null +++ b/examples/mines/Readme.md @@ -0,0 +1 @@ +This is a slightly larger example, showing an implementation of Minesweeper. diff --git a/examples/mines/game.rs b/examples/mines/game.rs new file mode 100644 index 0000000..072c012 --- /dev/null +++ b/examples/mines/game.rs @@ -0,0 +1,86 @@ +use cursive::vec::Vec2; +use rand::{thread_rng, Rng}; +use std::cmp::max; + +#[derive(Clone, Copy)] +pub struct Options { + pub size: Vec2, + pub mines: usize, +} + +#[derive(Clone, Copy)] +pub enum Cell { + Bomb, + Free(usize), +} + +pub struct Board { + pub size: Vec2, + pub cells: Vec, +} + +impl Board { + pub fn new(options: Options) -> Self { + let n_cells = options.size.x * options.size.y; + if options.mines > n_cells { + // Something is wrong here... + // Use different options instead. + return Board::new(Options { + size: options.size, + mines: n_cells, + }); + } + + let mut board = Board { + size: options.size, + cells: vec![Cell::Free(0); n_cells], + }; + + for _ in 0..options.mines { + // Find a free cell to put a bomb + let i = loop { + let i = thread_rng().gen_range(0, n_cells); + + if let Cell::Bomb = board.cells[i] { + continue; + } + + break i; + }; + + // We know we'll go through since that's how we picked i... + board.cells[i] = Cell::Bomb; + // Increase count on adjacent cells + + let pos = Vec2::new(i % options.size.x, i / options.size.x); + for p in board.neighbours(pos) { + if let Some(&mut Cell::Free(ref mut n)) = board.get_mut(p) { + *n += 1; + } + } + } + + board + } + + fn get_mut(&mut self, pos: Vec2) -> Option<&mut Cell> { + self.cell_id(pos).map(move |i| &mut self.cells[i]) + } + + pub fn cell_id(&self, pos: Vec2) -> Option { + if pos < self.size { + Some(pos.x + pos.y * self.size.x) + } else { + None + } + } + + pub fn neighbours(&self, pos: Vec2) -> Vec { + let pos_min = pos.saturating_sub((1, 1)); + let pos_max = (pos + (2, 2)).or_min(self.size); + (pos_min.x..pos_max.x) + .flat_map(|x| (pos_min.y..pos_max.y).map(move |y| Vec2::new(x, y))) + .filter(|&p| p != pos) + .collect() + } +} diff --git a/examples/mines/main.rs b/examples/mines/main.rs new file mode 100644 index 0000000..39a646f --- /dev/null +++ b/examples/mines/main.rs @@ -0,0 +1,288 @@ +extern crate cursive; +extern crate rand; + +mod game; + +use cursive::Cursive; +use cursive::Printer; +use cursive::direction::Direction; +use cursive::event::{Event, EventResult, MouseButton, MouseEvent}; +use cursive::theme::{BaseColor, Color, ColorStyle}; +use cursive::vec::Vec2; +use cursive::views::{Button, Dialog, LinearLayout, SelectView, Panel}; + +fn main() { + let mut siv = Cursive::new(); + + siv.add_layer( + Dialog::new() + .title("Minesweeper") + .padding((2, 2, 1, 1)) + .content( + LinearLayout::vertical() + .child(Button::new_raw(" New game ", show_options)) + .child(Button::new_raw(" Best scores ", |s| { + s.add_layer(Dialog::info("Not yet!").title("Scores")) + })) + .child(Button::new_raw(" Exit ", |s| s.quit())), + ), + ); + + siv.run(); +} + + +fn show_options(siv: &mut Cursive) { + siv.add_layer( + Dialog::new() + .title("Select difficulty") + .content( + SelectView::new() + .item( + "Easy: 8x8, 10 mines", + game::Options { + size: Vec2::new(8, 8), + mines: 10, + }, + ) + .item( + "Medium: 16x16, 40 mines", + game::Options { + size: Vec2::new(16, 16), + mines: 40, + }, + ) + .item( + "Difficult: 24x24, 99 mines", + game::Options { + size: Vec2::new(24, 24), + mines: 99, + }, + ) + .on_submit(|s, option| { + s.pop_layer(); + new_game(s, *option); + }), + ) + .button("Back", |s| s.pop_layer()), + ); +} + +#[derive(Clone, Copy, PartialEq)] +enum Cell { + Visible(usize), + Flag, + Unknown, +} + +struct BoardView { + // Actual board, unknown to the player. + board: game::Board, + + // Visible board + overlay: Vec, + + focused: Option, + missing_mines: usize, +} + +impl BoardView { + pub fn new(options: game::Options) -> Self { + let overlay = vec![Cell::Unknown; options.size.x * options.size.y]; + let board = game::Board::new(options); + BoardView { + board, + overlay, + focused: None, + missing_mines: options.mines, + } + } + + fn get_cell(&self, mouse_pos: Vec2, offset: Vec2) -> Option { + mouse_pos + .checked_sub(offset) + .map(|pos| pos.map_x(|x| x / 2)) + .and_then(|pos| { + if pos.fits_in(self.board.size) { + Some(pos) + } else { + None + } + }) + } + + fn flag(&mut self, pos: Vec2) { + if let Some(i) = self.board.cell_id(pos) { + let new_cell = match self.overlay[i] { + Cell::Unknown => Cell::Flag, + Cell::Flag => Cell::Unknown, + other => other, + }; + self.overlay[i] = new_cell; + } + } + + fn reveal(&mut self, pos: Vec2) -> EventResult { + if let Some(i) = self.board.cell_id(pos) { + if self.overlay[i] != Cell::Unknown { + return EventResult::Consumed(None); + } + + // Action! + match self.board.cells[i] { + game::Cell::Bomb => { + return EventResult::with_cb(|s| { + s.add_layer(Dialog::text("BOOOM").button("Ok", |s| { + s.pop_layer(); + s.pop_layer(); + })); + }) + } + game::Cell::Free(n) => { + self.overlay[i] = Cell::Visible(n); + if n == 0 { + // Reveal all surrounding cells + for p in self.board.neighbours(pos) { + self.reveal(p); + } + } + } + } + } + return EventResult::Consumed(None); + } + + fn auto_reveal(&mut self, pos: Vec2) -> EventResult { + if let Some(i) = self.board.cell_id(pos) { + if let Cell::Visible(n) = self.overlay[i] { + // First: is every possible cell tagged? + let neighbours = self.board.neighbours(pos); + let tagged = neighbours + .iter() + .filter_map(|&pos| self.board.cell_id(pos)) + .map(|i| self.overlay[i]) + .filter(|&cell| cell == Cell::Flag) + .count(); + if tagged != n { + return EventResult::Consumed(None); + } + + for p in neighbours { + let result = self.reveal(p); + if result.has_callback() { + return result; + } + } + } + } + + EventResult::Consumed(None) + } +} + +impl cursive::view::View for BoardView { + fn draw(&self, printer: &Printer) { + for (i, cell) in self.overlay.iter().enumerate() { + let x = (i % self.board.size.x) * 2; + let y = i / self.board.size.x; + + let text = match *cell { + Cell::Unknown => "[]", + Cell::Flag => "()", + Cell::Visible(n) => { + [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n] + } + }; + + let color = match *cell { + Cell::Unknown => Color::RgbLowRes(3, 3, 3), + Cell::Flag => Color::RgbLowRes(4, 4, 2), + Cell::Visible(1) => Color::RgbLowRes(3, 5, 3), + Cell::Visible(2) => Color::RgbLowRes(5, 5, 3), + Cell::Visible(3) => Color::RgbLowRes(5, 4, 3), + Cell::Visible(4) => Color::RgbLowRes(5, 3, 3), + Cell::Visible(5) => Color::RgbLowRes(5, 2, 2), + Cell::Visible(6) => Color::RgbLowRes(5, 0, 1), + Cell::Visible(7) => Color::RgbLowRes(5, 0, 2), + Cell::Visible(8) => Color::RgbLowRes(5, 0, 3), + _ => Color::Dark(BaseColor::White), + }; + + printer.with_color( + ColorStyle::Custom { + back: color, + front: Color::Dark(BaseColor::Black), + }, + |printer| printer.print((x, y), text), + ); + } + } + + fn take_focus(&mut self, _: Direction) -> bool { + true + } + + fn on_event(&mut self, event: Event) -> EventResult { + match event { + Event::Mouse { + offset, + position, + event: MouseEvent::Press(btn), + } => { + // Get cell for position + if let Some(pos) = self.get_cell(position, offset) { + self.focused = Some(pos); + return EventResult::Consumed(None); + } + } + Event::Mouse { + offset, + position, + event: MouseEvent::Release(btn), + } => { + // Get cell for position + if let Some(pos) = self.get_cell(position, offset) { + if self.focused == Some(pos) { + // We got a click here! + match btn { + MouseButton::Left => return self.reveal(pos), + MouseButton::Right => { + self.flag(pos); + return EventResult::Consumed(None); + } + MouseButton::Middle => { + return self.auto_reveal(pos); + } + _ => (), + } + } + + self.focused = None; + } + } + _ => (), + } + + EventResult::Ignored + } + + fn required_size(&mut self, _: Vec2) -> Vec2 { + self.board.size.map_x(|x| 2 * x) + } +} + +fn new_game(siv: &mut Cursive, options: game::Options) { + let board = game::Board::new(options); + + siv.add_layer( + Dialog::new() + .title("Minesweeper") + .content( + LinearLayout::horizontal() + .child(Panel::new(BoardView::new(options))), + ) + .button("Quit game", |s| { + s.pop_layer(); + }), + ); +} diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index 0c73cd5..29ea54a 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -424,7 +424,12 @@ fn initialize_keymap() -> HashMap { // Then add some dynamic ones for c in 1..26 { - map.insert(c, Event::CtrlChar((b'a' - 1 + c as u8) as char)); + let event = match c { + 9 => Event::Key(Key::Tab), + 10 => Event::Key(Key::Enter), + other => Event::CtrlChar((b'a' - 1 + other as u8) as char), + }; + map.insert(c, event); } // Ncurses provides a F1 variable, but no modifiers diff --git a/src/event.rs b/src/event.rs index 4ac63b9..dc49d1c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -76,7 +76,15 @@ impl EventResult { pub fn is_consumed(&self) -> bool { match *self { EventResult::Consumed(_) => true, - EventResult::Ignored => false, + _ => false, + } + } + + /// Returns `true` if `self` contains a callback. + pub fn has_callback(&self) -> bool { + match *self { + EventResult::Consumed(Some(_)) => true, + _ => false, } } diff --git a/src/views/button.rs b/src/views/button.rs index 50ed911..3fac701 100644 --- a/src/views/button.rs +++ b/src/views/button.rs @@ -28,6 +28,15 @@ pub struct Button { impl Button { /// Creates a new button with the given content and callback. pub fn new>(label: S, cb: F) -> Self + where + F: Fn(&mut Cursive) + 'static, + { + let label = label.into(); + Self::new_raw(format!("<{}>", label), cb) + } + + /// Creates a new button without angle brackets. + pub fn new_raw>(label: S, cb: F) -> Self where F: Fn(&mut Cursive) + 'static, { @@ -78,7 +87,7 @@ impl Button { } fn req_size(&self) -> Vec2 { - Vec2::new(2 + self.label.width(), 1) + Vec2::new(self.label.width(), 1) } } @@ -97,10 +106,10 @@ impl View for Button { }; let offset = - HAlign::Center.get_offset(self.label.len() + 2, printer.size.x); + HAlign::Center.get_offset(self.label.len(), printer.size.x); printer.with_color(style, |printer| { - printer.print((offset, 0), &format!("<{}>", self.label)); + printer.print((offset, 0), &self.label); }); } diff --git a/src/xy.rs b/src/xy.rs index 57955f4..35e2f20 100644 --- a/src/xy.rs +++ b/src/xy.rs @@ -21,6 +21,16 @@ impl XY { XY::new(f(self.x), f(self.y)) } + /// Creates a new `XY` by applying `f` to `x`, and carrying `y` over. + pub fn map_x T>(self, f: F) -> Self { + XY::new(f(self.x), self.y) + } + + /// Creates a new `XY` by applying `f` to `y`, and carrying `x` over. + pub fn map_y T>(self, f: F) -> Self { + XY::new(self.x, f(self.y)) + } + /// Destructure self into a pair. pub fn pair(self) -> (T, T) { (self.x, self.y)