Mitspieler und der Tourist
Heute bekommst du Gesellschaft. Erst spielen vier Menschen reihum am selben Rechner, dann übernehmen Computergegner drei der Plätze. Dafür lernt das Spiel, jede Handlung als Wert zu beschreiben, und der erste Gegner zieht im Takt: der Tourist, der sich einfach irgendeinen erlaubten Zug aussucht.
Vier an einem Tisch
Ein Wirtschaftsspiel allein ist wie Monopoly ohne Gegner. Bevor die Computer Platz nehmen,
bauen wir das Spiel so um, dass mehrere Menschen reihum am selben Rechner spielen können. Dafür
wandert die Hand in den Spieler, denn jeder hat seine eigene, und jeder bekommt einen
Namen:
pub struct Spieler {pub name: String,pub geld: u32, pub aktien: [u32; 7],pub hand: Vec<Feld>,}⋮pub stapel: Vec<Feld>,pub hand: Vec<Feld>,pub phase: Phase,pub spieler: Spieler,pub spieler: Vec<Spieler>, // Wer gerade dran ist: die Nummer in der Liste der Spieler pub am_zug: usize,
Spiel::neu bekommt jetzt eine Liste von Namen und macht daraus Spieler. Ersetze die
ganze Funktion und füge darunter zwei kleine Helfer ein:
pub fn neu() -> Spiel { // Für jedes Feld ein Plättchen, dann gut mischen let mut stapel = Vec::new(); for reihe in 0..REIHEN { for spalte in 0..SPALTEN { stapel.push(Feld { spalte, reihe }); } } stapel.shuffle(&mut rand::rng()); let mut spiel = Spiel { brett: vec![Belegung::Frei; SPALTEN * REIHEN], stapel, hand: Vec::new(), phase: Phase::Legen, spieler: Spieler { geld: STARTGELD, aktien: [0; 7], }, bank: [AKTIEN_JE_KETTE; 7], }; spiel.nachziehen(); spiel }pub fn neu(namen: &[&str]) -> Spiel { // Für jedes Feld ein Plättchen, dann gut mischen let mut stapel = Vec::new(); for reihe in 0..REIHEN { for spalte in 0..SPALTEN { stapel.push(Feld { spalte, reihe }); } } stapel.shuffle(&mut rand::rng()); let spieler = namen .iter() .map(|name| Spieler { name: name.to_string(), geld: STARTGELD, aktien: [0; 7], hand: Vec::new(), }) .collect(); let mut spiel = Spiel { brett: vec![Belegung::Frei; SPALTEN * REIHEN], stapel, phase: Phase::Legen, spieler, am_zug: 0, bank: [AKTIEN_JE_KETTE; 7], }; // Reihum zieht jeder seine Hand, dann fängt der Erste an for nr in 0..spiel.spieler.len() { spiel.am_zug = nr; spiel.nachziehen(); } spiel.am_zug = 0; spiel } // Der Spieler, der gerade dran ist pub fn aktueller(&self) -> &Spieler { &self.spieler[self.am_zug] } // Dasselbe zum Verändern fn dran(&mut self) -> &mut Spieler { &mut self.spieler[self.am_zug] }// Was steht auf diesem Feld?
namen: &[&str]ist ein Slice: ein Blick auf eine Liste, egal wie lang sie ist. So kann manSpiel::neumit zwei, vier oder sechs Namen aufrufen.mapundcollectverwandeln jeden Namen in einenSpielerund sammeln sie in einemVec. Welchen Typcollectbauen soll, weiß Rust, weil das Ergebnis im Feldspielerlandet.- Reihum ziehen.
nachziehenfüllt immer die Hand des Spielers am Zug. Also setzen wiram_zugnacheinander auf jeden Spieler und ziehen. aktuellerunddranliefern den Spieler am Zug, einmal zum Lesen (&), einmal zum Verändern (&mut). Die veränderbare Variante bleibt privat, damit nur die Regeln Spieler verändern können.
nachziehen arbeitet ab jetzt mit der Hand des Spielers am Zug und wird privat, denn
von außen braucht es niemand mehr:
// Füllt die Hand wieder auf, solange der Stapel reicht pub fn nachziehen(&mut self) { while self.hand.len() < HANDGROESSE { match self.stapel.pop() { Some(feld) => self.hand.push(feld), None => break, } } self.hand.sort_by_key(|feld| (feld.spalte, feld.reihe)); }// Füllt die Hand des Spielers am Zug wieder auf, solange der Stapel reicht fn nachziehen(&mut self) { let nr = self.am_zug; while self.spieler[nr].hand.len() < HANDGROESSE { match self.stapel.pop() { Some(feld) => self.spieler[nr].hand.push(feld), None => break, } } self.spieler[nr] .hand .sort_by_key(|feld| (feld.spalte, feld.reihe)); }// Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht.
Warum nicht einfach self.dran().hand.push(feld)? Weil wir in derselben Zeile auch
self.stapel verändern. Rust erlaubt nicht, dass zwei veränderbare Zugriffe auf
self gleichzeitig laufen, wenn einer davon über eine Methode geht. Über
self.spieler[nr] sieht Rust dagegen, dass es zwei verschiedene Felder sind. Überall
sonst ersetzen wir die alte Hand und das alte Geld durch den Spieler am Zug:
let Some(stelle) = self.hand.iter().position(|&h| h == feld) else {let Some(stelle) = self.aktueller().hand.iter().position(|&h| h == feld) else {⋮self.hand.remove(stelle);self.dran().hand.remove(stelle);⋮self.spieler.aktien[kette.nr()] += 1;self.dran().aktien[kette.nr()] += 1;⋮&& self.spieler.geld >= preis&& self.aktueller().geld >= preis⋮self.spieler.geld -= self.preis(kette); self.spieler.aktien[kette.nr()] += 1;let preis = self.preis(kette); let spieler = self.dran(); spieler.geld -= preis; spieler.aktien[kette.nr()] += 1;
Beim Kaufen rechnen wir den Preis zuerst aus und holen uns erst danach den Spieler zum Verändern.
Andersherum würde Rust sich beschweren, denn während wir den Spieler verändern, dürfen wir nicht
gleichzeitig self.preis fragen. Und dann das Wichtigste: Nach jedem Zug ist der
Nächste dran. Der Rest beim Teilen (%) sorgt dafür, dass nach dem Letzten wieder der
Erste kommt:
// Nachziehen, und der nächste Zug beginnt// Nachziehen, und der Nächste ist dranfn naechster_zug(&mut self) { self.nachziehen();self.am_zug = (self.am_zug + 1) % self.spieler.len();self.phase = Phase::Legen; }
In main.rs legen wir vier Spieler an und zeigen immer die Hand und die Kasse dessen,
der gerade dran ist:
spiel: Spiel::neu(),spiel: Spiel::neu(&["Anna", "Ben", "Carla", "Dario"]),
let kasse = text(format!("Kasse: {}", geld(self.spiel.spieler.geld)))let spieler = self.spiel.aktueller(); let kasse = text(format!("{} · Kasse: {}", spieler.name, geld(spieler.geld)))⋮for &feld in &self.spiel.hand {for &feld in &self.spiel.aktueller().hand {⋮text(self.spiel.spieler.aktien[kette.nr()].to_string())text(self.spiel.aktueller().aktien[kette.nr()].to_string())
Some(feld) if self.spiel.hand.contains(&feld) => mouse::Interaction::Pointer,Some(feld) if self.spiel.aktueller().hand.contains(&feld) => { mouse::Interaction::Pointer }⋮let im_plan = self.spiel.hand.contains(&feld);let im_plan = self.spiel.aktueller().hand.contains(&feld);
cargo run
Oben rechts steht jetzt, wer dran ist, zum Beispiel „Anna · Kasse: 6.000 €“. Nach jedem Zug wechseln Name, Kasse, Hand und Fähnchen. Hol drei Leute an den Rechner und spielt eine Runde, oder spiel alle vier selbst.Wer sitzt am Tisch?
Mit vier Spielern verliert man schnell den Überblick. Links oben kommt deshalb eine Tafel hin: alle Spieler mit Kasse und Aktien, der aktuelle farbig hinterlegt, und darunter ein kleines Protokoll der letzten Handlungen. Das Protokoll ist eine Liste von Texten im Spiel:
pub am_zug: usize,// Was bisher passiert ist, ein Eintrag pro Handlung pub protokoll: Vec<String>,⋮am_zug: 0,protokoll: Vec::new(),
&mut self.spieler[self.am_zug] }// Schreibt ins Protokoll, was der Spieler am Zug getan hat fn notiere(&mut self, was: String) { let eintrag = format!("{} {was}", self.aktueller().name); self.protokoll.push(eintrag); }// Was steht auf diesem Feld?
Dreimal wird notiert: beim Legen, beim Gründen und beim Kaufen.
self.dran().hand.remove(stelle);self.notiere(format!("legt {}", feld.name()));⋮self.dran().aktien[kette.nr()] += 1; }self.notiere(format!("gründet {}", kette.name()));⋮self.bank[kette.nr()] -= 1;self.notiere(format!("kauft {}", kette.name()));
In view bekommt die Tafel rechts einen Nachbarn links. Weil beide Tafeln gleich
aussehen sollen, ziehen wir den Stil in eine Variable tafel_stil. Das geht, weil die
Closure nur farben einfängt, und das ist Copy. Also ist auch die
Closure selbst kopierbar und lässt sich zweimal verwenden:
let tafel = container(ketten) .padding(14) .style(move |_theme: &Theme| container::Style { background: Some(Background::Color(Color { a: 0.85, ..farben.tafel })), border: Border { radius: 10.0.into(), ..Border::default() }, ..container::Style::default() }); let tafelzeile = row![space::horizontal(), tafel].padding([0, 24]);let tafel_stil = move |_theme: &Theme| container::Style { background: Some(Background::Color(Color { a: 0.85, ..farben.tafel })), border: Border { radius: 10.0.into(), ..Border::default() }, ..container::Style::default() }; let tafel = container(ketten).padding(14).style(tafel_stil); // Links oben: wer mitspielt und was zuletzt passiert ist let mut tisch = column![ text("Am Tisch") .size(15) .font(Font { weight: font::Weight::Bold, ..Font::DEFAULT }) .color(farben.schrift) ] .spacing(4); for (nr, spieler) in self.spiel.spieler.iter().enumerate() { let aktien: u32 = spieler.aktien.iter().sum(); let zeile = row![ text(spieler.name.as_str()) .size(13) .color(farben.schrift) .width(60), text(geld(spieler.geld)) .size(13) .color(farben.schrift) .width(80), text(format!("Aktien: {aktien}")) .size(13) .color(farben.schrift), ] .spacing(8); // Wer am Zug ist, bekommt einen farbigen Hintergrund let am_zug = nr == self.spiel.am_zug; let zeile = container(zeile) .padding([3, 8]) .style(move |_theme: &Theme| container::Style { background: am_zug.then_some(Background::Color(Color { a: 0.35, ..farben.akzent })), border: Border { radius: 6.0.into(), ..Border::default() }, ..container::Style::default() }); tisch = tisch.push(zeile); } tisch = tisch.push(text("Zuletzt").size(11).color(farben.beschriftung)); for eintrag in self.spiel.protokoll.iter().rev().take(5) { tisch = tisch.push(text(eintrag.as_str()).size(12).color(farben.schrift)); } let tisch = container(tisch).padding(14).width(270).style(tafel_stil); let tafelzeile = row![tisch, space::horizontal(), tafel].padding([0, 24]);
iter().enumerate()liefert zu jedem Spieler seine Nummer. So erkennen wir, wer am Zug ist.spieler.aktien.iter().sum()zählt alle Aktien zusammen. Rust muss dabei wissen, welcher Zahlentyp herauskommt, deshalb steht: u32an der Variable.am_zug.then_some(…)gibt dem Hintergrund nur dann eine Farbe, wenn der Spieler dran ist. Sonst ist erNone, also durchsichtig.iter().rev().take(5)geht das Protokoll rückwärts durch und nimmt nur die fünf neuesten Einträge. Der neueste steht damit oben.
cargo run
Links oben sitzen jetzt Anna, Ben, Carla und Dario am Tisch. Wer dran ist, ist orange hinterlegt. Darunter läuft das Protokoll mit: „Ben legt 4C“, „Ben gründet Aurora“, „Ben kauft Aurora“.Züge als Daten
Bald sollen Computer mitspielen. Ein Computergegner muss aber wissen, was er überhaupt tun darf,
und er muss es tun können, ohne auf Knöpfe zu klicken. Dafür machen wir aus jeder Handlung einen
Wert: ein enum Zug. Es kommt vor Spieler:
// Alles, was ein Spieler tun kann #[derive(Debug, Clone, Copy, PartialEq)] pub enum Zug { Legen(Feld), Gruenden(Kette), Kaufen(Kette), ZugBeenden, }// Was einem Spieler gehört
Die Prüfungen aus legen ziehen in eine eigene Methode darf_legen
um, genau wie beim Kaufen. So können Oberfläche und Computer fragen, bevor sie handeln. Ersetze
legen komplett durch diese drei Methoden:
// Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht. pub fn legen(&mut self, feld: Feld) -> bool { if self.phase != Phase::Legen { return false; } let Some(stelle) = self.aktueller().hand.iter().position(|&h| h == feld) else { return false; }; let ketten = self.ketten_neben(feld); let rohbau_daneben = feld .nachbarn() .iter() .any(|&nachbar| self.belegung(nachbar) == Belegung::Rohbau); // Zwei Ketten auf einmal berühren: Das ist eine Fusion, die kommt später if ketten.len() > 1 { return false; } // Eine neue Kette gründen geht nur, wenn noch eine frei ist if ketten.is_empty() && rohbau_daneben && self.freie_ketten().is_empty() { return false; } self.dran().hand.remove(stelle); self.notiere(format!("legt {}", feld.name())); self.brett[feld.index()] = Belegung::Rohbau; if let Some(&kette) = ketten.first() { self.ausbreiten(feld, kette); self.nach_dem_legen(); } else if rohbau_daneben { self.phase = Phase::Gruenden(feld); } else { self.nach_dem_legen(); } true }// Darf der Spieler am Zug dieses Plättchen jetzt legen? pub fn darf_legen(&self, feld: Feld) -> bool { if self.phase != Phase::Legen || !self.aktueller().hand.contains(&feld) { return false; } let ketten = self.ketten_neben(feld); // Zwei Ketten auf einmal berühren: Das ist eine Fusion, die kommt später if ketten.len() > 1 { return false; } // Eine neue Kette gründen geht nur, wenn noch eine frei ist let gruendet = ketten.is_empty() && self.rohbau_neben(feld); !(gruendet && self.freie_ketten().is_empty()) } // Hat der Spieler am Zug überhaupt ein Plättchen, das passt? pub fn kann_legen(&self) -> bool { self.aktueller() .hand .iter() .any(|&feld| self.darf_legen(feld)) } // Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht. pub fn legen(&mut self, feld: Feld) -> bool { if !self.darf_legen(feld) { return false; } let ketten = self.ketten_neben(feld); let rohbau_daneben = self.rohbau_neben(feld); self.dran().hand.retain(|&h| h != feld); self.notiere(format!("legt {}", feld.name())); self.brett[feld.index()] = Belegung::Rohbau; if let Some(&kette) = ketten.first() { self.ausbreiten(feld, kette); self.nach_dem_legen(); } else if rohbau_daneben { self.phase = Phase::Gruenden(feld); } else { self.nach_dem_legen(); } true }// Gründet an der wartenden Stelle eine neue Kette
legen ist dadurch kürzer geworden: Erst fragt es darf_legen, dann
handelt es. Statt die Position in der Hand zu suchen, entfernen wir das Plättchen mit
retain, das alle anderen behält. Der Nachbar-Rohbau wird jetzt an zwei Stellen
gebraucht und bekommt deshalb eine eigene Methode:
// Steht neben diesem Feld ein Rohbau? fn rohbau_neben(&self, feld: Feld) -> bool { feld.nachbarn() .iter() .any(|&nachbar| self.belegung(nachbar) == Belegung::Rohbau) }// Welche Ketten grenzen an dieses Feld? Jede nur einmal.
Neu ist auch ein Sonderfall: Was, wenn kein einziges Plättchen passt? Dann darfst du den Zug
beenden, ohne zu legen. Und damit Oberfläche und Computer denselben Eingang benutzen, gibt es
ausfuehren, das jeden Zug an die passende Methode weiterreicht:
pub fn zug_beenden(&mut self) { if let Phase::Kaufen { .. } = self.phase { self.naechster_zug(); } }pub fn zug_beenden(&mut self) { match self.phase { Phase::Kaufen { .. } => self.naechster_zug(), // Passt kein Plättchen, geht es ohne Legen weiter Phase::Legen if !self.kann_legen() => { self.notiere(String::from("kann nichts legen")); self.nach_dem_legen(); } _ => {} } } // Führt einen Zug aus, egal ob er vom Menschen oder vom Computer kommt pub fn ausfuehren(&mut self, zug: Zug) { match zug { Zug::Legen(feld) => { self.legen(feld); } Zug::Gruenden(kette) => self.gruenden(kette), Zug::Kaufen(kette) => self.kaufen(kette), Zug::ZugBeenden => self.zug_beenden(), } }// Gibt es schon Ketten, darf man Aktien kaufen. Sonst ist der Zug vorbei.
Das match in zug_beenden hat einen Zweig mit Bedingung:
Phase::Legen if !self.kann_legen() greift nur beim Legen, und nur wenn nichts passt.
Der Zweig _ => {} tut in allen anderen Fällen nichts.
In main.rs ersetzen drei Briefe durch einen einzigen:
Message::Spielzug transportiert einen ganzen Zug.
use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Zug};⋮Gruenden(Kette), ZugBeenden, Kaufen(Kette),Spielzug(Zug),}⋮Message::Geklickt(feld) => { self.spiel.legen(feld); }Message::Geklickt(feld) => self.spiel.ausfuehren(Zug::Legen(feld)),Message::TagNacht => self.nacht = !self.nacht,Message::Gruenden(kette) => self.spiel.gruenden(kette), Message::ZugBeenden => self.spiel.zug_beenden(), Message::Kaufen(kette) => self.spiel.kaufen(kette),Message::Spielzug(zug) => self.spiel.ausfuehren(zug),
Die drei Knöpfe schicken jetzt ihren Zug verpackt in Message::Spielzug:
.on_press(Message::ZugBeenden).on_press(Message::Spielzug(Zug::ZugBeenden))⋮.then_some(Message::Kaufen(kette)).then_some(Message::Spielzug(Zug::Kaufen(kette)))⋮.on_press(Message::Gruenden(kette)).on_press(Message::Spielzug(Zug::Gruenden(kette)))
Und jetzt zahlt sich darf_legen sofort aus: Plättchen-Knöpfe, Fähnchen und Mauszeiger
fragen nicht mehr nur „liegt es auf der Hand?“, sondern „darf es jetzt gelegt werden?“. Ein
Plättchen, das zwei Ketten verbinden würde, bekommt so gar kein Fähnchen mehr:
let darf_legen = self.spiel.phase == Phase::Legen;⋮.on_press_maybe(darf_legen.then_some(Message::Geklickt(feld))).on_press_maybe( self.spiel .darf_legen(feld) .then_some(Message::Geklickt(feld)), )
if let Phase::Kaufen { gekauft } = self.spiel.phase { let rest = text(format!( "Du darfst noch {} Aktien kaufen", KAEUFE_JE_ZUG - gekauft )) .size(14) .color(farben.schrift);let ansage = match self.spiel.phase { Phase::Kaufen { gekauft } => Some(format!( "Du darfst noch {} Aktien kaufen", KAEUFE_JE_ZUG - gekauft )), Phase::Legen if !self.spiel.kann_legen() => { Some(String::from("Keins deiner Plättchen passt gerade")) } _ => None, }; if let Some(ansage) = ansage { let rest = text(ansage).size(14).color(farben.schrift);let fertig = button(text("Zug beenden").size(14))
Some(feld) if self.spiel.aktueller().hand.contains(&feld) => { mouse::Interaction::Pointer }Some(feld) if self.spiel.darf_legen(feld) => mouse::Interaction::Pointer,⋮let im_plan = self.spiel.aktueller().hand.contains(&feld);let im_plan = self.spiel.darf_legen(feld);
cargo run
Es spielt sich wie vorher. Achte auf die Fähnchen: In der Kaufphase verschwinden sie, weil dann gerade nicht gelegt werden darf. Und würde ein Plättchen zwei Ketten berühren, bleibt es grau. Passt gar keins, steht unten „Keins deiner Plättchen passt gerade“ mit dem Knopf „Zug beenden“.Der Tourist
Zeit für den ersten Computergegner. Er heißt Tourist, weil er nicht viel nachdenkt: Er schaut sich an, welche Züge erlaubt sind, und nimmt irgendeinen. Das klingt dumm, ist aber ein wichtiger erster Schritt, denn er prüft die ganze Maschinerie, und später messen sich die klügeren Gegner an ihm. Zuerst muss das Spiel wissen, wer Mensch ist und wer nicht:
// Wer da spielt: ein Mensch oder der Computer #[derive(Debug, Clone, Copy, PartialEq)] pub enum Art { Mensch, Tourist, }// Was einem Spieler gehört pub struct Spieler { pub name: String,pub art: Art,
pub fn neu(namen: &[&str]) -> Spiel {pub fn neu(teilnehmer: &[(&str, Art)]) -> Spiel {⋮let spieler = namen .iter() .map(|name| Spieler {let spieler = teilnehmer .iter() .map(|&(name, art)| Spieler {name: name.to_string(),art,
|&(name, art)| packt jedes Paar der Liste gleich in zwei Namen aus. Jetzt die
wichtigste Frage für jeden Computergegner: Was darf ich gerade tun? Die Antwort liefert eine
Methode, die alle erlaubten Züge sammelt. Sie kommt direkt vor ausfuehren:
// Alle Züge, die der Spieler am Zug gerade machen darf pub fn moegliche_zuege(&self) -> Vec<Zug> { let mut zuege = Vec::new(); match self.phase { Phase::Legen => { for &feld in &self.aktueller().hand { if self.darf_legen(feld) { zuege.push(Zug::Legen(feld)); } } if zuege.is_empty() { zuege.push(Zug::ZugBeenden); } } Phase::Gruenden(_) => { for kette in self.freie_ketten() { zuege.push(Zug::Gruenden(kette)); } } Phase::Kaufen { .. } => { for kette in self.aktive_ketten() { if self.darf_kaufen(kette) { zuege.push(Zug::Kaufen(kette)); } } zuege.push(Zug::ZugBeenden); } } zuege }// Führt einen Zug aus, egal ob er vom Menschen oder vom Computer kommt
Je nach Phase sammelt sie Plättchen, die passen, freie Ketten zum Gründen oder Aktien, die man kaufen kann. Beim Kaufen ist „aufhören“ immer auch eine Möglichkeit.
Die Computergegner bekommen ein eigenes Modul src/ki.rs. Das Kürzel steht für
künstliche Intelligenz, auch wenn der Tourist davon nicht viel hat:
use rand::seq::IndexedRandom;
use crate::spiel::{Art, Spiel, Zug};
// Welchen Zug macht der Computer, der gerade dran ist?
pub fn waehle_zug(spiel: &Spiel) -> Zug {
match spiel.aktueller().art {
Art::Mensch | Art::Tourist => tourist(spiel),
}
}
// Der Tourist denkt nicht nach: Er nimmt irgendeinen erlaubten Zug
fn tourist(spiel: &Spiel) -> Zug {
let zuege = spiel.moegliche_zuege();
zuege
.choose(&mut rand::rng())
.copied()
.unwrap_or(Zug::ZugBeenden)
}
use crate::spiel::…:crateist die Wurzel unseres Programms. Vonki.rsaus erreichen wir so das Modulspiel.waehle_zugist der einzige Eingang. Es schaut, welche Art von Spieler dran ist, und fragt den passenden Denker. Noch gibt es nur den Touristen. In Etappe 8 und 9 kommen hier zwei weitere Zweige dazu.choosekommt vonIndexedRandomund zieht ein zufälliges Element aus einer Liste. Weil die Liste leer sein könnte, liefert es einOption.copiedmacht aus dem Verweis eine Kopie, undunwrap_ornimmt zur Not „Zug beenden“.
Damit der Computer nicht in einem Wimpernschlag durchspielt, soll er im Takt ziehen: alle
0,6 Sekunden eine Handlung. Einen Takt liefert iced über eine Subscription, ein
Abonnement auf Ereignisse von außen. Die Uhr dafür braucht einen Unterbau für nebenläufige
Aufgaben, und iced kann dafür tokio nutzen. Dieses Feature schalten wir zu:
cargo add iced@0.14 --features canvas,tokio
In main.rs melden wir das neue Modul an und holen uns Uhr und Abo:
mod ki;mod spiel;use std::time::Duration;use iced::mouse;⋮Theme, Vector, alignment, font,Subscription, Theme, Vector, alignment, font, time,};use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Zug};use spiel::{ Art, Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Spieler, Zug, };
.antialiasing(true).subscription(Bauloewe::abo).run()⋮spiel: Spiel::neu(&["Anna", "Ben", "Carla", "Dario"]),spiel: Spiel::neu(&[ ("Leo", Art::Mensch), ("Toni", Art::Tourist), ("Tine", Art::Tourist), ("Theo", Art::Tourist), ]),
Du spielst als Leo, der Baulöwe, gegen Toni, Tine und Theo. Jetzt die drei Helfer, die
main.rs ab sofort ständig braucht. Sie kommen in impl Bauloewe vor
update:
// Du bist immer Spieler Nummer 0 fn ich(&self) -> &Spieler { &self.spiel.spieler[0] } fn mensch_am_zug(&self) -> bool { self.spiel.aktueller().art == Art::Mensch } // Ist der Computer dran, tickt alle 0,6 Sekunden ein Takt fn abo(&self) -> Subscription<Message> { if self.mensch_am_zug() { Subscription::none() } else { time::every(Duration::from_millis(600)).map(|_| Message::KiTakt) } }fn update(&mut self, message: Message) {
abo gibt iced bei jeder Änderung Bescheid, welche Abos gerade gelten sollen. Ist
der Mensch dran, gibt es keins. Ist der Computer dran, schickt
time::every alle 600 Millisekunden ein Signal, das map in einen Brief
KiTakt verwandelt. Der Brief kommt ins enum, und update
bekommt zwei Wächter:
Spielzug(Zug),KiTakt,}⋮Message::MausUeber(feld) => self.maus_feld = feld,Message::Geklickt(feld) => self.spiel.ausfuehren(Zug::Legen(feld)),Message::TagNacht => self.nacht = !self.nacht,Message::Spielzug(zug) => self.spiel.ausfuehren(zug),// Züge vom Menschen zählen nur, wenn er dran ist Message::Geklickt(feld) if self.mensch_am_zug() => { self.spiel.ausfuehren(Zug::Legen(feld)) } Message::Spielzug(zug) if self.mensch_am_zug() => self.spiel.ausfuehren(zug), Message::Geklickt(_) | Message::Spielzug(_) => {} // Der Takt für den Computer Message::KiTakt => { if !self.mensch_am_zug() { let zug = ki::waehle_zug(&self.spiel); self.spiel.ausfuehren(zug); } }}
Die Zweige mit if self.mensch_am_zug() lassen Klicks nur durch, wenn du dran bist.
Die Zeile darunter fängt dieselben Briefe in allen anderen Fällen ab und tut nichts, denn ein
match muss jeden Fall behandeln. Bei KiTakt fragt das Programm den
Computer nach seinem Zug und führt ihn aus, über genau denselben Eingang wie deine Klicks.
Bleibt die Oberfläche. Sie zeigt ab jetzt immer deine Sicht: deine Hand, deine Kasse, deine Aktien. Und alles Klickbare ist nur aktiv, wenn du dran bist:
let hinweis = match self.maus_feld {_ if !self.mensch_am_zug() => format!("{} ist am Zug …", self.spiel.aktueller().name),Some(feld) => format!("Feld {}", feld.name()),⋮let spieler = self.spiel.aktueller(); let kasse = text(format!("{} · Kasse: {}", spieler.name, geld(spieler.geld)))let kasse = text(format!("Kasse: {}", geld(self.ich().geld)))
Der erste Zweig im match ist ein Trick: _ passt auf alles, und die
Bedingung dahinter entscheidet. Ist der Computer dran, gewinnt dieser Zweig, egal wo die Maus
ist.
for &feld in &self.spiel.aktueller().hand {for &feld in &self.ich().hand { let darf = self.mensch_am_zug() && self.spiel.darf_legen(feld);let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE)) .padding([8, 14]).on_press_maybe( self.spiel .darf_legen(feld) .then_some(Message::Geklickt(feld)), ).on_press_maybe(darf.then_some(Message::Geklickt(feld)))⋮let ansage = match self.spiel.phase {_ if !self.mensch_am_zug() => None,Phase::Kaufen { gekauft } => Some(format!(⋮text(self.spiel.aktueller().aktien[kette.nr()].to_string())text(self.ich().aktien[kette.nr()].to_string())⋮self.spiel .darf_kaufen(kette)(self.mensch_am_zug() && self.spiel.darf_kaufen(kette)).then_some(Message::Spielzug(Zug::Kaufen(kette)))
if let Phase::Gruenden(_) = self.spiel.phase {if self.mensch_am_zug() && matches!(self.spiel.phase, Phase::Gruenden(_)) {⋮Some(feld) if self.spiel.darf_legen(feld) => mouse::Interaction::Pointer,Some(feld) if self.mensch_am_zug() && self.spiel.darf_legen(feld) => { mouse::Interaction::Pointer }⋮let im_plan = self.spiel.darf_legen(feld);let im_plan = self.mensch_am_zug() && self.spiel.darf_legen(feld);
matches! prüft, ob ein Wert auf ein Muster passt, und liefert einfach wahr oder
falsch. Damit lässt sich die Musterprüfung mit && verknüpfen.
cargo run
Du bist Leo und fängst an. Leg dein Plättchen, kauf vielleicht etwas, beende den Zug, und dann lehn dich zurück: Toni, Tine und Theo legen, gründen und kaufen im Takt, und das Protokoll links schreibt mit. Danach bist du wieder dran. Die Touristen kaufen wild durcheinander. Mal sehen, wie lange sie mit ihrem Geld auskommen.
Der komplette Code nach Etappe 5
Hier ist der vollständige Stand nach Etappe 5. Nimm ihn zum Vergleichen, wenn irgendwo ein Fehler steckt, den du nicht findest, oder als Einstiegspunkt, wenn du erst bei der nächsten Etappe dazustößt.
Cargo.toml, src/main.rs, src/spiel.rs, src/ki.rs anzeigen
Cargo.toml
[package]
name = "bauloewe"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = { version = "0.14", features = ["canvas", "tokio"] }
rand = "0.9"
src/main.rs
mod ki;
mod spiel;
use std::time::Duration;
use iced::mouse;
use iced::widget::canvas::{Frame, Path, Stroke, Text, gradient};
use iced::widget::{button, canvas, column, container, row, space, stack, text};
use iced::{
Background, Border, Color, Element, Fill, Font, Point, Rectangle, Renderer, Shadow, Size,
Subscription, Theme, Vector, alignment, font, time,
};
use spiel::{
Art, Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Spieler, Zug,
};
// So groß erscheint ein Feld auf dem Bildschirm
const HALBE_BREITE: f32 = 40.0;
const HALBE_HOEHE: f32 = 20.0;
// Höhen in Pixeln
const PLATTE: f32 = 4.0;
const ANGEHOBEN: f32 = 9.0;
const STOCKWERK: f32 = 13.0;
fn main() -> iced::Result {
iced::application(Bauloewe::neu, Bauloewe::update, Bauloewe::view)
.title("Baulöwe")
.window_size((1280.0, 760.0))
.antialiasing(true)
.subscription(Bauloewe::abo)
.run()
}
struct Bauloewe {
spiel: Spiel,
maus_feld: Option<Feld>,
nacht: bool,
}
#[derive(Debug, Clone)]
enum Message {
MausUeber(Option<Feld>),
Geklickt(Feld),
TagNacht,
Spielzug(Zug),
KiTakt,
}
impl Bauloewe {
fn neu() -> Bauloewe {
Bauloewe {
spiel: Spiel::neu(&[
("Leo", Art::Mensch),
("Toni", Art::Tourist),
("Tine", Art::Tourist),
("Theo", Art::Tourist),
]),
maus_feld: None,
nacht: false,
}
}
// Du bist immer Spieler Nummer 0
fn ich(&self) -> &Spieler {
&self.spiel.spieler[0]
}
fn mensch_am_zug(&self) -> bool {
self.spiel.aktueller().art == Art::Mensch
}
// Ist der Computer dran, tickt alle 0,6 Sekunden ein Takt
fn abo(&self) -> Subscription<Message> {
if self.mensch_am_zug() {
Subscription::none()
} else {
time::every(Duration::from_millis(600)).map(|_| Message::KiTakt)
}
}
fn update(&mut self, message: Message) {
match message {
Message::MausUeber(feld) => self.maus_feld = feld,
Message::TagNacht => self.nacht = !self.nacht,
// Züge vom Menschen zählen nur, wenn er dran ist
Message::Geklickt(feld) if self.mensch_am_zug() => {
self.spiel.ausfuehren(Zug::Legen(feld))
}
Message::Spielzug(zug) if self.mensch_am_zug() => self.spiel.ausfuehren(zug),
Message::Geklickt(_) | Message::Spielzug(_) => {}
// Der Takt für den Computer
Message::KiTakt => {
if !self.mensch_am_zug() {
let zug = ki::waehle_zug(&self.spiel);
self.spiel.ausfuehren(zug);
}
}
}
}
fn view(&self) -> Element<'_, Message> {
let farben = self.farben();
let titel = text("BAULÖWE")
.size(26)
.font(Font {
weight: font::Weight::Bold,
..Font::DEFAULT
})
.color(farben.schrift);
let hinweis = match self.maus_feld {
_ if !self.mensch_am_zug() => format!("{} ist am Zug …", self.spiel.aktueller().name),
Some(feld) => format!("Feld {}", feld.name()),
None => String::from("Fahr mit der Maus über das Baugrundstück"),
};
let status = text(hinweis).size(15).color(farben.schrift);
let kasse = text(format!("Kasse: {}", geld(self.ich().geld)))
.size(17)
.font(Font {
weight: font::Weight::Bold,
..Font::DEFAULT
})
.color(farben.schrift);
let beschriftung = if self.nacht { "Tagmodus" } else { "Nachtmodus" };
let schalter = button(text(beschriftung).size(14))
.padding([6, 16])
.on_press(Message::TagNacht)
.style(move |_theme: &Theme, _status| button::Style {
background: Some(Background::Color(farben.akzent)),
text_color: Color::WHITE,
border: Border {
radius: 16.0.into(),
..Border::default()
},
..button::Style::default()
});
let kopfzeile = row![titel, space::horizontal(), status, kasse, schalter]
.spacing(20)
.padding([14, 24])
.align_y(alignment::Vertical::Center);
let brett = canvas(self).width(Fill).height(Fill);
// Unten: die Plättchen auf deiner Hand
let mut hand = row![text("Deine Plättchen:").size(15).color(farben.schrift)]
.spacing(10)
.align_y(alignment::Vertical::Center);
for &feld in &self.ich().hand {
let darf = self.mensch_am_zug() && self.spiel.darf_legen(feld);
let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE))
.padding([8, 14])
.on_press_maybe(darf.then_some(Message::Geklickt(feld)))
.style(move |_theme: &Theme, status| {
let grund = match status {
button::Status::Hovered => farben.rasen_hell,
button::Status::Disabled => Color {
a: 0.5,
..farben.stein
},
_ => farben.stein,
};
button::Style {
background: Some(Background::Color(grund)),
text_color: farben.schrift,
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
});
hand = hand.push(plaettchen);
}
let stapel = text(format!("Im Stapel: {}", self.spiel.stapel.len()))
.size(14)
.color(farben.schrift);
// In der Kaufphase gibt es einen Knopf, der den Zug beendet
let mut rechts = row![stapel]
.spacing(16)
.align_y(alignment::Vertical::Center);
let ansage = match self.spiel.phase {
_ if !self.mensch_am_zug() => None,
Phase::Kaufen { gekauft } => Some(format!(
"Du darfst noch {} Aktien kaufen",
KAEUFE_JE_ZUG - gekauft
)),
Phase::Legen if !self.spiel.kann_legen() => {
Some(String::from("Keins deiner Plättchen passt gerade"))
}
_ => None,
};
if let Some(ansage) = ansage {
let rest = text(ansage).size(14).color(farben.schrift);
let fertig = button(text("Zug beenden").size(14))
.padding([8, 16])
.on_press(Message::Spielzug(Zug::ZugBeenden))
.style(move |_theme: &Theme, _status| button::Style {
background: Some(Background::Color(farben.akzent)),
text_color: Color::WHITE,
border: Border {
radius: 16.0.into(),
..Border::default()
},
..button::Style::default()
});
rechts = rechts.push(rest).push(fertig);
}
let fussleiste = row![hand, space::horizontal(), rechts]
.padding([16, 24])
.align_y(alignment::Vertical::Center);
// Kopf- und Fußleiste liegen wie eine Folie über dem Brett
// Rechts oben: die Tafel mit allen Ketten
let mut ketten = column![
text("Hotelketten")
.size(15)
.font(Font {
weight: font::Weight::Bold,
..Font::DEFAULT
})
.color(farben.schrift)
]
.spacing(5);
let klein = |inhalt: &str, breite: f32| {
text(inhalt.to_string())
.size(11)
.color(farben.beschriftung)
.width(breite)
};
ketten = ketten.push(
row![
klein("", 12.0),
klein("Kette", 90.0),
klein("Größe", 70.0),
klein("Kurs", 70.0),
klein("Du", 30.0),
klein("Bank", 30.0),
]
.spacing(8),
);
for kette in Kette::ALLE {
let groesse = self.spiel.groesse(kette);
let farbe = kettenfarbe(kette);
let punkt = container(space())
.width(12)
.height(12)
.style(move |_theme: &Theme| container::Style {
background: Some(Background::Color(farbe)),
border: Border {
radius: 6.0.into(),
..Border::default()
},
..container::Style::default()
});
let (angabe, kurs) = if groesse == 0 {
(String::from("frei"), String::new())
} else {
(format!("{groesse} Felder"), geld(self.spiel.preis(kette)))
};
let zeile = row![
punkt,
text(kette.name()).size(13).color(farben.schrift).width(90),
text(angabe).size(13).color(farben.schrift).width(70),
text(kurs).size(13).color(farben.schrift).width(70),
text(self.ich().aktien[kette.nr()].to_string())
.size(13)
.color(farben.schrift)
.width(30),
text(self.spiel.bank[kette.nr()].to_string())
.size(13)
.color(farben.schrift)
.width(30),
button(text("+").size(14))
.padding([1, 9])
.on_press_maybe(
(self.mensch_am_zug() && self.spiel.darf_kaufen(kette))
.then_some(Message::Spielzug(Zug::Kaufen(kette)))
)
.style(move |_theme: &Theme, status| {
let grund = match status {
button::Status::Disabled => Color {
a: 0.25,
..farben.akzent
},
button::Status::Hovered => aufhellen(farben.akzent, 0.2),
_ => farben.akzent,
};
button::Style {
background: Some(Background::Color(grund)),
text_color: Color::WHITE,
border: Border {
radius: 12.0.into(),
..Border::default()
},
..button::Style::default()
}
}),
]
.spacing(8)
.align_y(alignment::Vertical::Center);
ketten = ketten.push(zeile);
}
let tafel_stil = move |_theme: &Theme| container::Style {
background: Some(Background::Color(Color {
a: 0.85,
..farben.tafel
})),
border: Border {
radius: 10.0.into(),
..Border::default()
},
..container::Style::default()
};
let tafel = container(ketten).padding(14).style(tafel_stil);
// Links oben: wer mitspielt und was zuletzt passiert ist
let mut tisch = column![
text("Am Tisch")
.size(15)
.font(Font {
weight: font::Weight::Bold,
..Font::DEFAULT
})
.color(farben.schrift)
]
.spacing(4);
for (nr, spieler) in self.spiel.spieler.iter().enumerate() {
let aktien: u32 = spieler.aktien.iter().sum();
let zeile = row![
text(spieler.name.as_str())
.size(13)
.color(farben.schrift)
.width(60),
text(geld(spieler.geld))
.size(13)
.color(farben.schrift)
.width(80),
text(format!("Aktien: {aktien}"))
.size(13)
.color(farben.schrift),
]
.spacing(8);
// Wer am Zug ist, bekommt einen farbigen Hintergrund
let am_zug = nr == self.spiel.am_zug;
let zeile = container(zeile)
.padding([3, 8])
.style(move |_theme: &Theme| container::Style {
background: am_zug.then_some(Background::Color(Color {
a: 0.35,
..farben.akzent
})),
border: Border {
radius: 6.0.into(),
..Border::default()
},
..container::Style::default()
});
tisch = tisch.push(zeile);
}
tisch = tisch.push(text("Zuletzt").size(11).color(farben.beschriftung));
for eintrag in self.spiel.protokoll.iter().rev().take(5) {
tisch = tisch.push(text(eintrag.as_str()).size(12).color(farben.schrift));
}
let tisch = container(tisch).padding(14).width(270).style(tafel_stil);
let tafelzeile = row![tisch, space::horizontal(), tafel].padding([0, 24]);
let folie = column![kopfzeile, tafelzeile, space::vertical(), fussleiste];
// Wartet eine Gründung, kommt das Auswahlfenster obendrauf
let mut ebenen = stack![brett, folie];
if self.mensch_am_zug() && matches!(self.spiel.phase, Phase::Gruenden(_)) {
ebenen = ebenen.push(self.gruendungsdialog(farben));
}
ebenen.into()
}
// Das Fenster zum Gründen einer neuen Kette
fn gruendungsdialog(&self, farben: Farben) -> Element<'_, Message> {
let mut knoepfe = row![].spacing(8);
for kette in self.spiel.freie_ketten() {
let farbe = kettenfarbe(kette);
let knopf = button(text(kette.name()).size(14))
.padding([8, 12])
.on_press(Message::Spielzug(Zug::Gruenden(kette)))
.style(move |_theme: &Theme, status| {
let grund = match status {
button::Status::Hovered => aufhellen(farbe, 0.25),
_ => farbe,
};
button::Style {
background: Some(Background::Color(grund)),
text_color: Color::WHITE,
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
});
knoepfe = knoepfe.push(knopf);
}
let titel = text("Neue Hotelkette!")
.size(24)
.font(Font {
weight: font::Weight::Bold,
..Font::DEFAULT
})
.color(farben.schrift);
let frage = text("Deine Häuser stehen jetzt nebeneinander. Welche Kette gründest du?")
.size(15)
.color(farben.schrift);
let karte = container(
column![titel, frage, knoepfe]
.spacing(14)
.align_x(alignment::Horizontal::Center),
)
.padding(24)
.style(move |_theme: &Theme| container::Style {
background: Some(Background::Color(farben.tafel)),
border: Border {
radius: 12.0.into(),
..Border::default()
},
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
offset: Vector::new(0.0, 8.0),
blur_radius: 24.0,
},
..container::Style::default()
});
// Ein dunkler Schleier über dem Brett, die Karte in der Mitte
container(karte)
.center(Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.3))),
..container::Style::default()
})
.into()
}
fn farben(&self) -> Farben {
if self.nacht {
Farben::nacht()
} else {
Farben::tag()
}
}
}
impl canvas::Program<Message> for Bauloewe {
type State = ();
fn update(
&self,
_state: &mut Self::State,
event: &canvas::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message>> {
let iso = Iso::neu(bounds.size());
let feld = cursor
.position_in(bounds)
.and_then(|punkt| iso.feld_unter(punkt));
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
feld.map(|feld| canvas::Action::publish(Message::Geklickt(feld)))
}
canvas::Event::Mouse(_) if feld != self.maus_feld => {
Some(canvas::Action::publish(Message::MausUeber(feld)))
}
_ => None,
}
}
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let farben = self.farben();
let iso = Iso::neu(bounds.size());
self.zeichne_himmel(&mut frame, &farben, bounds.size());
zeichne_sockel(&mut frame, &iso, &farben);
// Von hinten nach vorne zeichnen, damit Vorderes Hinteres verdeckt
let mut felder = Vec::new();
for reihe in 0..REIHEN {
for spalte in 0..SPALTEN {
felder.push(Feld { spalte, reihe });
}
}
felder.sort_by_key(|feld| feld.spalte + feld.reihe);
for feld in felder {
self.zeichne_parzelle(&mut frame, &iso, &farben, feld);
}
vec![frame.into_geometry()]
}
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> mouse::Interaction {
match self.maus_feld {
Some(feld) if self.mensch_am_zug() && self.spiel.darf_legen(feld) => {
mouse::Interaction::Pointer
}
_ => mouse::Interaction::default(),
}
}
}
// Alles, was auf die Leinwand gezeichnet wird
impl Bauloewe {
fn zeichne_himmel(&self, frame: &mut Frame, farben: &Farben, groesse: Size) {
let verlauf = gradient::Linear::new(Point::ORIGIN, Point::new(0.0, groesse.height))
.add_stop(0.0, farben.himmel_oben)
.add_stop(1.0, farben.himmel_unten);
frame.fill_rectangle(Point::ORIGIN, groesse, verlauf);
if self.nacht {
// 90 Sterne, jede Nacht an denselben Stellen
for nr in 0..90 {
let x = (zufall(nr, 1) % 1000) as f32 / 1000.0;
let y = (zufall(nr, 2) % 1000) as f32 / 1000.0;
let punkt = Point::new(x * groesse.width, y * groesse.height * 0.55);
let helligkeit = 0.3 + (zufall(nr, 3) % 70) as f32 / 100.0;
frame.fill(
&Path::circle(punkt, 1.2),
Color {
a: helligkeit,
..Color::WHITE
},
);
}
} else {
// Die Sonne: ein weicher Schein aus vielen blassen Kreisen
let sonne = Point::new(groesse.width * 0.84, groesse.height * 0.2);
for ring in 0..12 {
let radius = 30.0 + ring as f32 * 14.0;
frame.fill(
&Path::circle(sonne, radius),
Color::from_rgba(1.0, 0.97, 0.85, 0.05),
);
}
frame.fill(
&Path::circle(sonne, 26.0),
Color::from_rgb8(0xff, 0xf6, 0xd8),
);
}
}
fn zeichne_parzelle(&self, frame: &mut Frame, iso: &Iso, farben: &Farben, feld: Feld) {
let s = feld.spalte as f32;
let r = feld.reihe as f32;
let im_plan = self.mensch_am_zug() && self.spiel.darf_legen(feld);
let unter_maus = im_plan && self.maus_feld == Some(feld);
let hoehe = if unter_maus {
PLATTE + ANGEHOBEN
} else {
PLATTE
};
// Der Bordstein ist ein flacher Quader
let rand = 0.1;
let bordstein = Quader {
s0: s + rand,
r0: r + rand,
s1: s + 1.0 - rand,
r1: r + 1.0 - rand,
z0: 0.0,
z1: hoehe,
farbe: farben.stein,
};
zeichne_quader(frame, iso, &bordstein);
// Der Rasen liegt als Fläche oben auf dem Bordstein
let innen = rand + 0.04;
let rasen = [
iso.punkt(s + innen, r + innen, hoehe),
iso.punkt(s + 1.0 - innen, r + innen, hoehe),
iso.punkt(s + 1.0 - innen, r + 1.0 - innen, hoehe),
iso.punkt(s + innen, r + 1.0 - innen, hoehe),
];
let gruen = if unter_maus {
farben.rasen_hell
} else {
farben.rasen
};
frame.fill(&vieleck(&rasen), gruen);
if unter_maus {
let umriss = vieleck(&bordstein.oberseite(iso));
frame.stroke(
&umriss,
Stroke::default().with_color(farben.akzent).with_width(2.0),
);
}
if im_plan {
zeichne_faehnchen(frame, iso, farben, feld, hoehe);
}
match self.spiel.belegung(feld) {
Belegung::Rohbau => {
let plan = Bauplan {
stockwerke: 3,
fassade: farben.beton,
band: None,
dach: Dach::Flach,
};
self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
}
Belegung::Hotel(kette) => {
let groesse = self.spiel.groesse(kette);
let plan = hotel_bauplan(kette, groesse, farben, feld);
self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
}
Belegung::Frei => {
let schild = Text {
content: feld.name(),
position: iso.punkt(s + 0.5, r + 0.5, hoehe),
color: farben.beschriftung,
size: 11.0.into(),
font: Font::MONOSPACE,
align_x: text::Alignment::Center,
align_y: alignment::Vertical::Center,
..Text::default()
};
// Als Formen zeichnen, damit Häuser davor die Schrift verdecken
schild.draw_with(|form, farbe| frame.fill(&form, farbe));
}
}
}
fn zeichne_haus(
&self,
frame: &mut Frame,
iso: &Iso,
farben: &Farben,
feld: Feld,
boden: f32,
plan: &Bauplan,
) {
let s = feld.spalte as f32;
let r = feld.reihe as f32;
let stockwerke = plan.stockwerke;
let haus = Quader {
s0: s + 0.2,
r0: r + 0.2,
s1: s + 0.8,
r1: r + 0.8,
z0: boden,
z1: boden + stockwerke as f32 * STOCKWERK,
farbe: plan.fassade,
};
// Nachts fällt warmes Licht aus den Fenstern auf den Rasen
if self.nacht {
let schein = [
iso.punkt(s + 0.14, r + 0.14, boden),
iso.punkt(s + 0.86, r + 0.14, boden),
iso.punkt(s + 0.86, r + 0.86, boden),
iso.punkt(s + 0.14, r + 0.86, boden),
];
frame.fill(
&vieleck(&schein),
Color {
a: 0.18,
..farben.licht
},
);
}
// Der Schatten fällt nach rechts auf den Rasen
let schatten = [
iso.punkt(haus.s1, haus.r0, boden),
iso.punkt(haus.s1 + 0.12, haus.r0 + 0.08, boden),
iso.punkt(haus.s1 + 0.12, haus.r1, boden),
iso.punkt(haus.s1, haus.r1, boden),
];
frame.fill(&vieleck(&schatten), Color::from_rgba(0.0, 0.0, 0.0, 0.25));
zeichne_quader(frame, iso, &haus);
// Fenster: pro Stockwerk zwei auf jeder sichtbaren Seite
for stock in 0..stockwerke {
let unten = boden + stock as f32 * STOCKWERK + 4.0;
let oben = unten + 6.0;
for nr in 0..2 {
let a = 0.08 + nr as f32 * 0.27;
let b = a + 0.17;
let links = [
iso.punkt(haus.s0 + a, haus.r1, oben),
iso.punkt(haus.s0 + b, haus.r1, oben),
iso.punkt(haus.s0 + b, haus.r1, unten),
iso.punkt(haus.s0 + a, haus.r1, unten),
];
let rechts = [
iso.punkt(haus.s1, haus.r0 + a, oben),
iso.punkt(haus.s1, haus.r0 + b, oben),
iso.punkt(haus.s1, haus.r0 + b, unten),
iso.punkt(haus.s1, haus.r0 + a, unten),
];
for (seite, ecken) in [(0, links), (1, rechts)] {
let fenster_nr = stock * 4 + nr * 2 + seite;
let brennt =
self.nacht && zufall(feld.spalte * 16 + feld.reihe, fenster_nr) % 3 != 0;
let farbe = if brennt {
farben.licht
} else if seite == 0 {
farben.fenster
} else {
abdunkeln(farben.fenster, 0.8)
};
frame.fill(&vieleck(&ecken), farbe);
}
}
}
// Ein Band in der Farbe der Kette ganz oben um das Haus
if let Some(band) = plan.band {
let unten = haus.z1 - 3.5;
let links = [
iso.punkt(haus.s0, haus.r1, haus.z1),
iso.punkt(haus.s1, haus.r1, haus.z1),
iso.punkt(haus.s1, haus.r1, unten),
iso.punkt(haus.s0, haus.r1, unten),
];
let rechts = [
iso.punkt(haus.s1, haus.r0, haus.z1),
iso.punkt(haus.s1, haus.r1, haus.z1),
iso.punkt(haus.s1, haus.r1, unten),
iso.punkt(haus.s1, haus.r0, unten),
];
frame.fill(&vieleck(&links), band);
frame.fill(&vieleck(&rechts), abdunkeln(band, 0.75));
}
self.zeichne_dach(frame, iso, &haus, plan);
}
// Jede Dachform wird anders gezeichnet
fn zeichne_dach(&self, frame: &mut Frame, iso: &Iso, haus: &Quader, plan: &Bauplan) {
let farbe = plan.band.unwrap_or(plan.fassade);
let mitte_s = (haus.s0 + haus.s1) / 2.0;
let mitte_r = (haus.r0 + haus.r1) / 2.0;
match plan.dach {
Dach::Flach => flachdach(frame, iso, haus),
Dach::Garten => {
flachdach(frame, iso, haus);
let mitte = iso.punkt(mitte_s, mitte_r, haus.z1);
let busch = Color::from_rgb8(0x5f, 0x9a, 0x44);
frame.fill(
&Path::circle(Point::new(mitte.x - 5.0, mitte.y - 3.0), 4.5),
busch,
);
frame.fill(
&Path::circle(Point::new(mitte.x + 4.0, mitte.y - 1.0), 3.5),
aufhellen(busch, 0.2),
);
}
Dach::Zinnen => {
flachdach(frame, iso, haus);
let d = 0.12;
for (s, r) in [
(haus.s0, haus.r0),
(haus.s1 - d, haus.r0),
(haus.s0, haus.r1 - d),
(haus.s1 - d, haus.r1 - d),
] {
let zinne = Quader {
s0: s,
r0: r,
s1: s + d,
r1: r + d,
z0: haus.z1,
z1: haus.z1 + 5.0,
farbe: haus.farbe,
};
zeichne_quader(frame, iso, &zinne);
}
}
Dach::Antenne => {
flachdach(frame, iso, haus);
let fuss = iso.punkt(mitte_s, mitte_r, haus.z1);
let spitze = Point::new(fuss.x, fuss.y - 20.0);
let mast = Stroke::default()
.with_color(abdunkeln(farbe, 0.5))
.with_width(1.5);
frame.stroke(&Path::line(fuss, spitze), mast);
let lampe = if self.nacht {
Color::from_rgb8(0xff, 0x4a, 0x3d)
} else {
abdunkeln(farbe, 0.6)
};
frame.fill(&Path::circle(spitze, 2.2), lampe);
}
Dach::Pyramide => {
let spitze = iso.punkt(mitte_s, mitte_r, haus.z1 + 16.0);
pyramide(frame, iso, haus, spitze, farbe);
}
Dach::Stufen => {
for (stufe, hoehe) in [(0.1, 7.0), (0.2, 13.0)] {
let aufsatz = Quader {
s0: haus.s0 + stufe,
r0: haus.r0 + stufe,
s1: haus.s1 - stufe,
r1: haus.r1 - stufe,
z0: haus.z1,
z1: haus.z1 + hoehe,
farbe: haus.farbe,
};
zeichne_quader(frame, iso, &aufsatz);
}
}
Dach::Turmspitze => {
let sockel = Quader {
s0: haus.s0 + 0.15,
r0: haus.r0 + 0.15,
s1: haus.s1 - 0.15,
r1: haus.r1 - 0.15,
z0: haus.z1,
z1: haus.z1 + 5.0,
farbe: haus.farbe,
};
zeichne_quader(frame, iso, &sockel);
let spitze = iso.punkt(mitte_s, mitte_r, sockel.z1 + 26.0);
pyramide(frame, iso, &sockel, spitze, farbe);
}
}
}
}
// Ein Flachdach mit Rand
fn flachdach(frame: &mut Frame, iso: &Iso, haus: &Quader) {
let innen = 0.05;
let dach = [
iso.punkt(haus.s0 + innen, haus.r0 + innen, haus.z1),
iso.punkt(haus.s1 - innen, haus.r0 + innen, haus.z1),
iso.punkt(haus.s1 - innen, haus.r1 - innen, haus.z1),
iso.punkt(haus.s0 + innen, haus.r1 - innen, haus.z1),
];
frame.fill(&vieleck(&dach), abdunkeln(haus.farbe, 0.85));
}
// Ein spitzes Dach: zwei sichtbare Dreiecke von der Oberkante zur Spitze
fn pyramide(frame: &mut Frame, iso: &Iso, unten: &Quader, spitze: Point, farbe: Color) {
let links = [
iso.punkt(unten.s0, unten.r1, unten.z1),
iso.punkt(unten.s1, unten.r1, unten.z1),
spitze,
];
let rechts = [
iso.punkt(unten.s1, unten.r0, unten.z1),
iso.punkt(unten.s1, unten.r1, unten.z1),
spitze,
];
frame.fill(&vieleck(&links), abdunkeln(farbe, 0.9));
frame.fill(&vieleck(&rechts), abdunkeln(farbe, 0.65));
}
fn zeichne_sockel(frame: &mut Frame, iso: &Iso, farben: &Farben) {
let rand = 0.35;
let (s0, r0) = (-rand, -rand);
let (s1, r1) = (SPALTEN as f32 + rand, REIHEN as f32 + rand);
// Ein weicher Schatten unter dem ganzen Brett
let schatten = [
iso.punkt(s0, r0, -34.0),
iso.punkt(s1, r0, -34.0),
iso.punkt(s1, r1, -34.0),
iso.punkt(s0, r1, -34.0),
];
frame.fill(&vieleck(&schatten), Color::from_rgba(0.0, 0.0, 0.0, 0.18));
let sockel = Quader {
s0,
r0,
s1,
r1,
z0: -22.0,
z1: 0.0,
farbe: farben.strasse,
};
zeichne_quader(frame, iso, &sockel);
}
// Ein Vermessungsfähnchen: Hier darfst du bauen
fn zeichne_faehnchen(frame: &mut Frame, iso: &Iso, farben: &Farben, feld: Feld, boden: f32) {
let fuss = iso.punkt(feld.spalte as f32 + 0.3, feld.reihe as f32 + 0.3, boden);
let spitze = Point::new(fuss.x, fuss.y - 20.0);
let stange = Stroke::default().with_color(farben.stange).with_width(1.5);
frame.stroke(&Path::line(fuss, spitze), stange);
let fahne = [
spitze,
Point::new(spitze.x + 11.0, spitze.y + 3.5),
Point::new(spitze.x, spitze.y + 7.0),
];
frame.fill(&vieleck(&fahne), farben.akzent);
}
// Rechnet zwischen Brett und Bildschirm hin und her
struct Iso {
ursprung: Point,
}
impl Iso {
fn neu(groesse: Size) -> Iso {
// Die Mitte des Bretts soll in der Mitte der Leinwand liegen
let mitte_s = SPALTEN as f32 / 2.0;
let mitte_r = REIHEN as f32 / 2.0;
let x = groesse.width / 2.0 - (mitte_s - mitte_r) * HALBE_BREITE;
let y = groesse.height / 2.0 - (mitte_s + mitte_r) * HALBE_HOEHE + 10.0;
Iso {
ursprung: Point::new(x, y),
}
}
// Hinweg: Brett (Spalte, Reihe, Höhe) -> Bildschirm
fn punkt(&self, s: f32, r: f32, z: f32) -> Point {
Point::new(
self.ursprung.x + (s - r) * HALBE_BREITE,
self.ursprung.y + (s + r) * HALBE_HOEHE - z,
)
}
// Rückweg: Bildschirm -> Feld auf dem Brett
fn feld_unter(&self, punkt: Point) -> Option<Feld> {
let dx = (punkt.x - self.ursprung.x) / HALBE_BREITE;
let dy = (punkt.y - self.ursprung.y) / HALBE_HOEHE;
let s = (dy + dx) / 2.0;
let r = (dy - dx) / 2.0;
if s < 0.0 || r < 0.0 {
return None;
}
let spalte = s as usize;
let reihe = r as usize;
if spalte < SPALTEN && reihe < REIHEN {
Some(Feld { spalte, reihe })
} else {
None
}
}
}
// Die Beschreibung eines Hauses: wie hoch, welche Farbe
struct Bauplan {
stockwerke: usize,
fassade: Color,
band: Option<Color>,
dach: Dach,
}
// Die Dachformen der Ketten
#[derive(Clone, Copy)]
enum Dach {
Flach,
Garten,
Zinnen,
Antenne,
Pyramide,
Stufen,
Turmspitze,
}
// Wie ein Hotel aussieht: Je größer die Kette, desto höher das Haus
fn hotel_bauplan(kette: Kette, groesse: usize, farben: &Farben, feld: Feld) -> Bauplan {
let grundhoehe = 3.0 + (groesse as f32).sqrt() * 1.5;
let schwankung = zufall(feld.index(), 7) % 2;
Bauplan {
stockwerke: grundhoehe as usize + schwankung as usize,
fassade: mischen(farben.beton, kettenfarbe(kette), 0.25),
band: Some(kettenfarbe(kette)),
dach: dachform(kette),
}
}
// Jede Kette hat ihre eigene Dachform
fn dachform(kette: Kette) -> Dach {
match kette {
Kette::Zugvogel => Dach::Flach,
Kette::Ankerplatz => Dach::Garten,
Kette::Bergfried => Dach::Zinnen,
Kette::Uferblick => Dach::Antenne,
Kette::Morgenrot => Dach::Pyramide,
Kette::Aurora => Dach::Stufen,
Kette::Koenigshof => Dach::Turmspitze,
}
}
// Jede Kette hat ihre Erkennungsfarbe
fn kettenfarbe(kette: Kette) -> Color {
match kette {
Kette::Zugvogel => Color::from_rgb8(0xf2, 0xb1, 0x34),
Kette::Ankerplatz => Color::from_rgb8(0x2b, 0xb3, 0xa6),
Kette::Bergfried => Color::from_rgb8(0xd9, 0x48, 0x3b),
Kette::Uferblick => Color::from_rgb8(0x3b, 0x7d, 0xd8),
Kette::Morgenrot => Color::from_rgb8(0xe8, 0x73, 0x8f),
Kette::Aurora => Color::from_rgb8(0x8e, 0x5b, 0xd6),
Kette::Koenigshof => Color::from_rgb8(0xc9, 0xa2, 0x27),
}
}
// Ein Quader: von (s0, r0) bis (s1, r1) auf dem Brett, von z0 bis z1 in der Höhe
struct Quader {
s0: f32,
r0: f32,
s1: f32,
r1: f32,
z0: f32,
z1: f32,
farbe: Color,
}
impl Quader {
fn oberseite(&self, iso: &Iso) -> [Point; 4] {
[
iso.punkt(self.s0, self.r0, self.z1),
iso.punkt(self.s1, self.r0, self.z1),
iso.punkt(self.s1, self.r1, self.z1),
iso.punkt(self.s0, self.r1, self.z1),
]
}
}
fn zeichne_quader(frame: &mut Frame, iso: &Iso, q: &Quader) {
let links = [
iso.punkt(q.s0, q.r1, q.z1),
iso.punkt(q.s1, q.r1, q.z1),
iso.punkt(q.s1, q.r1, q.z0),
iso.punkt(q.s0, q.r1, q.z0),
];
let rechts = [
iso.punkt(q.s1, q.r0, q.z1),
iso.punkt(q.s1, q.r1, q.z1),
iso.punkt(q.s1, q.r1, q.z0),
iso.punkt(q.s1, q.r0, q.z0),
];
let oben = q.oberseite(iso);
// Das Licht kommt von links: links mittel, rechts dunkel, oben am hellsten
frame.fill(&vieleck(&links), abdunkeln(q.farbe, 0.78));
frame.fill(&vieleck(&rechts), abdunkeln(q.farbe, 0.58));
frame.fill(&vieleck(&oben), q.farbe);
// Eine feine Lichtkante an den beiden vorderen Oberkanten
let kante = Path::new(|p| {
p.move_to(oben[3]);
p.line_to(oben[2]);
p.line_to(oben[1]);
});
frame.stroke(
&kante,
Stroke::default()
.with_color(aufhellen(q.farbe, 0.35))
.with_width(1.0),
);
}
// Baut aus einer Liste von Ecken eine geschlossene Fläche
fn vieleck(ecken: &[Point]) -> Path {
Path::new(|p| {
p.move_to(ecken[0]);
for ecke in &ecken[1..] {
p.line_to(*ecke);
}
p.close();
})
}
fn abdunkeln(farbe: Color, faktor: f32) -> Color {
Color {
r: farbe.r * faktor,
g: farbe.g * faktor,
b: farbe.b * faktor,
a: farbe.a,
}
}
fn aufhellen(farbe: Color, anteil: f32) -> Color {
Color {
r: farbe.r + (1.0 - farbe.r) * anteil,
g: farbe.g + (1.0 - farbe.g) * anteil,
b: farbe.b + (1.0 - farbe.b) * anteil,
a: farbe.a,
}
}
// Schreibt Geld wie in Deutschland: 6000 wird zu "6.000 €"
fn geld(betrag: u32) -> String {
let ziffern = betrag.to_string();
let mut mit_punkten = String::new();
for (stelle, ziffer) in ziffern.chars().enumerate() {
if stelle > 0 && (ziffern.len() - stelle) % 3 == 0 {
mit_punkten.push('.');
}
mit_punkten.push(ziffer);
}
format!("{mit_punkten} €")
}
// Mischt zwei Farben: anteil 0.0 ist ganz a, 1.0 ist ganz b
fn mischen(a: Color, b: Color, anteil: f32) -> Color {
Color {
r: a.r + (b.r - a.r) * anteil,
g: a.g + (b.g - a.g) * anteil,
b: a.b + (b.b - a.b) * anteil,
a: 1.0,
}
}
// Würfeln ohne Würfel: gleiche Eingabe, immer das gleiche Ergebnis
fn zufall(a: usize, b: usize) -> u32 {
let mut h = (a as u32).wrapping_mul(2_654_435_761) ^ (b as u32).wrapping_mul(40_503);
h ^= h >> 15;
h = h.wrapping_mul(2_246_822_519);
h ^= h >> 13;
h
}
// Alle Farben an einem Ort
#[derive(Clone, Copy)]
struct Farben {
himmel_oben: Color,
himmel_unten: Color,
strasse: Color,
stein: Color,
rasen: Color,
rasen_hell: Color,
beschriftung: Color,
akzent: Color,
schrift: Color,
beton: Color,
fenster: Color,
licht: Color,
stange: Color,
tafel: Color,
}
impl Farben {
fn tag() -> Farben {
Farben {
himmel_oben: Color::from_rgb8(0x8e, 0xc5, 0xe8),
himmel_unten: Color::from_rgb8(0xe9, 0xf2, 0xf7),
strasse: Color::from_rgb8(0x5a, 0x62, 0x70),
stein: Color::from_rgb8(0xd9, 0xd4, 0xc7),
rasen: Color::from_rgb8(0x9c, 0xcb, 0x6b),
rasen_hell: Color::from_rgb8(0xc4, 0xe6, 0x8f),
beschriftung: Color::from_rgba8(0x1e, 0x32, 0x14, 0.55),
akzent: Color::from_rgb8(0xce, 0x5a, 0x1e),
schrift: Color::from_rgb8(0x1c, 0x2a, 0x3a),
beton: Color::from_rgb8(0xe8, 0xe2, 0xd6),
fenster: Color::from_rgb8(0x5b, 0x8d, 0xb8),
licht: Color::from_rgb8(0xff, 0xd2, 0x7a),
stange: Color::WHITE,
tafel: Color::from_rgb8(0xfb, 0xf8, 0xf1),
}
}
fn nacht() -> Farben {
Farben {
himmel_oben: Color::from_rgb8(0x0b, 0x10, 0x26),
himmel_unten: Color::from_rgb8(0x2a, 0x23, 0x50),
strasse: Color::from_rgb8(0x2a, 0x30, 0x42),
stein: Color::from_rgb8(0x5c, 0x60, 0x6e),
rasen: Color::from_rgb8(0x2e, 0x4a, 0x36),
rasen_hell: Color::from_rgb8(0x4d, 0x7a, 0x4f),
beschriftung: Color::from_rgba8(0xdc, 0xe6, 0xd2, 0.45),
akzent: Color::from_rgb8(0xe0, 0x7a, 0x2e),
schrift: Color::from_rgb8(0xe8, 0xec, 0xf5),
beton: Color::from_rgb8(0x6d, 0x6a, 0x78),
fenster: Color::from_rgb8(0x27, 0x32, 0x4a),
licht: Color::from_rgb8(0xff, 0xd2, 0x7a),
stange: Color::from_rgb8(0xcf, 0xd3, 0xdc),
tafel: Color::from_rgb8(0x1e, 0x22, 0x33),
}
}
}
src/spiel.rs
use rand::seq::SliceRandom;
// Das Brett: 12 Spalten (1 bis 12) und 9 Reihen (A bis I)
pub const SPALTEN: usize = 12;
pub const REIHEN: usize = 9;
// So viele Plättchen hat man auf der Hand
pub const HANDGROESSE: usize = 6;
// Geld und Aktien
pub const STARTGELD: u32 = 6000;
pub const AKTIEN_JE_KETTE: u32 = 25;
pub const KAEUFE_JE_ZUG: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Feld {
pub spalte: usize,
pub reihe: usize,
}
impl Feld {
// Die Nummer des Feldes in einer Liste mit 108 Einträgen
pub fn index(self) -> usize {
self.reihe * SPALTEN + self.spalte
}
// Die bis zu vier Nachbarn: links, rechts, oben, unten
pub fn nachbarn(self) -> Vec<Feld> {
let mut liste = Vec::new();
if self.spalte > 0 {
liste.push(Feld {
spalte: self.spalte - 1,
reihe: self.reihe,
});
}
if self.spalte + 1 < SPALTEN {
liste.push(Feld {
spalte: self.spalte + 1,
reihe: self.reihe,
});
}
if self.reihe > 0 {
liste.push(Feld {
spalte: self.spalte,
reihe: self.reihe - 1,
});
}
if self.reihe + 1 < REIHEN {
liste.push(Feld {
spalte: self.spalte,
reihe: self.reihe + 1,
});
}
liste
}
// Der Name wie auf dem Spielplan: "1A" bis "12I"
pub fn name(self) -> String {
let buchstabe = (b'A' + self.reihe as u8) as char;
format!("{}{}", self.spalte + 1, buchstabe)
}
}
// Die sieben Hotelketten
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kette {
Zugvogel,
Ankerplatz,
Bergfried,
Uferblick,
Morgenrot,
Aurora,
Koenigshof,
}
impl Kette {
pub const ALLE: [Kette; 7] = [
Kette::Zugvogel,
Kette::Ankerplatz,
Kette::Bergfried,
Kette::Uferblick,
Kette::Morgenrot,
Kette::Aurora,
Kette::Koenigshof,
];
// Die Nummer der Kette: 0 bis 6
pub fn nr(self) -> usize {
self as usize
}
// Preisstufe: 0 günstig, 1 mittel, 2 teuer
pub fn stufe(self) -> u32 {
match self {
Kette::Zugvogel | Kette::Ankerplatz => 0,
Kette::Bergfried | Kette::Uferblick | Kette::Morgenrot => 1,
Kette::Aurora | Kette::Koenigshof => 2,
}
}
pub fn name(self) -> &'static str {
match self {
Kette::Zugvogel => "Zugvogel",
Kette::Ankerplatz => "Ankerplatz",
Kette::Bergfried => "Bergfried",
Kette::Uferblick => "Uferblick",
Kette::Morgenrot => "Morgenrot",
Kette::Aurora => "Aurora",
Kette::Koenigshof => "Königshof",
}
}
}
// Was auf einem Feld steht
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Belegung {
Frei,
Rohbau,
Hotel(Kette),
}
// Woran das Spiel gerade ist
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Phase {
// Warten auf ein Plättchen
Legen,
// Hier wartet eine neue Kette auf ihren Namen
Gruenden(Feld),
// Aktien kaufen, bis zu drei Stück
Kaufen { gekauft: u32 },
}
// Alles, was ein Spieler tun kann
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Zug {
Legen(Feld),
Gruenden(Kette),
Kaufen(Kette),
ZugBeenden,
}
// Wer da spielt: ein Mensch oder der Computer
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Art {
Mensch,
Tourist,
}
// Was einem Spieler gehört
pub struct Spieler {
pub name: String,
pub art: Art,
pub geld: u32,
pub aktien: [u32; 7],
pub hand: Vec<Feld>,
}
// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
pub brett: Vec<Belegung>,
pub stapel: Vec<Feld>,
pub phase: Phase,
pub spieler: Vec<Spieler>,
// Wer gerade dran ist: die Nummer in der Liste der Spieler
pub am_zug: usize,
// Was bisher passiert ist, ein Eintrag pro Handlung
pub protokoll: Vec<String>,
// Die Aktien, die noch niemandem gehören
pub bank: [u32; 7],
}
impl Spiel {
pub fn neu(teilnehmer: &[(&str, Art)]) -> Spiel {
// Für jedes Feld ein Plättchen, dann gut mischen
let mut stapel = Vec::new();
for reihe in 0..REIHEN {
for spalte in 0..SPALTEN {
stapel.push(Feld { spalte, reihe });
}
}
stapel.shuffle(&mut rand::rng());
let spieler = teilnehmer
.iter()
.map(|&(name, art)| Spieler {
name: name.to_string(),
art,
geld: STARTGELD,
aktien: [0; 7],
hand: Vec::new(),
})
.collect();
let mut spiel = Spiel {
brett: vec![Belegung::Frei; SPALTEN * REIHEN],
stapel,
phase: Phase::Legen,
spieler,
am_zug: 0,
protokoll: Vec::new(),
bank: [AKTIEN_JE_KETTE; 7],
};
// Reihum zieht jeder seine Hand, dann fängt der Erste an
for nr in 0..spiel.spieler.len() {
spiel.am_zug = nr;
spiel.nachziehen();
}
spiel.am_zug = 0;
spiel
}
// Der Spieler, der gerade dran ist
pub fn aktueller(&self) -> &Spieler {
&self.spieler[self.am_zug]
}
// Dasselbe zum Verändern
fn dran(&mut self) -> &mut Spieler {
&mut self.spieler[self.am_zug]
}
// Schreibt ins Protokoll, was der Spieler am Zug getan hat
fn notiere(&mut self, was: String) {
let eintrag = format!("{} {was}", self.aktueller().name);
self.protokoll.push(eintrag);
}
// Was steht auf diesem Feld?
pub fn belegung(&self, feld: Feld) -> Belegung {
self.brett[feld.index()]
}
// Füllt die Hand des Spielers am Zug wieder auf, solange der Stapel reicht
fn nachziehen(&mut self) {
let nr = self.am_zug;
while self.spieler[nr].hand.len() < HANDGROESSE {
match self.stapel.pop() {
Some(feld) => self.spieler[nr].hand.push(feld),
None => break,
}
}
self.spieler[nr]
.hand
.sort_by_key(|feld| (feld.spalte, feld.reihe));
}
// Darf der Spieler am Zug dieses Plättchen jetzt legen?
pub fn darf_legen(&self, feld: Feld) -> bool {
if self.phase != Phase::Legen || !self.aktueller().hand.contains(&feld) {
return false;
}
let ketten = self.ketten_neben(feld);
// Zwei Ketten auf einmal berühren: Das ist eine Fusion, die kommt später
if ketten.len() > 1 {
return false;
}
// Eine neue Kette gründen geht nur, wenn noch eine frei ist
let gruendet = ketten.is_empty() && self.rohbau_neben(feld);
!(gruendet && self.freie_ketten().is_empty())
}
// Hat der Spieler am Zug überhaupt ein Plättchen, das passt?
pub fn kann_legen(&self) -> bool {
self.aktueller()
.hand
.iter()
.any(|&feld| self.darf_legen(feld))
}
// Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht.
pub fn legen(&mut self, feld: Feld) -> bool {
if !self.darf_legen(feld) {
return false;
}
let ketten = self.ketten_neben(feld);
let rohbau_daneben = self.rohbau_neben(feld);
self.dran().hand.retain(|&h| h != feld);
self.notiere(format!("legt {}", feld.name()));
self.brett[feld.index()] = Belegung::Rohbau;
if let Some(&kette) = ketten.first() {
self.ausbreiten(feld, kette);
self.nach_dem_legen();
} else if rohbau_daneben {
self.phase = Phase::Gruenden(feld);
} else {
self.nach_dem_legen();
}
true
}
// Gründet an der wartenden Stelle eine neue Kette
pub fn gruenden(&mut self, kette: Kette) {
let Phase::Gruenden(feld) = self.phase else {
return;
};
if !self.freie_ketten().contains(&kette) {
return;
}
self.ausbreiten(feld, kette);
// Wer gründet, bekommt eine Aktie geschenkt
if self.bank[kette.nr()] > 0 {
self.bank[kette.nr()] -= 1;
self.dran().aktien[kette.nr()] += 1;
}
self.notiere(format!("gründet {}", kette.name()));
self.nach_dem_legen();
}
// Darf man jetzt eine Aktie dieser Kette kaufen?
pub fn darf_kaufen(&self, kette: Kette) -> bool {
let Phase::Kaufen { gekauft } = self.phase else {
return false;
};
let preis = self.preis(kette);
gekauft < KAEUFE_JE_ZUG
&& preis > 0
&& self.bank[kette.nr()] > 0
&& self.aktueller().geld >= preis
}
// Kauft eine Aktie. Nach dem dritten Kauf ist der Zug vorbei.
pub fn kaufen(&mut self, kette: Kette) {
if !self.darf_kaufen(kette) {
return;
}
let Phase::Kaufen { gekauft } = self.phase else {
return;
};
let preis = self.preis(kette);
let spieler = self.dran();
spieler.geld -= preis;
spieler.aktien[kette.nr()] += 1;
self.bank[kette.nr()] -= 1;
self.notiere(format!("kauft {}", kette.name()));
if gekauft + 1 == KAEUFE_JE_ZUG {
self.naechster_zug();
} else {
self.phase = Phase::Kaufen {
gekauft: gekauft + 1,
};
}
}
// Beendet die Kaufphase und damit den Zug
pub fn zug_beenden(&mut self) {
match self.phase {
Phase::Kaufen { .. } => self.naechster_zug(),
// Passt kein Plättchen, geht es ohne Legen weiter
Phase::Legen if !self.kann_legen() => {
self.notiere(String::from("kann nichts legen"));
self.nach_dem_legen();
}
_ => {}
}
}
// Alle Züge, die der Spieler am Zug gerade machen darf
pub fn moegliche_zuege(&self) -> Vec<Zug> {
let mut zuege = Vec::new();
match self.phase {
Phase::Legen => {
for &feld in &self.aktueller().hand {
if self.darf_legen(feld) {
zuege.push(Zug::Legen(feld));
}
}
if zuege.is_empty() {
zuege.push(Zug::ZugBeenden);
}
}
Phase::Gruenden(_) => {
for kette in self.freie_ketten() {
zuege.push(Zug::Gruenden(kette));
}
}
Phase::Kaufen { .. } => {
for kette in self.aktive_ketten() {
if self.darf_kaufen(kette) {
zuege.push(Zug::Kaufen(kette));
}
}
zuege.push(Zug::ZugBeenden);
}
}
zuege
}
// Führt einen Zug aus, egal ob er vom Menschen oder vom Computer kommt
pub fn ausfuehren(&mut self, zug: Zug) {
match zug {
Zug::Legen(feld) => {
self.legen(feld);
}
Zug::Gruenden(kette) => self.gruenden(kette),
Zug::Kaufen(kette) => self.kaufen(kette),
Zug::ZugBeenden => self.zug_beenden(),
}
}
// Gibt es schon Ketten, darf man Aktien kaufen. Sonst ist der Zug vorbei.
fn nach_dem_legen(&mut self) {
if self.aktive_ketten().is_empty() {
self.naechster_zug();
} else {
self.phase = Phase::Kaufen { gekauft: 0 };
}
}
// Nachziehen, und der Nächste ist dran
fn naechster_zug(&mut self) {
self.nachziehen();
self.am_zug = (self.am_zug + 1) % self.spieler.len();
self.phase = Phase::Legen;
}
// Wie viele Felder gehören zu dieser Kette?
pub fn groesse(&self, kette: Kette) -> usize {
self.brett
.iter()
.filter(|&&belegung| belegung == Belegung::Hotel(kette))
.count()
}
// Was eine Aktie dieser Kette gerade kostet
pub fn preis(&self, kette: Kette) -> u32 {
let grundpreis = match self.groesse(kette) {
0 => return 0,
groesse @ 1..=5 => groesse as u32 * 100,
6..=10 => 600,
11..=20 => 700,
21..=30 => 800,
31..=40 => 900,
_ => 1000,
};
grundpreis + kette.stufe() * 100
}
// Alle Ketten, die schon auf dem Brett stehen
pub fn aktive_ketten(&self) -> Vec<Kette> {
Kette::ALLE
.into_iter()
.filter(|&kette| self.groesse(kette) > 0)
.collect()
}
// Alle Ketten, die noch nicht auf dem Brett stehen
pub fn freie_ketten(&self) -> Vec<Kette> {
Kette::ALLE
.into_iter()
.filter(|&kette| self.groesse(kette) == 0)
.collect()
}
// Steht neben diesem Feld ein Rohbau?
fn rohbau_neben(&self, feld: Feld) -> bool {
feld.nachbarn()
.iter()
.any(|&nachbar| self.belegung(nachbar) == Belegung::Rohbau)
}
// Welche Ketten grenzen an dieses Feld? Jede nur einmal.
fn ketten_neben(&self, feld: Feld) -> Vec<Kette> {
let mut ketten = Vec::new();
for nachbar in feld.nachbarn() {
if let Belegung::Hotel(kette) = self.belegung(nachbar) {
if !ketten.contains(&kette) {
ketten.push(kette);
}
}
}
ketten
}
// Färbt ein Feld und alle angrenzenden Rohbauten in der Farbe der Kette
fn ausbreiten(&mut self, start: Feld, kette: Kette) {
let mut offen = vec![start];
while let Some(feld) = offen.pop() {
self.brett[feld.index()] = Belegung::Hotel(kette);
for nachbar in feld.nachbarn() {
if self.belegung(nachbar) == Belegung::Rohbau {
offen.push(nachbar);
}
}
}
}
}
src/ki.rs
use rand::seq::IndexedRandom;
use crate::spiel::{Art, Spiel, Zug};
// Welchen Zug macht der Computer, der gerade dran ist?
pub fn waehle_zug(spiel: &Spiel) -> Zug {
match spiel.aktueller().art {
Art::Mensch | Art::Tourist => tourist(spiel),
}
}
// Der Tourist denkt nicht nach: Er nimmt irgendeinen erlaubten Zug
fn tourist(spiel: &Spiel) -> Zug {
let zuege = spiel.moegliche_zuege();
zuege
.choose(&mut rand::rng())
.copied()
.unwrap_or(Zug::ZugBeenden)
}
Spieler, und
am_zug geht reihum, mit % wieder von vorn. Jede Handlung ist ein Wert vom
Typ Zug, moegliche_zuege sammelt alle erlaubten, und
ausfuehren ist der gemeinsame Eingang für Mensch und Computer. Prüfen
(darf_legen) und Handeln (legen) sind getrennt. Eine
Subscription liefert den Takt für den Computer, und match-Zweige mit
Bedingung sorgen dafür, dass du nur klicken kannst, wenn du dran bist.Zum Ausprobieren
- Dein Name: Spiel unter deinem eigenen Namen statt als Leo.
- Turbo: Die Touristen sollen fünfmal so schnell ziehen. Und wie viele Gegner verträgt der Tisch? Probier es mit fünf.
- Computer gegen Computer: Mach aus Leo einen Touristen und schau einfach zu. Was passiert, wenn der Stapel leer ist? (Keine Sorge, das reparieren wir in Etappe 7.)
Lösungen anzeigen
1. In Bauloewe::neu den Namen im ersten Paar
ändern. Die Tafel zeigt ihn sofort an, das Protokoll auch.
2. In abo aus 600
120 machen. Einen fünften Gegner bekommst du mit einer weiteren Zeile wie
("Tessa", Art::Tourist),. Weil alles mit self.spieler.len() rechnet,
funktioniert der Rest von selbst. Nur die Tafel wird etwas länger.
3. Mit ("Leo", Art::Tourist) spielt der Computer
gegen sich selbst, denn mensch_am_zug ist dann nie wahr und das Abo tickt immer.
Ist der Stapel leer und niemand kann mehr legen, sagt jeder nur noch „kann nichts legen“, und
das Spiel endet nie. Genau das werden wir mit einer richtigen Schlussabrechnung
lösen.
Etappe 5 geschafft 🎲
Du spielst gegen den Computer! Noch denkt er nicht nach, aber die Maschinerie steht: erlaubte Züge sammeln, einen auswählen, ausführen. In der nächsten Etappe kommt die Regel, die das Spiel spannend macht: Stoßen zwei Ketten zusammen, schluckt die große die kleine, und die Aktionäre der kleinen bekommen eine Prämie.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren