Add solution for sheet11

This commit is contained in:
Lukas Kalbertodt 2017-01-31 16:10:59 +01:00
parent b34feb7ad8
commit b9cb742082
12 changed files with 17242 additions and 0 deletions

49
aufgaben/sheet11/sol1/loop.rs Executable file
View 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
View 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"

View 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

File diff suppressed because it is too large Load Diff

View 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)
}
}

View 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
}
}

View 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
}
}

View 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
}
}
}

View 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;
}
}
}
}
}

View 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)
}
}

View 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
}
}

View 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)
)
}
}