Spielende und Abrechnung
Jedes gute Spiel braucht ein Ende. Heute werden tote Plättchen abgeworfen, wer vorne liegt, darf die Partie beenden, und wenn gar nichts mehr passt, endet sie von selbst. In der Schlussabrechnung gibt es die letzten Prämien, die Bank kauft alle Aktien zurück, und eine Rangliste zeigt, wer der größte Baulöwe ist.
Tote Plättchen
Seit Etappe 6 gibt es sichere Ketten, und damit ein neues Problem: Ein Plättchen, das zwei
sichere Ketten verbinden würde, passt nie wieder. Es ist tot. Im echten Spiel
wirft man solche Plättchen am Ende seines Zuges ab und zieht Ersatz. Damit wir tote Plättchen
erkennen, zerlegen wir darf_legen in drei Fragen, die jeweils für sich nützlich
sind:
// 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 sichere Ketten kann niemand zusammenlegen let sichere = ketten .iter() .filter(|&&kette| self.groesse(kette) >= SICHER) .count(); if sichere >= 2 { 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()) }// Darf der Spieler am Zug dieses Plättchen jetzt legen? pub fn darf_legen(&self, feld: Feld) -> bool { self.phase == Phase::Legen && self.aktueller().hand.contains(&feld) && self.passt(feld) } // Passt dieses Plättchen gerade aufs Brett, egal wer es auf der Hand hat? pub fn passt(&self, feld: Feld) -> bool { if self.ist_tot(feld) { return false; } // Eine neue Kette gründen geht nur, wenn noch eine frei ist let gruendet = self.ketten_neben(feld).is_empty() && self.rohbau_neben(feld); !(gruendet && self.freie_ketten().is_empty()) } // Ein totes Plättchen würde zwei sichere Ketten verbinden. Es passt nie mehr. pub fn ist_tot(&self, feld: Feld) -> bool { let sichere = self .ketten_neben(feld) .iter() .filter(|&&kette| self.groesse(kette) >= SICHER) .count(); sichere >= 2 }// Hat der Spieler am Zug überhaupt ein Plättchen, das passt?
ist_totprüft nur das Brett: Liegen zwei sichere Ketten daneben? Dann ist das Plättchen für immer verloren.passtfragt, ob ein Plättchen jetzt gerade aufs Brett darf, egal wer es in der Hand hat. Ein Plättchen, das eine achte Kette gründen würde, passt im Moment nicht, kann aber später wieder passen, wenn eine Kette geschluckt wurde.darf_legenschrumpft auf eine Zeile: richtige Phase, auf der Hand, und es passt.&&prüft von links nach rechts und hört beim ersten Nein auf. Die teure Prüfung am Brett läuft also nur, wenn die billigen schon bestanden sind.
Abgeworfen wird in nachziehen, bevor die Hand aufgefüllt wird:
fn nachziehen(&mut self) { let nr = self.am_zug;// Tote Plättchen kommen aus dem Spiel let tote: Vec<Feld> = self.spieler[nr] .hand .iter() .copied() .filter(|&feld| self.ist_tot(feld)) .collect(); for feld in tote { self.spieler[nr].hand.retain(|&h| h != feld); let eintrag = format!("{} wirft {} ab", self.spieler[nr].name, feld.name()); self.protokoll.push(eintrag); }while self.spieler[nr].hand.len() < HANDGROESSE {
Warum erst eine Liste tote sammeln und dann entfernen? Weil wir die Hand nicht
verändern dürfen, während wir sie noch durchgehen. Rust würde das nicht zulassen, und das ist
gut so: In vielen anderen Sprachen ist genau das eine beliebte Quelle für schwer zu findende
Fehler.
cargo run
Spiel, bis zwei Ketten sicher sind und nur noch ein Feld zwischen ihnen frei ist. Hast du das Plättchen dafür, bekommt es kein Fähnchen mehr, und am Ende deines Zuges verschwindet es aus deiner Hand. Im Protokoll steht dann zum Beispiel „Leo wirft 7E ab“.Schluss!
Ein Spiel endet auf zwei Wegen. Entweder wird es beendet: Wer dran ist, darf Schluss machen, sobald eine Kette 41 Felder oder mehr hat, oder wenn alle Ketten auf dem Brett sicher sind. Das ist ein taktisches Werkzeug, denn wer vorne liegt, beendet gern früh. Oder es endet von selbst, wenn der Stapel leer ist und niemand mehr ein passendes Plättchen hat. Dafür bekommt das Spiel eine neue Konstante, eine neue Phase und einen neuen Zug:
pub const SICHER: usize = 11;// Ist eine Kette so groß, darf man das Spiel beenden pub const RIESIG: usize = 41;⋮ueberlebende: Kette, },// Das Spiel ist aus Ende,}⋮ZugBeenden,SpielBeenden,}
In naechster_zug prüfen wir nach jedem Zug, ob es von selbst vorbei ist. Darunter
kommen drei neue Methoden:
self.am_zug = (self.am_zug + 1) % self.spieler.len(); self.phase = Phase::Legen;// Stapel leer und nichts passt mehr: Dann ist von selbst Schluss if self.stapel.is_empty() && self.niemand_kann_legen() { self.schluss(); } } // Kein Spieler hat noch ein Plättchen, das aufs Brett passt fn niemand_kann_legen(&self) -> bool { self.spieler .iter() .all(|spieler| spieler.hand.iter().all(|&feld| !self.passt(feld))) } // Beenden darf man, wenn eine Kette riesig ist oder alle Ketten sicher sind pub fn darf_beenden(&self) -> bool { let aktive = self.aktive_ketten(); let riesig = aktive.iter().any(|&kette| self.groesse(kette) >= RIESIG); let alle_sicher = aktive.iter().all(|&kette| self.groesse(kette) >= SICHER); self.phase == Phase::Legen && !aktive.is_empty() && (riesig || alle_sicher) } // Schluss: letzte Prämien, dann kauft die Bank alle Aktien zum Kurs zurück fn schluss(&mut self) { for kette in self.aktive_ketten() { self.praemien_zahlen(kette); let preis = self.preis(kette); for spieler in self.spieler.iter_mut() { spieler.geld += spieler.aktien[kette.nr()] * preis; spieler.aktien[kette.nr()] = 0; } } self.protokoll.push(String::from("Das Spiel ist aus!")); self.phase = Phase::Ende; }// Wie viele Felder gehören zu dieser Kette?
niemand_kann_legenverschachtelt zweiall: Für alle Spieler gilt, dass für alle Plättchen auf ihrer Hand gilt, dass sie nicht passen. Genau dafür haben wir in Schritt 1passtgebaut, das nicht danach fragt, wer dran ist.darf_beendenbeantwortet die Frage in drei Teilen. Eine Besonderheit:allauf einer leeren Liste ist immer wahr. Ohne die Prüfung!aktive.is_empty()dürfte man also gleich im ersten Zug beenden, weil „alle null Ketten sicher“ sind.schlussist die Schlussabrechnung. Für jede Kette auf dem Brett gibt es noch einmal die Prämien wie bei einer Fusion, dann kauft die Bank alle Aktien zum aktuellen Kurs zurück. Aktien von Ketten, die nicht mehr auf dem Brett stehen, sind wertlos.
Beenden gehört zu den möglichen Zügen beim Legen, und ausfuehren prüft es noch
einmal, bevor es Schluss macht. In der Endphase gibt es keine Züge mehr:
if zuege.is_empty() { zuege.push(Zug::ZugBeenden); }if self.darf_beenden() { zuege.push(Zug::SpielBeenden); }⋮zuege.push(Zug::Aktien(Entscheidung::Behalten)); }Phase::Ende => {}⋮Zug::ZugBeenden => self.zug_beenden(),Zug::SpielBeenden => { if self.darf_beenden() { self.notiere(String::from("beendet das Spiel")); self.schluss(); } }}
In main.rs soll der Computer nach dem Ende aufhören zu ticken, und du bekommst
einen goldenen Knopf zum Beenden, sobald du darfst. Dafür benutzen wir den
farbknopf aus Etappe 6:
fn abo(&self) -> Subscription<Message> {if self.mensch_am_zug() {if self.mensch_am_zug() || self.spiel.phase == Phase::Ende {⋮rechts = rechts.push(rest).push(fertig); }if self.mensch_am_zug() && self.spiel.darf_beenden() { let farbe = kettenfarbe(Kette::Koenigshof); rechts = rechts.push(farbknopf("Spiel beenden", farbe, Zug::SpielBeenden)); }
cargo run
Spiel so lange, bis alle Ketten auf dem Brett sicher sind oder eine 41 Felder hat. Dann erscheint unten rechts „Spiel beenden“. Klickst du darauf, hört der Computer auf zu ziehen, und das Protokoll meldet „Das Spiel ist aus!“. Die Aktien sind verkauft, in der Tafel „Am Tisch“ steht nur noch Geld. Aber auch die Touristen können beenden, und weil sie zufällig wählen, tun sie das manchmal ziemlich unvermittelt.Die Abrechnung
Wer hat gewonnen? Das soll eine eigene Karte verraten, mit einer Rangliste und einem Knopf für die nächste Runde. Zuerst sortiert das Spiel seine Spieler nach Geld:
// Alle Spieler, der reichste zuerst pub fn rangliste(&self) -> Vec<&Spieler> { let mut liste: Vec<&Spieler> = self.spieler.iter().collect(); liste.sort_by_key(|spieler| Reverse(spieler.geld)); liste }// Schluss: letzte Prämien, dann kauft die Bank alle Aktien zum Kurs zurück
Die Liste enthält keine Kopien der Spieler, sondern Verweise
(&Spieler). Sortiert werden nur die Verweise, die Spieler selbst bleiben, wo sie
sind. Reverse kennst du schon aus der Fusion.
In main.rs gibt es einen Brief für ein neues Spiel. Beim Neustart soll der Tag- oder
Nachtmodus erhalten bleiben. Wir merken ihn uns deshalb, ersetzen mit
*self = Bauloewe::neu() den ganzen Zustand auf einmal und setzen ihn danach
zurück:
KiTakt,NeuesSpiel,}⋮// Noch eine Runde, Tag oder Nacht bleiben, wie sie sind Message::NeuesSpiel => { let nacht = self.nacht; *self = Bauloewe::neu(); self.nacht = nacht; }// Der Takt für den Computer
Die Karte bauen wir mit dem dialogfenster aus Etappe 6. Die Rangliste wird ein
mehrzeiliger Text, jede Zeile endet mit \n, dem Zeichen für einen Zeilenumbruch.
trim_end schneidet den letzten Umbruch wieder ab:
// Die Abrechnung: Wer ist der größte Baulöwe? fn schlussdialog(&self, farben: Farben) -> Element<'_, Message> { let rangliste = self.spiel.rangliste(); let mut tabelle = String::new(); for (platz, spieler) in rangliste.iter().enumerate() { let zeile = format!("{}. {}: {}\n", platz + 1, spieler.name, geld(spieler.geld)); tabelle.push_str(&zeile); } let titel = if rangliste[0].art == Art::Mensch { "Du bist der größte Baulöwe!" } else { "Das Spiel ist aus!" }; let nochmal = button(text("Neues Spiel").size(14)) .padding([8, 16]) .on_press(Message::NeuesSpiel) .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() }); dialogfenster(farben, titel, tabelle.trim_end().to_string(), row![nochmal]) }fn farben(&self) -> Farben {
_ => {} } }if self.spiel.phase == Phase::Ende { ebenen = ebenen.push(self.schlussdialog(farben)); }ebenen.into()⋮let hinweis = match self.maus_feld {_ if self.spiel.phase == Phase::Ende => String::from("Schlussabrechnung"),_ if !self.mensch_am_zug() => format!("{} ist am Zug …", self.spiel.aktueller().name),
Die Schlusskarte erscheint für alle, nicht nur, wenn du dran bist. Deshalb steht sie außerhalb des
Blocks mit if self.mensch_am_zug().
cargo run
Spiel eine Partie bis zum Ende. Über dem Brett erscheint die Abrechnung mit allen vier Spielern. Liegst du vorne, heißt es „Du bist der größte Baulöwe!“. Mit „Neues Spiel“ geht es sofort in die nächste Runde, mit frisch gemischtem Stapel.Der komplette Code nach Etappe 7
Hier ist der vollständige Stand nach Etappe 7. 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::{Row, 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, Entscheidung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SICHER, 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,
NeuesSpiel,
}
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() || self.spiel.phase == Phase::Ende {
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(_) => {}
// Noch eine Runde, Tag oder Nacht bleiben, wie sie sind
Message::NeuesSpiel => {
let nacht = self.nacht;
*self = Bauloewe::neu();
self.nacht = nacht;
}
// 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.spiel.phase == Phase::Ende => String::from("Schlussabrechnung"),
_ 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);
}
if self.mensch_am_zug() && self.spiel.darf_beenden() {
let farbe = kettenfarbe(Kette::Koenigshof);
rechts = rechts.push(farbknopf("Spiel beenden", farbe, Zug::SpielBeenden));
}
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 {
// Sichere Ketten kann niemand mehr schlucken
let wort = if groesse >= SICHER {
"sicher"
} else {
"Felder"
};
(format!("{groesse} {wort}"), 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() {
match self.spiel.phase {
Phase::Gruenden(_) => ebenen = ebenen.push(self.gruendungsdialog(farben)),
Phase::Fusion {
tot, ueberlebende, ..
} => ebenen = ebenen.push(self.fusionsdialog(farben, tot, ueberlebende)),
_ => {}
}
}
if self.spiel.phase == Phase::Ende {
ebenen = ebenen.push(self.schlussdialog(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 zug = Zug::Gruenden(kette);
knoepfe = knoepfe.push(farbknopf(kette.name(), kettenfarbe(kette), zug));
}
let frage = "Deine Häuser stehen jetzt nebeneinander. Welche Kette gründest du?";
dialogfenster(farben, "Neue Hotelkette!", frage.to_string(), knoepfe)
}
// Das Fenster für deine Aktien einer Kette, die gerade geschluckt wird
fn fusionsdialog(
&self,
farben: Farben,
tot: Kette,
ueberlebende: Kette,
) -> Element<'_, Message> {
let anzahl = self.ich().aktien[tot.nr()];
let preis = self.spiel.preis(tot);
let frage = format!(
"{} schluckt {}. Du hast {anzahl} Aktien von {}, eine ist {} wert. Was machst du damit?",
ueberlebende.name(),
tot.name(),
tot.name(),
geld(preis)
);
let verkaufen = format!("Verkaufen für {}", geld(anzahl * preis));
let tauschen = format!("2 zu 1 in {} tauschen", ueberlebende.name());
let knoepfe = row![
farbknopf(
&verkaufen,
kettenfarbe(tot),
Zug::Aktien(Entscheidung::Verkaufen)
),
farbknopf(
&tauschen,
kettenfarbe(ueberlebende),
Zug::Aktien(Entscheidung::Tauschen)
),
farbknopf(
"Behalten",
farben.akzent,
Zug::Aktien(Entscheidung::Behalten)
),
]
.spacing(8);
dialogfenster(farben, "Fusion!", frage, knoepfe)
}
// Die Abrechnung: Wer ist der größte Baulöwe?
fn schlussdialog(&self, farben: Farben) -> Element<'_, Message> {
let rangliste = self.spiel.rangliste();
let mut tabelle = String::new();
for (platz, spieler) in rangliste.iter().enumerate() {
let zeile = format!("{}. {}: {}\n", platz + 1, spieler.name, geld(spieler.geld));
tabelle.push_str(&zeile);
}
let titel = if rangliste[0].art == Art::Mensch {
"Du bist der größte Baulöwe!"
} else {
"Das Spiel ist aus!"
};
let nochmal = button(text("Neues Spiel").size(14))
.padding([8, 16])
.on_press(Message::NeuesSpiel)
.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()
});
dialogfenster(farben, titel, tabelle.trim_end().to_string(), row![nochmal])
}
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 farbiger Knopf, der einen Spielzug verschickt
fn farbknopf<'a>(beschriftung: &str, farbe: Color, zug: Zug) -> Element<'a, Message> {
button(text(beschriftung.to_string()).size(14))
.padding([8, 12])
.on_press(Message::Spielzug(zug))
.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()
}
})
.into()
}
// Eine Karte mit Titel, Frage und Knöpfen über einem dunklen Schleier
fn dialogfenster<'a>(
farben: Farben,
titel: &'a str,
frage: String,
knoepfe: Row<'a, Message>,
) -> Element<'a, Message> {
let titel = text(titel)
.size(24)
.font(Font {
weight: font::Weight::Bold,
..Font::DEFAULT
})
.color(farben.schrift);
let frage = text(frage)
.size(15)
.color(farben.schrift)
.align_x(text::Alignment::Center);
let karte = container(
column![titel, frage, knoepfe]
.spacing(14)
.align_x(alignment::Horizontal::Center),
)
.padding(24)
.max_width(640)
.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()
}
// 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 std::cmp::Reverse;
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;
// Ab dieser Größe kann eine Kette nicht mehr geschluckt werden
pub const SICHER: usize = 11;
// Ist eine Kette so groß, darf man das Spiel beenden
pub const RIESIG: usize = 41;
#[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,
},
// Eine Kette wird geschluckt, die Aktionäre entscheiden reihum
Fusion {
feld: Feld,
tot: Kette,
ueberlebende: Kette,
},
// Das Spiel ist aus
Ende,
}
// Was ein Aktionär mit Aktien einer geschluckten Kette macht
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Entscheidung {
Verkaufen,
Tauschen,
Behalten,
}
// Alles, was ein Spieler tun kann
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Zug {
Legen(Feld),
Gruenden(Kette),
Kaufen(Kette),
Aktien(Entscheidung),
ZugBeenden,
SpielBeenden,
}
// 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>,
// Bei einer Fusion: Diese Ketten werden noch geschluckt
pub offene_fusionen: Vec<Kette>,
// Diese Aktionäre müssen noch entscheiden, der vorderste zuerst
pub entscheider: Vec<usize>,
// 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(),
offene_fusionen: Vec::new(),
entscheider: 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
}
// Wer gerade handeln muss: Bei einer Fusion entscheiden die Aktionäre reihum
pub fn handelnder(&self) -> usize {
match self.phase {
Phase::Fusion { .. } => self.entscheider[0],
_ => self.am_zug,
}
}
// Der Spieler, der gerade handeln muss
pub fn aktueller(&self) -> &Spieler {
&self.spieler[self.handelnder()]
}
// Dasselbe zum Verändern
fn dran(&mut self) -> &mut Spieler {
let nr = self.handelnder();
&mut self.spieler[nr]
}
// 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;
// Tote Plättchen kommen aus dem Spiel
let tote: Vec<Feld> = self.spieler[nr]
.hand
.iter()
.copied()
.filter(|&feld| self.ist_tot(feld))
.collect();
for feld in tote {
self.spieler[nr].hand.retain(|&h| h != feld);
let eintrag = format!("{} wirft {} ab", self.spieler[nr].name, feld.name());
self.protokoll.push(eintrag);
}
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 {
self.phase == Phase::Legen && self.aktueller().hand.contains(&feld) && self.passt(feld)
}
// Passt dieses Plättchen gerade aufs Brett, egal wer es auf der Hand hat?
pub fn passt(&self, feld: Feld) -> bool {
if self.ist_tot(feld) {
return false;
}
// Eine neue Kette gründen geht nur, wenn noch eine frei ist
let gruendet = self.ketten_neben(feld).is_empty() && self.rohbau_neben(feld);
!(gruendet && self.freie_ketten().is_empty())
}
// Ein totes Plättchen würde zwei sichere Ketten verbinden. Es passt nie mehr.
pub fn ist_tot(&self, feld: Feld) -> bool {
let sichere = self
.ketten_neben(feld)
.iter()
.filter(|&&kette| self.groesse(kette) >= SICHER)
.count();
sichere >= 2
}
// 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 ketten.len() > 1 {
self.fusion(feld, ketten);
} else 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
}
// Das Plättchen verbindet mehrere Ketten: Die größte schluckt die anderen
fn fusion(&mut self, feld: Feld, mut ketten: Vec<Kette>) {
// Größte zuerst. Bei Gleichstand gewinnt die teurere Kette.
ketten.sort_by_key(|&kette| Reverse((self.groesse(kette), kette.stufe())));
let ueberlebende = ketten[0];
self.offene_fusionen = ketten[1..].to_vec();
self.naechste_fusion(feld, ueberlebende);
}
// Nimmt sich die nächste Kette vor, die geschluckt wird
fn naechste_fusion(&mut self, feld: Feld, ueberlebende: Kette) {
if self.offene_fusionen.is_empty() {
self.ausbreiten(feld, ueberlebende);
self.nach_dem_legen();
return;
}
let tot = self.offene_fusionen.remove(0);
let eintrag = format!(
"{}: {} schluckt {}",
self.spieler[self.am_zug].name,
ueberlebende.name(),
tot.name()
);
self.protokoll.push(eintrag);
self.praemien_zahlen(tot);
// Reihum, beim Spieler am Zug beginnend, entscheidet jeder, der Aktien hat
let anzahl = self.spieler.len();
let entscheider: Vec<usize> = (0..anzahl)
.map(|schritt| (self.am_zug + schritt) % anzahl)
.filter(|&nr| self.spieler[nr].aktien[tot.nr()] > 0)
.collect();
self.entscheider = entscheider;
if self.entscheider.is_empty() {
self.umfaerben(tot, ueberlebende);
self.naechste_fusion(feld, ueberlebende);
} else {
self.phase = Phase::Fusion {
feld,
tot,
ueberlebende,
};
}
}
// Ein Aktionär entscheidet, was mit seinen Aktien der geschluckten Kette passiert
fn entscheiden(&mut self, wahl: Entscheidung) {
let Phase::Fusion {
feld,
tot,
ueberlebende,
} = self.phase
else {
return;
};
let preis = self.preis(tot);
let anzahl = self.aktueller().aktien[tot.nr()];
match wahl {
Entscheidung::Verkaufen => {
let spieler = self.dran();
spieler.geld += anzahl * preis;
spieler.aktien[tot.nr()] = 0;
self.bank[tot.nr()] += anzahl;
self.notiere(format!("verkauft {anzahl} {}", tot.name()));
}
Entscheidung::Tauschen => {
// Zwei alte gegen eine neue, solange die Bank welche hat
let paare = (anzahl / 2).min(self.bank[ueberlebende.nr()]);
let spieler = self.dran();
spieler.aktien[tot.nr()] -= paare * 2;
spieler.aktien[ueberlebende.nr()] += paare;
self.bank[tot.nr()] += paare * 2;
self.bank[ueberlebende.nr()] -= paare;
self.notiere(format!("tauscht in {paare} {}", ueberlebende.name()));
}
Entscheidung::Behalten => {
self.notiere(format!("behält {anzahl} {}", tot.name()));
}
}
self.entscheider.remove(0);
if self.entscheider.is_empty() {
self.umfaerben(tot, ueberlebende);
self.naechste_fusion(feld, ueberlebende);
}
}
// Die beiden größten Aktionäre der geschluckten Kette bekommen eine Prämie
fn praemien_zahlen(&mut self, kette: Kette) {
let preis = self.preis(kette);
let bestand: Vec<u32> = self.spieler.iter().map(|s| s.aktien[kette.nr()]).collect();
let meiste = bestand.iter().copied().max().unwrap_or(0);
if meiste == 0 {
return;
}
let zweitmeiste = bestand
.iter()
.copied()
.filter(|&b| b < meiste)
.max()
.unwrap_or(0);
let erste: Vec<usize> = (0..bestand.len())
.filter(|&nr| bestand[nr] == meiste)
.collect();
let zweite: Vec<usize> = (0..bestand.len())
.filter(|&nr| zweitmeiste > 0 && bestand[nr] == zweitmeiste)
.collect();
let gross = preis * 10;
let klein = preis * 5;
if erste.len() > 1 || zweite.is_empty() {
// Gleichstand an der Spitze oder nur einer hält Aktien: Alles wird geteilt
self.auszahlen(&erste, gross + klein);
} else {
self.auszahlen(&erste, gross);
self.auszahlen(&zweite, klein);
}
}
// Teilt eine Summe gerecht auf, aufgerundet auf volle Hundert
fn auszahlen(&mut self, empfaenger: &[usize], summe: u32) {
let anteil = (summe / empfaenger.len() as u32).div_ceil(100) * 100;
for &nr in empfaenger {
self.spieler[nr].geld += anteil;
let eintrag = format!("{} erhält {anteil} € Prämie", self.spieler[nr].name);
self.protokoll.push(eintrag);
}
}
// Alle Hotels einer Kette gehören ab jetzt einer anderen
fn umfaerben(&mut self, alt: Kette, neu: Kette) {
for belegung in self.brett.iter_mut() {
if *belegung == Belegung::Hotel(alt) {
*belegung = Belegung::Hotel(neu);
}
}
}
// 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);
}
if self.darf_beenden() {
zuege.push(Zug::SpielBeenden);
}
}
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);
}
Phase::Fusion { .. } => {
zuege.push(Zug::Aktien(Entscheidung::Verkaufen));
zuege.push(Zug::Aktien(Entscheidung::Tauschen));
zuege.push(Zug::Aktien(Entscheidung::Behalten));
}
Phase::Ende => {}
}
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::Aktien(wahl) => self.entscheiden(wahl),
Zug::ZugBeenden => self.zug_beenden(),
Zug::SpielBeenden => {
if self.darf_beenden() {
self.notiere(String::from("beendet das Spiel"));
self.schluss();
}
}
}
}
// 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;
// Stapel leer und nichts passt mehr: Dann ist von selbst Schluss
if self.stapel.is_empty() && self.niemand_kann_legen() {
self.schluss();
}
}
// Kein Spieler hat noch ein Plättchen, das aufs Brett passt
fn niemand_kann_legen(&self) -> bool {
self.spieler
.iter()
.all(|spieler| spieler.hand.iter().all(|&feld| !self.passt(feld)))
}
// Beenden darf man, wenn eine Kette riesig ist oder alle Ketten sicher sind
pub fn darf_beenden(&self) -> bool {
let aktive = self.aktive_ketten();
let riesig = aktive.iter().any(|&kette| self.groesse(kette) >= RIESIG);
let alle_sicher = aktive.iter().all(|&kette| self.groesse(kette) >= SICHER);
self.phase == Phase::Legen && !aktive.is_empty() && (riesig || alle_sicher)
}
// Alle Spieler, der reichste zuerst
pub fn rangliste(&self) -> Vec<&Spieler> {
let mut liste: Vec<&Spieler> = self.spieler.iter().collect();
liste.sort_by_key(|spieler| Reverse(spieler.geld));
liste
}
// Schluss: letzte Prämien, dann kauft die Bank alle Aktien zum Kurs zurück
fn schluss(&mut self) {
for kette in self.aktive_ketten() {
self.praemien_zahlen(kette);
let preis = self.preis(kette);
for spieler in self.spieler.iter_mut() {
spieler.geld += spieler.aktien[kette.nr()] * preis;
spieler.aktien[kette.nr()] = 0;
}
}
self.protokoll.push(String::from("Das Spiel ist aus!"));
self.phase = Phase::Ende;
}
// 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)
}
ist_tot, passt, darf_legen. Mit &&
verknüpft, läuft die teure Prüfung nur, wenn die billigen bestanden sind. Eine Liste, die man
gerade durchgeht, darf man nicht gleichzeitig verändern, also sammelt man erst und ändert dann.
all auf einer leeren Liste ist wahr, darauf muss man achten. Das Spiel endet durch einen
Zug oder von selbst, die Schlussabrechnung zahlt Prämien und kauft Aktien zurück, und
*self = … setzt den ganzen Zustand auf einmal neu.Zum Ausprobieren
- Kurzes Spiel: Man soll schon ab 25 Feldern beenden dürfen.
- Wer hat wie viel? Die Rangliste soll hinter dem Geld auch zeigen, wie viel davon Prämien waren. Welche Information fehlt dem Spiel dafür, und wo würdest du sie speichern?
- Ein Test, der ein ganzes Spiel spielt: Schreib einen Test, in dem vier Spieler
so lange zufällige erlaubte Züge machen, bis die Phase
Endeerreicht ist. Nach höchstens 20.000 Zügen muss Schluss sein. Lass ihn hundertmal laufen.
Lösungen anzeigen
1. pub const RIESIG: usize = 25;
2. Das Spiel weiß nach dem Auszahlen nicht mehr, woher Geld
kam. Eine Möglichkeit: Ein neues Feld pub praemien: u32 in Spieler,
das in auszahlen mitgezählt wird, genau wie geld. In der Rangliste
kann man es dann mit anzeigen.
3. Am Ende von spiel.rs:
#[cfg(test)]
mod tests {
use super::*;
use rand::seq::IndexedRandom;
#[test]
fn jedes_spiel_geht_zu_ende() {
for _ in 0..100 {
let mut spiel = Spiel::neu(&[
("A", Art::Tourist),
("B", Art::Tourist),
("C", Art::Tourist),
("D", Art::Tourist),
]);
let mut zuege = 0;
while spiel.phase != Phase::Ende {
let moeglich = spiel.moegliche_zuege();
let zug = *moeglich.choose(&mut rand::rng()).unwrap();
spiel.ausfuehren(zug);
zuege += 1;
assert!(zuege < 20_000, "Das Spiel endet nicht");
}
}
}
}
So ein Test, der viele zufällige Partien durchspielt, findet Fehler, an die man beim Planen nie gedacht hätte. Weil die Regeln nichts von Fenstern wissen, laufen hundert Partien in weniger als einer Sekunde.
Etappe 7 geschafft 🏆
Der Baulöwe ist jetzt ein vollständiges Spiel, von der ersten Gründung bis zur Rangliste. Nur die Gegner sind noch ziemlich einfältig. In der nächsten Etappe bekommt der Tourist Konkurrenz: Der Makler denkt nach, kämpft um Mehrheiten und weiß, wann sich eine Fusion lohnt.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren