mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-23 17:35:00 +00:00
Add mines example
This commit is contained in:
parent
6bc63a40c5
commit
70906aa5de
@ -56,7 +56,7 @@ Check out the other [examples](https://github.com/gyscos/Cursive/tree/master/exa
|
||||
<a href="examples/lorem.rs"><img src="doc/examples/lorem.png" alt="lorem.rs example", width="48%" /></a>
|
||||
<a href="examples/menubar.rs"><img src="doc/examples/menubar.png" alt="menubar.rs example", width="48%" /></a>
|
||||
<a href="examples/select.rs"><img src="doc/examples/select.png" alt="select.rs example", width="48%" /></a>
|
||||
<a href="examples/list_view.rs"><img src="doc/examples/list_view.png" alt="list_view.rs example", width="48%" /></a>
|
||||
<a href="examples/mines/"><img src="doc/examples/mines.png" alt="mines example", width="48%" /></a>
|
||||
<a href="examples/theme.rs"><img src="doc/examples/theme.png" alt="theme.rs example", width="48%" /></a>
|
||||
</div>
|
||||
|
||||
|
BIN
doc/examples/mines.png
Normal file
BIN
doc/examples/mines.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
1
examples/mines/Readme.md
Normal file
1
examples/mines/Readme.md
Normal file
@ -0,0 +1 @@
|
||||
This is a slightly larger example, showing an implementation of Minesweeper.
|
86
examples/mines/game.rs
Normal file
86
examples/mines/game.rs
Normal file
@ -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<Cell>,
|
||||
}
|
||||
|
||||
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<usize> {
|
||||
if pos < self.size {
|
||||
Some(pos.x + pos.y * self.size.x)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn neighbours(&self, pos: Vec2) -> Vec<Vec2> {
|
||||
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()
|
||||
}
|
||||
}
|
288
examples/mines/main.rs
Normal file
288
examples/mines/main.rs
Normal file
@ -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<Cell>,
|
||||
|
||||
focused: Option<Vec2>,
|
||||
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<Vec2> {
|
||||
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();
|
||||
}),
|
||||
);
|
||||
}
|
@ -424,7 +424,12 @@ fn initialize_keymap() -> HashMap<i32, Event> {
|
||||
// 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
|
||||
|
10
src/event.rs
10
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,15 @@ pub struct Button {
|
||||
impl Button {
|
||||
/// Creates a new button with the given content and callback.
|
||||
pub fn new<F, S: Into<String>>(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<F, S: Into<String>>(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);
|
||||
});
|
||||
}
|
||||
|
||||
|
10
src/xy.rs
10
src/xy.rs
@ -21,6 +21,16 @@ impl<T> XY<T> {
|
||||
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<F: Fn(T) -> 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<F: Fn(T) -> 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)
|
||||
|
Loading…
Reference in New Issue
Block a user