mirror of
https://github.com/LukasKalbertodt/programmieren-in-rust.git
synced 2024-11-18 02:48:58 +01:00
Add solution for sheet11
This commit is contained in:
parent
b34feb7ad8
commit
b9cb742082
49
aufgaben/sheet11/sol1/loop.rs
Executable file
49
aufgaben/sheet11/sol1/loop.rs
Executable file
@ -0,0 +1,49 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A helper type to visualise drops on the terminal. Isn't it cute? :3
|
||||
struct HappyDrop;
|
||||
|
||||
impl Drop for HappyDrop {
|
||||
fn drop(&mut self) {
|
||||
println!("❤ I have been dropped! ╰( ◕ ᗜ ◕ )╯");
|
||||
}
|
||||
}
|
||||
|
||||
/// We need another wrapper type to avoid having infinitely nested types. This
|
||||
/// is also useful to include the drop-indicator.
|
||||
struct Node {
|
||||
/// Drop-indicator. This could also be sensitive data that requires
|
||||
/// its destructor to be executed (you shouldn't rely on that for safety
|
||||
/// purposes!).
|
||||
///
|
||||
/// Silence warnings, because the `drop()` *is* used.
|
||||
#[allow(dead_code)]
|
||||
happy: HappyDrop,
|
||||
/// An optional shared reference to another node, which can mutated through
|
||||
/// an immutable reference.
|
||||
rc: RefCell<Option<Rc<Node>>>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// We create our first object, without a reference.
|
||||
let one = Rc::new(Node {
|
||||
happy: HappyDrop,
|
||||
rc: RefCell::new(None),
|
||||
});
|
||||
|
||||
// This is the second node which now also owns the first node.
|
||||
let two = Rc::new(Node {
|
||||
happy: HappyDrop,
|
||||
// `two` is owner of `one` now. Note that we `clone()` the Rc around
|
||||
// one to create a new owner, instead of moving the ownership from the
|
||||
// stack frame into two. After this, `two` and the `main` stackframe
|
||||
// are owner of `one`.
|
||||
rc: RefCell::new(Some(one.clone())),
|
||||
});
|
||||
|
||||
// This next line closes the cycle. We can access `one` here, because our
|
||||
// stackframe is owner too! We use the RefCell to mutate the optional
|
||||
// reference to another node and make `one` owner of `two`.
|
||||
*one.rc.borrow_mut() = Some(two);
|
||||
}
|
72
aufgaben/sheet11/sol2/Cargo.lock
generated
Executable file
72
aufgaben/sheet11/sol2/Cargo.lock
generated
Executable file
@ -0,0 +1,72 @@
|
||||
[root]
|
||||
name = "ttt"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"boolinator 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term-painter 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boolinator"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term-painter"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-build"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum boolinator 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum libc 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = "9e030dc72013ed68994d1b2cbf36a94dd0e58418ba949c4b0db7eeb70a7a6352"
|
||||
"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d"
|
||||
"checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a"
|
||||
"checksum term-painter 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ab900bf2f05175932b13d4fc12f8ff09ef777715b04998791ab2c930841e496b"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
9
aufgaben/sheet11/sol2/Cargo.toml
Executable file
9
aufgaben/sheet11/sol2/Cargo.toml
Executable file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "ttt"
|
||||
version = "0.1.0"
|
||||
authors = ["Lukas Kalbertodt <lukas.kalbertodt@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
rand = "0.3.15"
|
||||
term-painter = "0.2.3"
|
||||
boolinator = "2.4.0"
|
16472
aufgaben/sheet11/sol2/out.txt
Normal file
16472
aufgaben/sheet11/sol2/out.txt
Normal file
File diff suppressed because it is too large
Load Diff
129
aufgaben/sheet11/sol2/src/game/cell.rs
Executable file
129
aufgaben/sheet11/sol2/src/game/cell.rs
Executable file
@ -0,0 +1,129 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Cell {
|
||||
Empty,
|
||||
Circle,
|
||||
Cross,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
/// Represents the index of one cell on the board.
|
||||
///
|
||||
/// They can be parsed from the string representation which looks like `"b1"`.
|
||||
/// The letter represents the column, the digit represents the row.
|
||||
pub struct CellId {
|
||||
/// This index is row-major.
|
||||
///
|
||||
/// 0 1 2
|
||||
/// 3 4 5
|
||||
/// 6 7 8
|
||||
index: u8,
|
||||
}
|
||||
|
||||
impl CellId {
|
||||
/// Creates a new ID of the cell in the `row` and `col`.
|
||||
///
|
||||
/// Panics if `row` or `col` are invalid (≥ 3).
|
||||
pub fn new(row: u8, col: u8) -> Self {
|
||||
assert!(row < 3);
|
||||
assert!(col < 3);
|
||||
|
||||
CellId {
|
||||
index: row * 3 + col,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn row(&self) -> u8 {
|
||||
self.index / 3
|
||||
}
|
||||
|
||||
pub fn col(&self) -> u8 {
|
||||
self.index % 3
|
||||
}
|
||||
|
||||
/// Returns a number that can be used to index an array. For every possible
|
||||
/// cell id, this function returns a different number, starting from 0,
|
||||
/// without gaps in between.
|
||||
pub fn index(&self) -> usize {
|
||||
self.index.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CellId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"[{}{}]",
|
||||
(self.col() + 'a' as u8) as char,
|
||||
(self.row() + '1' as u8) as char,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CellId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"CellId {{ {}, {} }}",
|
||||
self.index,
|
||||
self,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// This might be overengineered here. A simpler solution would fit this simple
|
||||
// game better, I think.
|
||||
impl FromStr for CellId {
|
||||
type Err = ParseCellIdError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Check if the string has the correct length and only ascii chars.
|
||||
if s.len() > 2 {
|
||||
return Err(ParseCellIdError::TooManyChars);
|
||||
}
|
||||
if s.len() < 2 {
|
||||
return Err(ParseCellIdError::TooFewChars);
|
||||
}
|
||||
if s.chars().count() != 2 {
|
||||
return Err(ParseCellIdError::NonAscii);
|
||||
}
|
||||
|
||||
// We know the string contains two ASCII-bytes
|
||||
let s = s.to_lowercase().into_bytes();
|
||||
if let 'a' ... 'c' = s[0] as char {
|
||||
if let '1' ... '3' = s[1] as char {
|
||||
let col = s[0] as u8 - 'a' as u8;
|
||||
let row = s[1] as u8 - '1' as u8;
|
||||
Ok(CellId::new(row, col))
|
||||
} else {
|
||||
Err(ParseCellIdError::InvalidRow)
|
||||
}
|
||||
} else {
|
||||
Err(ParseCellIdError::InvalidColumn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ParseCellIdError {
|
||||
TooFewChars,
|
||||
TooManyChars,
|
||||
NonAscii,
|
||||
InvalidColumn,
|
||||
InvalidRow,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseCellIdError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::ParseCellIdError::*;
|
||||
|
||||
match *self {
|
||||
TooFewChars => "too few chars (2 required)",
|
||||
TooManyChars => "too many chars (2 required)",
|
||||
NonAscii => "both chars have to be ascii",
|
||||
InvalidColumn => "column is invalid (has to be 'a', 'b' or 'c')",
|
||||
InvalidRow => "row is invalid (has to be '1', '2' or '3')",
|
||||
}.fmt(f)
|
||||
}
|
||||
}
|
68
aufgaben/sheet11/sol2/src/game/mod.rs
Executable file
68
aufgaben/sheet11/sol2/src/game/mod.rs
Executable file
@ -0,0 +1,68 @@
|
||||
use term_painter::{Color, ToStyle};
|
||||
|
||||
|
||||
mod state;
|
||||
mod cell;
|
||||
|
||||
pub use self::cell::{Cell, CellId};
|
||||
pub use self::state::{GameState, Move};
|
||||
use player::{Player, Role as PlayerRole};
|
||||
|
||||
|
||||
pub fn play<'a>(circle: &'a mut Player, cross: &'a mut Player) {
|
||||
use std::mem::swap;
|
||||
|
||||
let mut state = GameState::new();
|
||||
let mut active = (cross, PlayerRole::Cross);
|
||||
let mut passive = (circle, PlayerRole::Circle);
|
||||
|
||||
while state.status().is_running() {
|
||||
let cell_id = match active.0.next_move(&state, active.1) {
|
||||
Some(m) => m.id(),
|
||||
None => {
|
||||
println!(
|
||||
"Player {} ({}) has given up! :(",
|
||||
active.1,
|
||||
active.0.player_kind()
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
state[cell_id] = active.1.marker();
|
||||
|
||||
println!(
|
||||
"Player {} ({}) marked cell {}:",
|
||||
active.1,
|
||||
active.0.player_kind(),
|
||||
Color::BrightWhite.bold().paint(cell_id),
|
||||
);
|
||||
state.print();
|
||||
println!("");
|
||||
|
||||
swap(&mut active, &mut passive);
|
||||
}
|
||||
|
||||
if let Status::Won(winner) = state.status() {
|
||||
println!(
|
||||
"Player {} ({}) won! :)",
|
||||
winner,
|
||||
passive.0.player_kind(),
|
||||
);
|
||||
} else {
|
||||
// Tie
|
||||
println!("The game tied :'(");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
Running,
|
||||
Tied,
|
||||
Won(PlayerRole),
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn is_running(&self) -> bool {
|
||||
*self == Status::Running
|
||||
}
|
||||
}
|
140
aufgaben/sheet11/sol2/src/game/state.rs
Executable file
140
aufgaben/sheet11/sol2/src/game/state.rs
Executable file
@ -0,0 +1,140 @@
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use term_painter::{Color, ToStyle};
|
||||
|
||||
use game::{Cell, CellId, Status};
|
||||
use player::Role as PlayerRole;
|
||||
|
||||
/// Describes the state of the game.
|
||||
///
|
||||
/// *Note*: we don't derive/implement `Copy` although we could. This is one
|
||||
/// example where not implementing `Copy` is useful: we don't want to
|
||||
/// accidentally copy game state. Thus is shouldn't be easy to do so.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GameState {
|
||||
board: [Cell; 9],
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
/// Creates an empty board.
|
||||
pub fn new() -> Self {
|
||||
GameState {
|
||||
board: [Cell::Empty; 9],
|
||||
}
|
||||
}
|
||||
|
||||
/// Pretty prints the board on the terminal. We don't use the `Display`
|
||||
/// trait, because I'm using colors and terminal formatting.
|
||||
pub fn print(&self) {
|
||||
println!(" a b c ");
|
||||
println!(" +---+---+---+");
|
||||
|
||||
for row in 0..3 {
|
||||
print!("{} ", row + 1);
|
||||
for col in 0..3 {
|
||||
let cell = self[CellId::new(row, col)];
|
||||
let symb = match cell {
|
||||
Cell::Empty => Color::NotSet.paint(" "),
|
||||
Cell::Circle => {
|
||||
PlayerRole::from_marker(cell)
|
||||
.style()
|
||||
.bold()
|
||||
.paint("◯")
|
||||
}
|
||||
Cell::Cross => {
|
||||
PlayerRole::from_marker(cell)
|
||||
.style()
|
||||
.bold()
|
||||
.paint("✗")
|
||||
}
|
||||
};
|
||||
|
||||
print!("| {} ", symb);
|
||||
|
||||
// Trailing pipe
|
||||
if col == 2 {
|
||||
print!("|");
|
||||
}
|
||||
}
|
||||
println!("");
|
||||
println!(" +---+---+---+");
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies that a referenced cell is empty or returns None. This is
|
||||
/// the only way to change mutate this state.
|
||||
pub fn verify_move(&self, id: CellId) -> Option<Move> {
|
||||
match self[id] {
|
||||
Cell::Empty => Some(Move { id: id }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the status of the game (there is a winner, it has tied or
|
||||
/// it is still running)
|
||||
pub fn status(&self) -> Status {
|
||||
macro_rules! check {
|
||||
($( [$a:expr, $b:expr, $c:expr] , )+) => {
|
||||
$(
|
||||
let a = self[CellId::from_str($a).unwrap()];
|
||||
let b = self[CellId::from_str($b).unwrap()];
|
||||
let c = self[CellId::from_str($c).unwrap()];
|
||||
if a != Cell::Empty && a == b && a == c {
|
||||
return Status::Won(PlayerRole::from_marker(a));
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
check![
|
||||
// rows
|
||||
["a1", "a2", "a3"],
|
||||
["b1", "b2", "b3"],
|
||||
["c1", "c2", "c3"],
|
||||
|
||||
// columns
|
||||
["a1", "b1", "c1"],
|
||||
["a2", "b2", "c2"],
|
||||
["a3", "b3", "c3"],
|
||||
|
||||
// diagonals
|
||||
["a1", "b2", "c3"],
|
||||
["a3", "b2", "c1"],
|
||||
];
|
||||
|
||||
if self.board.iter().all(|&c| c != Cell::Empty) {
|
||||
return Status::Tied;
|
||||
}
|
||||
|
||||
Status::Running
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<CellId> for GameState {
|
||||
type Output = Cell;
|
||||
fn index(&self, idx: CellId) -> &Self::Output {
|
||||
&self.board[idx.index()]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<CellId> for GameState {
|
||||
fn index_mut(&mut self, idx: CellId) -> &mut Self::Output {
|
||||
assert_eq!(self.board[idx.index()], Cell::Empty);
|
||||
&mut self.board[idx.index()]
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a valid move (referencing a cell which is empty). The only way
|
||||
/// to modify the game state is by obtaining a `Move` and call
|
||||
/// `set_cell()` on it.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct Move {
|
||||
id: CellId,
|
||||
}
|
||||
|
||||
impl Move {
|
||||
pub fn id(&self) -> CellId {
|
||||
self.id
|
||||
}
|
||||
}
|
60
aufgaben/sheet11/sol2/src/main.rs
Executable file
60
aufgaben/sheet11/sol2/src/main.rs
Executable file
@ -0,0 +1,60 @@
|
||||
extern crate rand;
|
||||
extern crate term_painter;
|
||||
extern crate boolinator;
|
||||
|
||||
mod game;
|
||||
mod player;
|
||||
|
||||
use player::Player;
|
||||
use term_painter::{ToStyle, Color};
|
||||
use std::process;
|
||||
|
||||
|
||||
fn main() {
|
||||
// We do argument parsing manually... which might not be the best idea,
|
||||
// because it's annoying to extend it. It's probably the best to use `clap`
|
||||
// or something like that from the very beginning.
|
||||
let args: Vec<_> = std::env::args().skip(1).take(3).collect();
|
||||
if args.len() != 2 {
|
||||
Color::Red.with(|| {
|
||||
println!("You have to specify the players as command line parameters!");
|
||||
println!("");
|
||||
println!(" ttt <cross-player> <circle-player>");
|
||||
println!("");
|
||||
println!("Player: one of 'human', 'random' or 'smart'.");
|
||||
});
|
||||
}
|
||||
|
||||
let mut cross = get_player(&args[0]).unwrap_or_else(|| process::exit(1));
|
||||
let mut circle = get_player(&args[1]).unwrap_or_else(|| process::exit(1));
|
||||
|
||||
// Pass a mutable reference to the box contents into the main function
|
||||
game::play(&mut *circle, &mut *cross);
|
||||
}
|
||||
|
||||
/// This will return a trait object corresponding to the given player name
|
||||
/// or `None` if the name is invalid. Because this function is returning a
|
||||
/// trait object, we have to box it.
|
||||
fn get_player(name: &str) -> Option<Box<Player>> {
|
||||
use player::human::HumanPlayer;
|
||||
use player::random::RandomPlayer;
|
||||
use player::smart::SmartPlayer;
|
||||
|
||||
// Note that automatic conversions are used here: for example, the type
|
||||
// `Box<HumanPlayer>` is converted to `Box<Player>`. This is the
|
||||
// "type erasure" step.
|
||||
match name {
|
||||
"human" => Some(Box::new(HumanPlayer::new())),
|
||||
"random" => Some(Box::new(RandomPlayer::new())),
|
||||
"smart" => Some(Box::new(SmartPlayer::new())),
|
||||
invalid_name => {
|
||||
Color::Red.with(|| {
|
||||
println!(
|
||||
"'{}' is not a valid player! Use 'human', 'random' or 'smart'.",
|
||||
invalid_name,
|
||||
);
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
50
aufgaben/sheet11/sol2/src/player/human.rs
Executable file
50
aufgaben/sheet11/sol2/src/player/human.rs
Executable file
@ -0,0 +1,50 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use term_painter::{Color, ToStyle};
|
||||
|
||||
use game::{CellId, GameState, Move};
|
||||
use super::{Player, Role};
|
||||
|
||||
pub struct HumanPlayer;
|
||||
|
||||
impl Player for HumanPlayer {
|
||||
fn new() -> Self {
|
||||
HumanPlayer
|
||||
}
|
||||
|
||||
fn player_kind(&self) -> &'static str {
|
||||
"human"
|
||||
}
|
||||
|
||||
fn next_move<'s>(&mut self, state: &'s GameState, _role: Role)
|
||||
-> Option<Move>
|
||||
{
|
||||
loop {
|
||||
print!("Put mark on: ");
|
||||
io::stdout().flush().expect("failed flushing stdout");
|
||||
|
||||
let mut s = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut s)
|
||||
.expect("failed reading from stdin");
|
||||
|
||||
let id: CellId = match s.trim().parse() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
Color::Red.with(|| println!("invalid input: {}", e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match state.verify_move(id) {
|
||||
Some(valid_move) => return Some(valid_move),
|
||||
None => {
|
||||
Color::Red.with(|| {
|
||||
println!("invalid input: field {} is already occupied", id);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
aufgaben/sheet11/sol2/src/player/mod.rs
Executable file
76
aufgaben/sheet11/sol2/src/player/mod.rs
Executable file
@ -0,0 +1,76 @@
|
||||
use std::fmt;
|
||||
|
||||
use term_painter::{Color, Style, ToStyle};
|
||||
|
||||
use game::{Cell, GameState, Move};
|
||||
|
||||
pub mod random;
|
||||
pub mod human;
|
||||
pub mod smart;
|
||||
|
||||
/// A player that may be able to play tic tac toe.
|
||||
pub trait Player {
|
||||
/// Returns a new player. This is useful to have in this trait, but we
|
||||
/// can't use it as virtual method on a trait object. Thus we manually
|
||||
/// exclude it from the set of virtual methods by requiring `Self: Sized`.
|
||||
fn new() -> Self where Self: Sized;
|
||||
|
||||
/// Returns the kind of player in human readable form, e.g.
|
||||
/// "random player".
|
||||
fn player_kind(&self) -> &'static str;
|
||||
|
||||
/// Returns the player's next move or `None` if the player gives up.
|
||||
///
|
||||
/// Note that a valid move has to be returned. The only way to obtain an
|
||||
/// instance of `Move` is to use the `GameState::verify_move()`.
|
||||
fn next_move<'s>(&mut self, state: &'s GameState, role: Role)
|
||||
-> Option<Move>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Role {
|
||||
Circle,
|
||||
Cross,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn marker(&self) -> Cell {
|
||||
match *self {
|
||||
Role::Circle => Cell::Circle,
|
||||
Role::Cross => Cell::Cross,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enemy(&self) -> Role {
|
||||
match *self {
|
||||
Role::Circle => Role::Cross,
|
||||
Role::Cross => Role::Circle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_marker(c: Cell) -> Self {
|
||||
match c {
|
||||
Cell::Circle => Role::Circle,
|
||||
Cell::Cross => Role::Cross,
|
||||
_ => panic!("empty cell cannot be converted into role"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Style {
|
||||
match *self {
|
||||
Role::Circle => Color::BrightGreen.to_style(),
|
||||
Role::Cross => Color::BrightBlue.to_style(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = match *self {
|
||||
Role::Circle => "circle",
|
||||
Role::Cross => "cross",
|
||||
};
|
||||
self.style().paint(s).fmt(f)
|
||||
}
|
||||
}
|
34
aufgaben/sheet11/sol2/src/player/random.rs
Executable file
34
aufgaben/sheet11/sol2/src/player/random.rs
Executable file
@ -0,0 +1,34 @@
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use game::{CellId, GameState, Move};
|
||||
use super::{Player, Role};
|
||||
|
||||
pub struct RandomPlayer;
|
||||
|
||||
impl Player for RandomPlayer {
|
||||
fn new() -> Self {
|
||||
RandomPlayer
|
||||
}
|
||||
|
||||
fn player_kind(&self) -> &'static str {
|
||||
"random player"
|
||||
}
|
||||
|
||||
fn next_move<'s>(&mut self, state: &'s GameState, _role: Role)
|
||||
-> Option<Move>
|
||||
{
|
||||
// If we haven't found a valid move after 1000 attempts, we give up
|
||||
for _ in 0..1000 {
|
||||
let mut rng = thread_rng();
|
||||
let row = rng.gen_range(0, 3);
|
||||
let col = rng.gen_range(0, 3);
|
||||
let id = CellId::new(row, col);
|
||||
|
||||
if let Some(valid_move) = state.verify_move(id) {
|
||||
return Some(valid_move);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
83
aufgaben/sheet11/sol2/src/player/smart.rs
Executable file
83
aufgaben/sheet11/sol2/src/player/smart.rs
Executable file
@ -0,0 +1,83 @@
|
||||
use game::{CellId, GameState, Move, Status};
|
||||
use super::{Player, Role};
|
||||
use super::random::RandomPlayer;
|
||||
use boolinator::Boolinator;
|
||||
|
||||
pub struct SmartPlayer(RandomPlayer);
|
||||
|
||||
impl Player for SmartPlayer {
|
||||
fn new() -> Self {
|
||||
SmartPlayer(RandomPlayer)
|
||||
}
|
||||
|
||||
fn player_kind(&self) -> &'static str {
|
||||
"smart player"
|
||||
}
|
||||
|
||||
fn next_move<'s>(&mut self, state: &GameState, role: Role)
|
||||
-> Option<Move>
|
||||
{
|
||||
|
||||
// Just a few useful things for later...
|
||||
let valid_moves = || (0..9)
|
||||
.map(|i| CellId::new(i / 3, i % 3))
|
||||
.filter_map(move |id| state.verify_move(id));
|
||||
|
||||
let ca = CellId::new(0, 0);
|
||||
let cb = CellId::new(0, 2);
|
||||
let cc = CellId::new(2, 0);
|
||||
let cd = CellId::new(2, 2);
|
||||
|
||||
// We use a few simple tactics here. The rules below are taken from
|
||||
// Wikipedia [1] (note: not all are implemented). This is not a perfect
|
||||
// player! You can beat it.
|
||||
//
|
||||
// [1]: https://en.wikipedia.org/wiki/Tic-tac-toe#Strategy
|
||||
None.or(
|
||||
// Rule 1: If we can win with the next move, we use that move.
|
||||
valid_moves().find(|valid_move| {
|
||||
let mut new_state = state.clone();
|
||||
new_state[valid_move.id()] = role.marker();
|
||||
new_state.status() == Status::Won(role)
|
||||
})
|
||||
|
||||
).or(
|
||||
// Rule 2: If the enemy could win with their next move, we
|
||||
// try to avoid that by marking that field
|
||||
valid_moves().find(|valid_move| {
|
||||
let mut new_state = state.clone();
|
||||
new_state[valid_move.id()] = role.enemy().marker();
|
||||
new_state.status() == Status::Won(role.enemy())
|
||||
})
|
||||
).or(
|
||||
// Rule 5: If the field is empty, we choose a corner cell,
|
||||
// otherwise we will choose the center.
|
||||
if valid_moves().count() == 9 {
|
||||
state.verify_move(CellId::new(0, 0))
|
||||
} else {
|
||||
state.verify_move(CellId::new(1, 1))
|
||||
}
|
||||
).or_else(|| {
|
||||
// Rule 6: If the enemy has a marker in one corner and the
|
||||
// opposite corner is free, put a marker there.
|
||||
let enemy = role.enemy().marker();
|
||||
|
||||
None
|
||||
.or((state[ca] == enemy).and_option(state.verify_move(cd)))
|
||||
.or((state[cd] == enemy).and_option(state.verify_move(ca)))
|
||||
.or((state[cb] == enemy).and_option(state.verify_move(cc)))
|
||||
.or((state[cc] == enemy).and_option(state.verify_move(cb)))
|
||||
}).or(
|
||||
// Rule 7: If there is an empty corner, use it
|
||||
None
|
||||
.or(state.verify_move(ca))
|
||||
.or(state.verify_move(cb))
|
||||
.or(state.verify_move(cc))
|
||||
.or(state.verify_move(cd))
|
||||
).or(
|
||||
// If none of those rules results in a valid move, we just choose
|
||||
// something at random.
|
||||
self.0.next_move(state, role)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user