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