Aktien und Geld
Ein Hotel ist schön, eine Beteiligung daran ist besser. Ab heute hat jede Kette einen Aktienkurs, der mit ihrer Größe steigt, und du startest mit 6.000 € in der Kasse. Nach jedem Plättchen darfst du bis zu drei Aktien kaufen, und wer eine Kette gründet, bekommt eine geschenkt. Dafür lernt das Spiel, einen Zug in Phasen zu denken.
Ein Zug in Phasen
Bisher ist ein Zug ein einziger Klick. Im echten Spiel hat ein Zug aber mehrere Abschnitte:
erst ein Plättchen legen, dann vielleicht eine Kette gründen, dann Aktien kaufen und zum
Schluss nachziehen. In Etappe 3 haben wir die Gründung mit einem Option gelöst.
Jetzt kommen weitere Abschnitte dazu, und damit wird es Zeit für ein richtiges
Phasen-Modell: ein enum, das sagt, woran das Spiel gerade ist.
Es kommt in spiel.rs vor die Struktur Spiel:
// 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 die Spielregeln wissen müssen
Kaufen { gekauft: u32 } ist ein Fall mit einem benannten Feld: Er merkt sich, wie
viele Aktien in diesem Zug schon gekauft wurden. Das alte gruendung wird durch die
Phase ersetzt:
pub hand: Vec<Feld>,// Hier wartet eine neue Kette darauf, dass man ihr einen Namen gibt pub gruendung: Option<Feld>,pub phase: Phase,}⋮hand: Vec::new(),gruendung: None,phase: Phase::Legen,
Ein Programm, das so von Phase zu Phase springt, nennt man einen
Zustandsautomaten. Die Regel für jede Methode lautet: Erst nachsehen, ob sie
in der aktuellen Phase überhaupt erlaubt ist. legen geht nur beim Legen:
pub fn legen(&mut self, feld: Feld) -> bool {if self.gruendung.is_some() {if self.phase != Phase::Legen {return false; }
Am Ende von legen ziehen wir nicht mehr sofort nach. Stattdessen geht es in die
nächste Phase:
if let Some(&kette) = ketten.first() { self.ausbreiten(feld, kette);self.nach_dem_legen();} else if rohbau_daneben {self.gruendung = Some(feld); } self.nachziehen();self.phase = Phase::Gruenden(feld); } else { self.nach_dem_legen(); }true }
gruenden holt sich das wartende Feld jetzt aus der Phase. Die Zeile
let Phase::Gruenden(feld) = self.phase else { return; }; ist wieder ein Türsteher:
Nur wenn die Phase wirklich eine Gründung ist, geht es weiter, und dann steht das Feld gleich in
feld. Darunter kommen drei neue Methoden:
pub fn gruenden(&mut self, kette: Kette) { if !self.freie_ketten().contains(&kette) { return; } if let Some(feld) = self.gruendung.take() { self.ausbreiten(feld, 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); self.nach_dem_legen(); } // Beendet die Kaufphase und damit den Zug pub fn zug_beenden(&mut self) { if let Phase::Kaufen { .. } = self.phase { self.naechster_zug(); } } // 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 Zug beginnt fn naechster_zug(&mut self) { self.nachziehen(); self.phase = Phase::Legen; }// Wie viele Felder gehören zu dieser Kette?
zug_beendenist öffentlich, denn den Knopf dafür drückst du. Es wirkt aber nur in der Kaufphase.Phase::Kaufen { .. }heißt: Kaufphase, egal mit welcher Zahl darin.nach_dem_legenentscheidet, wie es weitergeht: Gibt es noch gar keine Kette, gibt es auch nichts zu kaufen, und der Zug endet sofort.naechster_zugzieht nach und stellt die Phase zurück auf Legen. Ab Etappe 5 ist hier auch der nächste Spieler dran.
Fehlt noch aktive_ketten, das Gegenstück zu freie_ketten:
// 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
In main.rs gibt es einen neuen Brief, und der Gründungsdialog fragt jetzt die
Phase:
use spiel::{Belegung, Feld, Kette, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, Kette, Phase, REIHEN, SPALTEN, Spiel};⋮Gruenden(Kette),ZugBeenden,}⋮Message::Gruenden(kette) => self.spiel.gruenden(kette),Message::ZugBeenden => self.spiel.zug_beenden(),}
let mut ebenen = stack![brett, folie];if self.spiel.gruendung.is_some() {if let Phase::Gruenden(_) = self.spiel.phase {
Die Plättchen in der Fußleiste sollen nur klickbar sein, solange gelegt werden darf.
on_press_maybe nimmt ein Option: Bei Some(brief) ist der
Knopf aktiv, bei None ausgegraut. darf_legen.then_some(…) macht aus
einem Wahrheitswert genau so ein Option. Für den ausgegrauten Zustand bekommt der
Stil einen eigenen Fall:
// Unten: die Plättchen auf deiner Handlet darf_legen = self.spiel.phase == Phase::Legen;⋮.padding([8, 14]).on_press(Message::Geklickt(feld)).on_press_maybe(darf_legen.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,
Und rechts unten erscheint in der Kaufphase der Knopf „Zug beenden“:
.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); if let Phase::Kaufen { .. } = self.spiel.phase { let fertig = button(text("Zug beenden").size(14)) .padding([8, 16]) .on_press(Message::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(fertig); }let fussleiste = row![hand, space::horizontal(), stapel]let fussleiste = row![hand, space::horizontal(), rechts]
cargo run
Solange noch keine Kette steht, spielt sich alles wie bisher. Sobald die erste Kette gegründet ist, werden die Plättchen nach jedem Zug blass, und rechts unten erscheint „Zug beenden“. Erst nach dem Klick ziehst du nach und darfst wieder legen.Aktienkurse
Jede Kette hat einen Aktienkurs, und der hängt von zwei Dingen ab. Erstens von der
Größe: Je mehr Felder eine Kette hat, desto wertvoller ist sie. Zweitens von
der Preisstufe: Zugvogel und Ankerplatz sind günstige Häuser, Aurora und
Königshof die Luxusklasse, der Rest liegt dazwischen. Die Stufe kommt in
impl Kette:
// 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 {
Der senkrechte Strich | in einem match-Zweig heißt „oder“. Und nun
die Kurstabelle, als Methode von Spiel, weil sie die Größe auf dem Brett
braucht:
// 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
Hier stecken gleich drei match-Kniffe drin:
1..=5ist ein Bereich: alle Zahlen von 1 bis einschließlich 5.groesse @ 1..=5prüft den Bereich und gibt dem Wert gleichzeitig einen Namen, damit wir damit rechnen können: 2 Felder kosten 200 €, 3 Felder 300 €, und so weiter.0 => return 0verlässt die Funktion mitten immatch: Eine Kette, die es nicht gibt, hat keinen Kurs. Der Unterstrich_ganz unten fängt alles ab, was vorher nicht gepasst hat, also 41 Felder und mehr.
Zum Anzeigen brauchen wir Geldbeträge in deutscher Schreibweise mit Tausenderpunkt. Rust kann
das nicht von Haus aus, also schreiben wir es selbst, vor mischen:
// 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
enumerate liefert zu jeder Ziffer ihre Position. Vor einer Ziffer kommt ein Punkt,
wenn danach noch eine durch drei teilbare Anzahl Ziffern folgt. Bei „6000“ ist das vor der
zweiten Ziffer der Fall: Danach kommen noch genau drei.
In der Kettentafel zeigen wir den Kurs als neue Spalte. Weil jetzt zwei Angaben von der Frage
abhängen, ob es die Kette gibt, liefert das if gleich ein Paar
zurück:
let angabe = if groesse == 0 { String::from("frei") } else { format!("{groesse} Felder") }; let zeile = row![ punkt, text(kette.name()).size(13).color(farben.schrift).width(90), text(angabe).size(13).color(farben.schrift),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),]
cargo run
In der Kettentafel steht jetzt neben jeder Kette ihr Kurs. Lass eine Kette wachsen und schau zu, wie der Kurs mit ihr steigt. Eine Königshof-Aktie ist immer 200 € teurer als eine Zugvogel-Aktie gleicher Größe.Die Kasse
Jetzt darfst du Aktien kaufen. Jeder Spieler startet mit 6.000 € in der Kasse, und von jeder
Kette gibt es 25 Aktien. Pro Zug darfst du höchstens drei kaufen. Diese Zahlen kommen als
Konstanten nach oben in spiel.rs:
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;
Was einem Spieler gehört, fassen wir in einer Struktur Spieler zusammen. Die Aktien
sind ein Array mit sieben Plätzen, einem pro Kette. Damit wir den richtigen Platz finden, bekommt
Kette eine Nummer. self as usize verrät die Position eines Falls im
enum: Zugvogel ist 0, Königshof ist 6.
pub const ALLE: [Kette; 7] = [⋮];// Die Nummer der Kette: 0 bis 6 pub fn nr(self) -> usize { self as usize }// Preisstufe: 0 günstig, 1 mittel, 2 teuer
// Was einem Spieler gehört pub struct Spieler { pub geld: u32, pub aktien: [u32; 7], }// Alles, was die Spielregeln wissen müssen⋮pub phase: Phase,pub spieler: Spieler, // Die Aktien, die noch niemandem gehören pub bank: [u32; 7],}
phase: Phase::Legen,spieler: Spieler { geld: STARTGELD, aktien: [0; 7], }, bank: [AKTIEN_JE_KETTE; 7],};
[0; 7] ist ein Array aus sieben Nullen, [AKTIEN_JE_KETTE; 7] eins
aus siebenmal 25. Wer eine Kette gründet, bekommt eine Aktie geschenkt, sofern die Bank noch eine
hat. Das kommt in gruenden:
self.ausbreiten(feld, kette);// Wer gründet, bekommt eine Aktie geschenkt if self.bank[kette.nr()] > 0 { self.bank[kette.nr()] -= 1; self.spieler.aktien[kette.nr()] += 1; }self.nach_dem_legen(); }
Und nun das Kaufen, in zwei Methoden direkt unter gruenden. Die erste prüft nur,
die zweite handelt. Diese Trennung hat einen Vorteil: Die Oberfläche kann mit
darf_kaufen vorab fragen, ob ein Knopf aktiv sein soll, und benutzt dafür genau
dieselbe Regel wie der Kauf selbst.
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.spieler.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; }; self.spieler.geld -= self.preis(kette); self.spieler.aktien[kette.nr()] += 1; self.bank[kette.nr()] -= 1; if gekauft + 1 == KAEUFE_JE_ZUG { self.naechster_zug(); } else { self.phase = Phase::Kaufen { gekauft: gekauft + 1, }; } }// Beendet die Kaufphase und damit den Zug
In darf_kaufen werden vier Bedingungen mit && verknüpft,
alle müssen stimmen: noch Käufe frei, die Kette existiert, die Bank hat noch Aktien, und das Geld
reicht. Nach dem dritten Kauf endet der Zug von selbst.
In main.rs machen wir zuerst das Fenster größer, denn die Tafel bekommt drei neue
Spalten und braucht Platz. Dann kommt der Brief fürs Kaufen:
use spiel::{Belegung, Feld, Kette, Phase, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel};⋮.window_size((980.0, 680.0)).window_size((1280.0, 760.0))⋮ZugBeenden,Kaufen(Kette),}⋮Message::ZugBeenden => self.spiel.zug_beenden(),Message::Kaufen(kette) => self.spiel.kaufen(kette),}
Die Kasse zeigen wir groß in der Kopfzeile:
let status = text(hinweis).size(15).color(farben.schrift);let kasse = text(format!("Kasse: {}", geld(self.spiel.spieler.geld))) .size(17) .font(Font { weight: font::Weight::Bold, ..Font::DEFAULT }) .color(farben.schrift);⋮let kopfzeile = row![titel, space::horizontal(), status, schalter]let kopfzeile = row![titel, space::horizontal(), status, kasse, schalter]
Die Kettentafel bekommt eine Überschriftenzeile und drei neue Spalten: wie viele Aktien du
hast, wie viele die Bank noch hat, und einen Kaufknopf. Für die Überschriften bauen wir uns eine
kleine Closure klein, weil wir sechsmal dasselbe brauchen, nur mit anderem Text und
anderer Breite:
.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 {⋮text(kurs).size(13).color(farben.schrift).width(70),text(self.spiel.spieler.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.spiel .darf_kaufen(kette) .then_some(Message::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() } }),]
Der Kaufknopf ist nur aktiv, wenn darf_kaufen ja sagt. Sonst verblasst er auf ein
Viertel seiner Deckkraft. Zum Schluss sagt die Fußleiste in der Kaufphase, wie viele Käufe noch
übrig sind:
if let Phase::Kaufen { .. } = self.spiel.phase {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 fertig = button(text("Zug beenden").size(14))⋮rechts = rechts.push(fertig);rechts = rechts.push(rest).push(fertig);}
cargo run
Das Fenster ist größer, oben rechts steht deine Kasse mit 6.000 €. Gründe eine Kette: In der Spalte „Du“ steht sofort eine 1, deine Gründeraktie. In der Kaufphase leuchten die Plus-Knöpfe. Kauf drei Aktien, und der Zug endet von selbst. Wird das Geld knapp, bleiben teure Aktien grau.Der komplette Code nach Etappe 4
Hier ist der vollständige Stand nach Etappe 4. 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 anzeigen
Cargo.toml
[package]
name = "bauloewe"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = { version = "0.14", features = ["canvas"] }
rand = "0.9"
src/main.rs
mod spiel;
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,
Theme, Vector, alignment, font,
};
use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel};
// 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)
.run()
}
struct Bauloewe {
spiel: Spiel,
maus_feld: Option<Feld>,
nacht: bool,
}
#[derive(Debug, Clone)]
enum Message {
MausUeber(Option<Feld>),
Geklickt(Feld),
TagNacht,
Gruenden(Kette),
ZugBeenden,
Kaufen(Kette),
}
impl Bauloewe {
fn neu() -> Bauloewe {
Bauloewe {
spiel: Spiel::neu(),
maus_feld: None,
nacht: false,
}
}
fn update(&mut self, message: Message) {
match message {
Message::MausUeber(feld) => self.maus_feld = feld,
Message::Geklickt(feld) => {
self.spiel.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),
}
}
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 {
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.spiel.spieler.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 darf_legen = self.spiel.phase == Phase::Legen;
let mut hand = row![text("Deine Plättchen:").size(15).color(farben.schrift)]
.spacing(10)
.align_y(alignment::Vertical::Center);
for &feld in &self.spiel.hand {
let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE))
.padding([8, 14])
.on_press_maybe(darf_legen.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);
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 fertig = button(text("Zug beenden").size(14))
.padding([8, 16])
.on_press(Message::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.spiel.spieler.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.spiel
.darf_kaufen(kette)
.then_some(Message::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 = 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 folie = column![kopfzeile, tafelzeile, space::vertical(), fussleiste];
// Wartet eine Gründung, kommt das Auswahlfenster obendrauf
let mut ebenen = stack![brett, folie];
if let Phase::Gruenden(_) = self.spiel.phase {
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::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.spiel.hand.contains(&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.spiel.hand.contains(&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 },
}
// Was einem Spieler gehört
pub struct Spieler {
pub geld: u32,
pub aktien: [u32; 7],
}
// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
pub brett: Vec<Belegung>,
pub stapel: Vec<Feld>,
pub hand: Vec<Feld>,
pub phase: Phase,
pub spieler: Spieler,
// Die Aktien, die noch niemandem gehören
pub bank: [u32; 7],
}
impl Spiel {
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
}
// Was steht auf diesem Feld?
pub fn belegung(&self, feld: Feld) -> Belegung {
self.brett[feld.index()]
}
// 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));
}
// 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.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.hand.remove(stelle);
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.spieler.aktien[kette.nr()] += 1;
}
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.spieler.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;
};
self.spieler.geld -= self.preis(kette);
self.spieler.aktien[kette.nr()] += 1;
self.bank[kette.nr()] -= 1;
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) {
if let Phase::Kaufen { .. } = self.phase {
self.naechster_zug();
}
}
// 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 Zug beginnt
fn naechster_zug(&mut self) {
self.nachziehen();
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()
}
// 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);
}
}
}
}
}
let … else und if let holt man Daten sicher aus einem
enum. In match gibt es Bereiche wie 1..=5, Namen mit
@ und Alternativen mit |. Prüfen (darf_kaufen) und
Handeln (kaufen) sind getrennt, damit Oberfläche und Regeln immer dieselbe Antwort
geben. Und on_press_maybe schaltet Knöpfe passend an und aus.Zum Ausprobieren
- Reich geboren: Starte mit 10.000 €. Was musst du ändern?
- Depotwert: Zeig in der Kopfzeile neben der Kasse auch, was deine Aktien
gerade zusammen wert sind. Tipp: Schreib dafür in
spiel.rseine Methodedepotwert, die für jede Kette Anzahl mal Kurs rechnet. - Grenzfall: Was passiert, wenn du in der Kaufphase eine Kette kaufen willst, von der die Bank keine Aktien mehr hat? Wo genau im Code wird das verhindert?
Lösungen anzeigen
1. Eine Zeile in spiel.rs:
pub const STARTGELD: u32 = 10000;. Zahlen dürfen in Rust übrigens Unterstriche
haben, 10_000 ist dasselbe und liest sich leichter.
2. In impl Spiel:
pub fn depotwert(&self) -> u32 {
Kette::ALLE
.into_iter()
.map(|kette| self.spieler.aktien[kette.nr()] * self.preis(kette))
.sum()
}
map verwandelt jede Kette in einen Betrag, sum zählt alles
zusammen. In view kannst du den Text der Kasse dann zum Beispiel so bauen:
format!("Kasse: {} · Aktien: {}", geld(…), geld(self.spiel.depotwert())).
3. Nichts, und das ist Absicht. In
darf_kaufen steht self.bank[kette.nr()] > 0. Ist die Bank leer,
ist die Bedingung falsch, der Knopf bleibt grau, und selbst wenn jemand
kaufen direkt aufriefe, würde es sofort mit return aussteigen.
Etappe 4 geschafft 💶
Es gibt Kurse, eine Kasse und eine Bank. Nur spielst du noch allein, und das ist auf Dauer langweilig. In der nächsten Etappe setzen sich Mitspieler an den Tisch: drei Computergegner, die reihum legen und kaufen. Den Anfang macht der Tourist, der nicht viel nachdenkt.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren