Feinschliff und Leuchtreklamen
Die letzte Etappe macht aus einem guten Spiel ein schönes. Vor jeder Partie wählst du deine Gegner, neue Häuser wachsen sichtbar aus dem Rasen, und nachts schweben über den Ketten ihre Namen als Leuchtreklamen. Danach ist der Baulöwe fertig.
Wer spielt mit?
Bisher stehen deine Gegner fest im Code. Zum Abschluss darfst du sie dir vor jeder Partie aussuchen: Ein Klick auf einen Gegner wechselt seine Spielstärke von Tourist über Makler zu Hai und wieder zurück. Das Spiel merkt sich dafür die drei Arten und ob es schon losgegangen ist:
denkt: bool,// Wie stark die drei Gegner spielen gegner: [Art; 3], // Läuft das Spiel schon, oder wählst du noch die Gegner? gestartet: bool,}⋮NeuesSpiel,GegnerWechseln(usize), Starten,}
Das Spiel entsteht jetzt an zwei Stellen, beim Programmstart und bei jeder neuen Runde. Deshalb
bekommt es eine eigene Funktion neues_spiel, die aus den drei gewählten Arten einen
Tisch baut:
fn neu() -> Bauloewe { Bauloewe { spiel: Spiel::neu(&[ ("Leo", Art::Mensch), ("Toni", Art::Tourist), ("Mia", Art::Makler), ("Hanna", Art::Hai), ]), maus_feld: None, nacht: false, denkt: false, } }fn neu() -> Bauloewe { let gegner = [Art::Tourist, Art::Makler, Art::Hai]; Bauloewe { spiel: Bauloewe::neues_spiel(gegner), maus_feld: None, nacht: false, denkt: false, gegner, gestartet: false, } } // Die Namen an den drei Plätzen bleiben gleich, die Spielstärke wählst du fn neues_spiel(gegner: [Art; 3]) -> Spiel { Spiel::neu(&[ ("Leo", Art::Mensch), ("Toni", gegner[0]), ("Mia", gegner[1]), ("Hanna", gegner[2]), ]) }// Du bist immer Spieler Nummer 0
Solange noch nicht gestartet ist, darf niemand ziehen. Das erreichen wir an zwei Stellen: Du bist erst am Zug, wenn es losgegangen ist, und der Takt für den Computer bleibt stumm:
self.spiel.aktueller().art == Art::Menschself.gestartet && self.spiel.aktueller().art == Art::Mensch⋮if self.mensch_am_zug() || self.spiel.phase == Phase::Ende {if !self.gestartet || self.mensch_am_zug() || self.spiel.phase == Phase::Ende {
In update kommen zwei Briefe dazu, und „Neues Spiel“ führt ab jetzt zurück zur
Gegnerwahl. Deine Auswahl und der Tag- oder Nachtmodus bleiben dabei erhalten, weil wir nur das
Spiel ersetzen und nicht mehr den ganzen Zustand:
Message::NeuesSpiel => { let nacht = self.nacht; *self = Bauloewe::neu(); self.nacht = nacht; }// Zurück zur Gegnerwahl, mit frisch gemischtem Spiel Message::NeuesSpiel => { self.spiel = Bauloewe::neues_spiel(self.gegner); self.gestartet = false; self.denkt = false; } // Tourist, Makler, Hai und wieder von vorn Message::GegnerWechseln(platz) => { self.gegner[platz] = match self.gegner[platz] { Art::Tourist => Art::Makler, Art::Makler => Art::Hai, _ => Art::Tourist, }; self.spiel = Bauloewe::neues_spiel(self.gegner); } Message::Starten => self.gestartet = true,// Der Takt für den Computer
Beim Wechseln der Gegner bauen wir den Tisch jedes Mal neu, damit die Tafel „Am Tisch“ sofort die
neue Auswahl zeigt. Das match mit _ im letzten Zweig fängt Hai und
Mensch ab. Einen Menschen kann man hier ohnehin nicht auswählen.
Für die Auswahl brauchen wir Knöpfe, die einen beliebigen Brief verschicken, nicht nur einen
Spielzug. Wir ziehen deshalb den Körper von farbknopf in eine allgemeinere Funktion
nachrichtenknopf um. farbknopf ruft sie nur noch auf:
fn farbknopf<'a>(beschriftung: &str, farbe: Color, zug: Zug) -> Element<'a, Message> {nachrichtenknopf(beschriftung, farbe, Message::Spielzug(zug)) } // Ein farbiger Knopf, der einen beliebigen Brief verschickt fn nachrichtenknopf<'a>( beschriftung: &str, farbe: Color, nachricht: Message, ) -> Element<'a, Message> {button(text(beschriftung.to_string()).size(14)) .padding([8, 12]).on_press(Message::Spielzug(zug)).on_press(nachricht)
Zwei kleine Helfer übersetzen eine Spielerart in einen Namen und eine Farbe. Sie kommen vor
dialogfenster:
// Wie die Spielstärken heißen und welche Farbe ihr Knopf bekommt fn art_name(art: Art) -> &'static str { match art { Art::Mensch => "Mensch", Art::Tourist => "Tourist", Art::Makler => "Makler", Art::Hai => "Hai", } } fn art_farbe(art: Art) -> Color { match art { Art::Mensch => kettenfarbe(Kette::Koenigshof), Art::Tourist => kettenfarbe(Kette::Ankerplatz), Art::Makler => kettenfarbe(Kette::Uferblick), Art::Hai => kettenfarbe(Kette::Bergfried), } }// Eine Karte mit Titel, Frage und Knöpfen über einem dunklen Schleier
Und nun der Startdialog selbst, mit dem bewährten dialogfenster:
// Vor dem Spiel: Gegen wen spielst du? fn startdialog(&self, farben: Farben) -> Element<'_, Message> { let mut knoepfe = row![].spacing(8); for (platz, spieler) in self.spiel.spieler.iter().skip(1).enumerate() { let art = self.gegner[platz]; let beschriftung = format!("{} · {}", spieler.name, art_name(art)); let nachricht = Message::GegnerWechseln(platz); knoepfe = knoepfe.push(nachrichtenknopf(&beschriftung, art_farbe(art), nachricht)); } let los = nachrichtenknopf("Los geht's!", farben.akzent, Message::Starten); knoepfe = knoepfe.push(los); let frage = "Klick auf einen Gegner, um seine Spielstärke zu wechseln."; dialogfenster(farben, "Wer spielt mit?", frage.to_string(), knoepfe) }// Die Abrechnung: Wer ist der größte Baulöwe?
skip(1) überspringt dich, denn du bist Spieler 0. enumerate zählt danach
wieder ab null, und das passt genau zu den drei Plätzen in gegner. Zum Schluss legen wir
den Dialog als oberste Ebene über alles, und die Kopfzeile bekommt einen passenden Hinweis:
let hinweis = match self.maus_feld {_ if !self.gestartet => String::from("Wähle deine Gegner"),_ if self.spiel.phase == Phase::Ende => String::from("Schlussabrechnung"),⋮ebenen = ebenen.push(self.schlussdialog(farben)); }if !self.gestartet { ebenen = ebenen.push(self.startdialog(farben)); }ebenen.into()
cargo run
Beim Start fragt dich eine Karte „Wer spielt mit?“. Klick ein paarmal auf die Gegner und beobachte, wie sich Knopf und Tafel „Am Tisch“ ändern. Mit „Los geht’s!“ beginnt die Partie. Nach der Schlussabrechnung führt „Neues Spiel“ wieder hierher.
Häuser, die wachsen
Bisher springen Häuser einfach ins Bild. Viel schöner ist es, wenn man zusehen kann, wie sie in die Höhe wachsen: ein neuer Rohbau aus dem Rasen, ein Hotelturm, der nach einer Fusion Stockwerk für Stockwerk nach oben schießt. Dafür merken wir uns für jedes Feld, wie hoch das Haus gerade gezeichnet wird, und lassen diese Zahl bei jedem Bild ein Stück auf die richtige Höhe zuwachsen. Kommazahlen erlauben dabei halbe Stockwerke:
gestartet: bool,// Wie hoch jedes Haus gerade gezeichnet wird, in Stockwerken anzeige: Vec<f32>,}⋮gestartet: false,anzeige: vec![0.0; SPALTEN * REIHEN],}⋮Starten,Bild,}
Wie hoch ein Hotel werden soll, steht bisher mitten in hotel_bauplan. Wir brauchen die
Rechnung jetzt auch an anderer Stelle, also bekommt sie eine eigene Funktion:
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,stockwerke: hotel_stockwerke(groesse, feld),⋮// Wie viele Stockwerke ein Hotel einer Kette dieser Größe hat fn hotel_stockwerke(groesse: usize, feld: Feld) -> usize { let grundhoehe = 3.0 + (groesse as f32).sqrt() * 1.5; let schwankung = zufall(feld.index(), 7) % 2; grundhoehe as usize + schwankung as usize }// Jede Kette hat ihre eigene Dachform
Das Wachsen treibt ein neues Abo an. window::frames() schickt zu jedem Bild, das der
Bildschirm zeichnet, ein Signal, meist 60-mal pro Sekunde. Das wollen wir aber nur, solange
irgendwo gebaut wird, sonst würde das Programm ständig neu zeichnen und unnötig Strom verbrauchen.
Zwei Abos auf einmal verbindet Subscription::batch:
fn abo(&self) -> Subscription<Message> { if !self.gestartet || self.mensch_am_zug() || self.spiel.phase == Phase::Ende { Subscription::none() } else { time::every(Duration::from_millis(600)).map(|_| Message::KiTakt) } }fn abo(&self) -> Subscription<Message> { let takt = if !self.gestartet || self.mensch_am_zug() || self.spiel.phase == Phase::Ende { Subscription::none() } else { time::every(Duration::from_millis(600)).map(|_| Message::KiTakt) }; // Solange irgendwo gebaut wird, kommt zu jedem Bild ein Brief let bauen = if self.baustelle() { window::frames().map(|_| Message::Bild) } else { Subscription::none() }; Subscription::batch([takt, bauen]) } // Wie hoch soll das Haus auf diesem Feld am Ende sein? fn ziel_hoehe(&self, feld: Feld) -> f32 { match self.spiel.belegung(feld) { Belegung::Frei => 0.0, Belegung::Rohbau => 3.0, Belegung::Hotel(kette) => hotel_stockwerke(self.spiel.groesse(kette), feld) as f32, } } // Wächst irgendwo noch ein Haus? fn baustelle(&self) -> bool { (0..self.anzeige.len()).any(|nr| { let feld = Feld { spalte: nr % SPALTEN, reihe: nr / SPALTEN, }; self.anzeige[nr] < self.ziel_hoehe(feld) }) }fn update(&mut self, message: Message) -> Task<Message> {
In baustelle steckt ein kleiner Rechentrick: Aus der Nummer eines Platzes im Brett
wird wieder ein Feld. Der Rest beim Teilen durch 12 ist die Spalte, das Ergebnis der Teilung die
Reihe. Das ist genau die Umkehrung von Feld::index. In update wächst bei
jedem Bild jedes Haus um 0,12 Stockwerke, aber nie über sein Ziel hinaus:
Message::Starten => self.gestartet = true,// Jedes Haus wächst ein Stück auf seine Zielhöhe zu Message::Bild => { for nr in 0..self.anzeige.len() { let feld = Feld { spalte: nr % SPALTEN, reihe: nr / SPALTEN, }; let ziel = self.ziel_hoehe(feld); self.anzeige[nr] = (self.anzeige[nr] + 0.12).min(ziel); } } // Der Takt für den Computer Message::KiTakt => { if !self.mensch_am_zug() && !self.denkt { self.denkt = true; let kopie = self.spiel.clone(); let denken = async move { ki::waehle_zug(&kopie) }; return Task::perform(denken, Message::KiZug); } } // Die Antwort aus dem Hintergrund ist da Message::KiZug(zug) => { self.denkt = false; if !self.mensch_am_zug() && self.spiel.moegliche_zuege().contains(&zug) { self.spiel.ausfuehren(zug); } }}⋮self.denkt = false;self.anzeige = vec![0.0; SPALTEN * REIHEN];}
Der letzte Teil steht bei NeuesSpiel: Ein neues Spiel beginnt mit leerem Brett, also
auch mit null Stockwerken überall. Beim Zeichnen nimmt zeichne_haus jetzt die kleinere
der beiden Höhen, die aus dem Bauplan und die gerade angezeigte. Fenster gibt es nur für fertige
Stockwerke, der Rest ist sozusagen noch Baustelle:
let r = feld.reihe as f32;let stockwerke = plan.stockwerke;// Neue Häuser wachsen erst noch in die Höhe let hoehe = (plan.stockwerke as f32).min(self.anzeige[feld.index()]); let stockwerke = hoehe as usize;⋮z1: boden + stockwerke as f32 * STOCKWERK,z1: boden + hoehe * STOCKWERK,⋮Subscription, Task, Theme, Vector, alignment, font, time,Subscription, Task, Theme, Vector, alignment, font, time, window,
cargo run
Leg ein Plättchen: Der Rohbau wächst aus dem Rasen. Gründe eine Kette, und alle ihre Häuser recken sich in die Höhe. Am schönsten ist eine große Fusion, bei der eine ganze Skyline gleichzeitig nach oben schießt.Leuchtreklamen
Zum Schluss ein bisschen Großstadt: Nachts schwebt über jeder Kette ihr Name in einer
leuchtenden Tafel, wie eine Neonreklame auf dem Dach. Sie sitzt über dem vordersten Haus der Kette,
also dem Feld mit der größten Summe aus Spalte und Reihe, damit kein anderes Haus sie verdeckt. Die
Methode kommt vor zeichne_dach:
// Über dem vordersten Haus einer Kette schwebt ihr Name fn zeichne_leuchtreklame(&self, frame: &mut Frame, iso: &Iso, kette: Kette) { // Das vorderste Feld hat die größte Summe aus Spalte und Reihe let mut vorne: Option<Feld> = None; for nr in 0..SPALTEN * REIHEN { let feld = Feld { spalte: nr % SPALTEN, reihe: nr / SPALTEN, }; let weiter_vorn = vorne.is_none_or(|v| feld.spalte + feld.reihe > v.spalte + v.reihe); if self.spiel.belegung(feld) == Belegung::Hotel(kette) && weiter_vorn { vorne = Some(feld); } } let Some(feld) = vorne else { return; }; let hoehe = PLATTE + self.anzeige[feld.index()] * STOCKWERK + 34.0; let mitte = iso.punkt(feld.spalte as f32 + 0.5, feld.reihe as f32 + 0.5, hoehe); let farbe = kettenfarbe(kette); // Eine dunkle Tafel mit leuchtendem Rand und weichem Schein let breite = kette.name().chars().count() as f32 * 8.0 + 18.0; let tafel = Path::rounded_rectangle( Point::new(mitte.x - breite / 2.0, mitte.y - 11.0), Size::new(breite, 22.0), 11.0.into(), ); let schein = Stroke::default() .with_color(Color { a: 0.25, ..farbe }) .with_width(7.0); frame.stroke(&tafel, schein); frame.fill(&tafel, Color::from_rgba(0.05, 0.06, 0.12, 0.85)); frame.stroke(&tafel, Stroke::default().with_color(farbe).with_width(1.5)); frame.fill_text(Text { content: kette.name().to_string(), position: mitte, color: aufhellen(farbe, 0.4), size: 13.0.into(), font: Font { weight: font::Weight::Bold, ..Font::DEFAULT }, align_x: text::Alignment::Center, align_y: alignment::Vertical::Center, ..Text::default() }); }// Jede Dachform wird anders gezeichnet
is_none_orist wahr, wenn es noch gar kein Feld gibt, oder wenn die Bedingung für das vorhandene zutrifft. So finden wir in einem Durchgang das vorderste Feld.- Die Höhe ergibt sich aus der Platte, dem gerade angezeigten Haus und 34 Pixeln
Luft für das Dach. Weil wir
anzeigebenutzen, steigt die Reklame mit, während der Turm wächst. - Der Leuchteffekt entsteht aus drei Schichten: zuerst ein breiter, fast durchsichtiger Rand als Schein, dann die dunkle Tafel, zum Schluss ein schmaler, kräftiger Rand. Die Breite der Tafel schätzen wir aus der Zahl der Buchstaben.
fill_textdürfen wir diesmal direkt nehmen. Erinnerst du dich an Etappe 1? iced zeichnet Text immer ganz oben. Für eine Reklame, die über der Stadt schwebt, ist das genau richtig.
In draw zeichnen wir die Reklamen nach allen Parzellen, nur nachts und nur für Ketten,
die es gibt:
self.zeichne_parzelle(&mut frame, &iso, &farben, feld); }// Nachts schweben über den Ketten ihre Namen in Neon if self.nacht { for kette in self.spiel.aktive_ketten() { self.zeichne_leuchtreklame(&mut frame, &iso, kette); } }vec![frame.into_geometry()]
cargo run
Schalt auf Nachtmodus: Über jeder Kette leuchtet ihr Name. Wächst eine Kette nach vorne, wandert die Reklame mit, und bei einer Fusion verschwindet die Tafel der geschluckten Kette.Der komplette Code nach Etappe 10
Hier ist der vollständige Stand nach Etappe 10. 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, Task, Theme, Vector, alignment, font, time, window,
};
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,
// Rechnet der Computer gerade im Hintergrund?
denkt: bool,
// Wie stark die drei Gegner spielen
gegner: [Art; 3],
// Läuft das Spiel schon, oder wählst du noch die Gegner?
gestartet: bool,
// Wie hoch jedes Haus gerade gezeichnet wird, in Stockwerken
anzeige: Vec<f32>,
}
#[derive(Debug, Clone)]
enum Message {
MausUeber(Option<Feld>),
Geklickt(Feld),
TagNacht,
Spielzug(Zug),
KiTakt,
KiZug(Zug),
NeuesSpiel,
GegnerWechseln(usize),
Starten,
Bild,
}
impl Bauloewe {
fn neu() -> Bauloewe {
let gegner = [Art::Tourist, Art::Makler, Art::Hai];
Bauloewe {
spiel: Bauloewe::neues_spiel(gegner),
maus_feld: None,
nacht: false,
denkt: false,
gegner,
gestartet: false,
anzeige: vec![0.0; SPALTEN * REIHEN],
}
}
// Die Namen an den drei Plätzen bleiben gleich, die Spielstärke wählst du
fn neues_spiel(gegner: [Art; 3]) -> Spiel {
Spiel::neu(&[
("Leo", Art::Mensch),
("Toni", gegner[0]),
("Mia", gegner[1]),
("Hanna", gegner[2]),
])
}
// Du bist immer Spieler Nummer 0
fn ich(&self) -> &Spieler {
&self.spiel.spieler[0]
}
fn mensch_am_zug(&self) -> bool {
self.gestartet && self.spiel.aktueller().art == Art::Mensch
}
// Ist der Computer dran, tickt alle 0,6 Sekunden ein Takt
fn abo(&self) -> Subscription<Message> {
let takt = if !self.gestartet || self.mensch_am_zug() || self.spiel.phase == Phase::Ende {
Subscription::none()
} else {
time::every(Duration::from_millis(600)).map(|_| Message::KiTakt)
};
// Solange irgendwo gebaut wird, kommt zu jedem Bild ein Brief
let bauen = if self.baustelle() {
window::frames().map(|_| Message::Bild)
} else {
Subscription::none()
};
Subscription::batch([takt, bauen])
}
// Wie hoch soll das Haus auf diesem Feld am Ende sein?
fn ziel_hoehe(&self, feld: Feld) -> f32 {
match self.spiel.belegung(feld) {
Belegung::Frei => 0.0,
Belegung::Rohbau => 3.0,
Belegung::Hotel(kette) => hotel_stockwerke(self.spiel.groesse(kette), feld) as f32,
}
}
// Wächst irgendwo noch ein Haus?
fn baustelle(&self) -> bool {
(0..self.anzeige.len()).any(|nr| {
let feld = Feld {
spalte: nr % SPALTEN,
reihe: nr / SPALTEN,
};
self.anzeige[nr] < self.ziel_hoehe(feld)
})
}
fn update(&mut self, message: Message) -> Task<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
// Zurück zur Gegnerwahl, mit frisch gemischtem Spiel
Message::NeuesSpiel => {
self.spiel = Bauloewe::neues_spiel(self.gegner);
self.gestartet = false;
self.denkt = false;
self.anzeige = vec![0.0; SPALTEN * REIHEN];
}
// Tourist, Makler, Hai und wieder von vorn
Message::GegnerWechseln(platz) => {
self.gegner[platz] = match self.gegner[platz] {
Art::Tourist => Art::Makler,
Art::Makler => Art::Hai,
_ => Art::Tourist,
};
self.spiel = Bauloewe::neues_spiel(self.gegner);
}
Message::Starten => self.gestartet = true,
// Jedes Haus wächst ein Stück auf seine Zielhöhe zu
Message::Bild => {
for nr in 0..self.anzeige.len() {
let feld = Feld {
spalte: nr % SPALTEN,
reihe: nr / SPALTEN,
};
let ziel = self.ziel_hoehe(feld);
self.anzeige[nr] = (self.anzeige[nr] + 0.12).min(ziel);
}
}
// Der Takt für den Computer
Message::KiTakt => {
if !self.mensch_am_zug() && !self.denkt {
self.denkt = true;
let kopie = self.spiel.clone();
let denken = async move { ki::waehle_zug(&kopie) };
return Task::perform(denken, Message::KiZug);
}
}
// Die Antwort aus dem Hintergrund ist da
Message::KiZug(zug) => {
self.denkt = false;
if !self.mensch_am_zug() && self.spiel.moegliche_zuege().contains(&zug) {
self.spiel.ausfuehren(zug);
}
}
}
Task::none()
}
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.gestartet => String::from("Wähle deine Gegner"),
_ if self.spiel.phase == Phase::Ende => String::from("Schlussabrechnung"),
_ if self.denkt => format!("{} denkt nach …", self.spiel.aktueller().name),
_ 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 wer = match spieler.art {
Art::Mensch => spieler.name.clone(),
Art::Tourist => format!("{} · Tourist", spieler.name),
Art::Makler => format!("{} · Makler", spieler.name),
Art::Hai => format!("{} · Hai", spieler.name),
};
let zeile = row![
text(wer).size(13).color(farben.schrift).width(115),
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(330).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));
}
if !self.gestartet {
ebenen = ebenen.push(self.startdialog(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)
}
// Vor dem Spiel: Gegen wen spielst du?
fn startdialog(&self, farben: Farben) -> Element<'_, Message> {
let mut knoepfe = row![].spacing(8);
for (platz, spieler) in self.spiel.spieler.iter().skip(1).enumerate() {
let art = self.gegner[platz];
let beschriftung = format!("{} · {}", spieler.name, art_name(art));
let nachricht = Message::GegnerWechseln(platz);
knoepfe = knoepfe.push(nachrichtenknopf(&beschriftung, art_farbe(art), nachricht));
}
let los = nachrichtenknopf("Los geht's!", farben.akzent, Message::Starten);
knoepfe = knoepfe.push(los);
let frage = "Klick auf einen Gegner, um seine Spielstärke zu wechseln.";
dialogfenster(farben, "Wer spielt mit?", frage.to_string(), 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);
}
// Nachts schweben über den Ketten ihre Namen in Neon
if self.nacht {
for kette in self.spiel.aktive_ketten() {
self.zeichne_leuchtreklame(&mut frame, &iso, kette);
}
}
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;
// Neue Häuser wachsen erst noch in die Höhe
let hoehe = (plan.stockwerke as f32).min(self.anzeige[feld.index()]);
let stockwerke = hoehe as usize;
let haus = Quader {
s0: s + 0.2,
r0: r + 0.2,
s1: s + 0.8,
r1: r + 0.8,
z0: boden,
z1: boden + hoehe * 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);
}
// Über dem vordersten Haus einer Kette schwebt ihr Name
fn zeichne_leuchtreklame(&self, frame: &mut Frame, iso: &Iso, kette: Kette) {
// Das vorderste Feld hat die größte Summe aus Spalte und Reihe
let mut vorne: Option<Feld> = None;
for nr in 0..SPALTEN * REIHEN {
let feld = Feld {
spalte: nr % SPALTEN,
reihe: nr / SPALTEN,
};
let weiter_vorn = vorne.is_none_or(|v| feld.spalte + feld.reihe > v.spalte + v.reihe);
if self.spiel.belegung(feld) == Belegung::Hotel(kette) && weiter_vorn {
vorne = Some(feld);
}
}
let Some(feld) = vorne else {
return;
};
let hoehe = PLATTE + self.anzeige[feld.index()] * STOCKWERK + 34.0;
let mitte = iso.punkt(feld.spalte as f32 + 0.5, feld.reihe as f32 + 0.5, hoehe);
let farbe = kettenfarbe(kette);
// Eine dunkle Tafel mit leuchtendem Rand und weichem Schein
let breite = kette.name().chars().count() as f32 * 8.0 + 18.0;
let tafel = Path::rounded_rectangle(
Point::new(mitte.x - breite / 2.0, mitte.y - 11.0),
Size::new(breite, 22.0),
11.0.into(),
);
let schein = Stroke::default()
.with_color(Color { a: 0.25, ..farbe })
.with_width(7.0);
frame.stroke(&tafel, schein);
frame.fill(&tafel, Color::from_rgba(0.05, 0.06, 0.12, 0.85));
frame.stroke(&tafel, Stroke::default().with_color(farbe).with_width(1.5));
frame.fill_text(Text {
content: kette.name().to_string(),
position: mitte,
color: aufhellen(farbe, 0.4),
size: 13.0.into(),
font: Font {
weight: font::Weight::Bold,
..Font::DEFAULT
},
align_x: text::Alignment::Center,
align_y: alignment::Vertical::Center,
..Text::default()
});
}
// 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> {
nachrichtenknopf(beschriftung, farbe, Message::Spielzug(zug))
}
// Ein farbiger Knopf, der einen beliebigen Brief verschickt
fn nachrichtenknopf<'a>(
beschriftung: &str,
farbe: Color,
nachricht: Message,
) -> Element<'a, Message> {
button(text(beschriftung.to_string()).size(14))
.padding([8, 12])
.on_press(nachricht)
.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()
}
// Wie die Spielstärken heißen und welche Farbe ihr Knopf bekommt
fn art_name(art: Art) -> &'static str {
match art {
Art::Mensch => "Mensch",
Art::Tourist => "Tourist",
Art::Makler => "Makler",
Art::Hai => "Hai",
}
}
fn art_farbe(art: Art) -> Color {
match art {
Art::Mensch => kettenfarbe(Kette::Koenigshof),
Art::Tourist => kettenfarbe(Kette::Ankerplatz),
Art::Makler => kettenfarbe(Kette::Uferblick),
Art::Hai => kettenfarbe(Kette::Bergfried),
}
}
// 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 {
Bauplan {
stockwerke: hotel_stockwerke(groesse, feld),
fassade: mischen(farben.beton, kettenfarbe(kette), 0.25),
band: Some(kettenfarbe(kette)),
dach: dachform(kette),
}
}
// Wie viele Stockwerke ein Hotel einer Kette dieser Größe hat
fn hotel_stockwerke(groesse: usize, feld: Feld) -> usize {
let grundhoehe = 3.0 + (groesse as f32).sqrt() * 1.5;
let schwankung = zufall(feld.index(), 7) % 2;
grundhoehe as usize + schwankung as usize
}
// 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,
Makler,
Hai,
}
// Was einem Spieler gehört
#[derive(Clone)]
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
#[derive(Clone)]
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::Rng;
use rand::seq::{IndexedRandom, SliceRandom};
use crate::spiel::{Art, Feld, Kette, Phase, Spiel, Zug};
// So oft spielt der Hai jeden möglichen Zug durch
const DURCHLAEUFE: usize = 40;
// So viele Handlungen weit schaut er in jede Zukunft
const WEITSICHT: usize = 60;
// 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),
Art::Makler => makler(spiel),
Art::Hai => hai(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)
}
// Der Makler probiert jeden erlaubten Zug auf einer Kopie des Spiels aus
// und nimmt den, nach dem er am besten dasteht
fn makler(spiel: &Spiel) -> Zug {
let ich = spiel.handelnder();
let mut bester_zug = Zug::ZugBeenden;
let mut beste_bewertung = i64::MIN;
for zug in spiel.moegliche_zuege() {
let mut probe = spiel.clone();
probe.ausfuehren(zug);
let bewertung = vorausschauen(&probe, ich, 2);
if bewertung > beste_bewertung {
beste_bewertung = bewertung;
bester_zug = zug;
}
}
bester_zug
}
// Muss ich nach diesem Zug gleich noch einmal entscheiden (Gründung, Fusion),
// schaut der Makler weiter und rechnet mit seiner besten Antwort
fn vorausschauen(spiel: &Spiel, ich: usize, tiefe: u32) -> i64 {
let gleich_nochmal = matches!(spiel.phase, Phase::Gruenden(_) | Phase::Fusion { .. })
&& spiel.handelnder() == ich;
if !gleich_nochmal || tiefe == 0 {
return bewerten(spiel, ich);
}
spiel
.moegliche_zuege()
.into_iter()
.map(|zug| {
let mut probe = spiel.clone();
probe.ausfuehren(zug);
vorausschauen(&probe, ich, tiefe - 1)
})
.max()
.unwrap_or_else(|| bewerten(spiel, ich))
}
// Der Hai spielt jeden möglichen Zug viele Male in die Zukunft
// und nimmt den, der im Durchschnitt am besten ausgeht
fn hai(spiel: &Spiel) -> Zug {
let zuege = spiel.moegliche_zuege();
if zuege.len() == 1 {
return zuege[0];
}
let ich = spiel.handelnder();
let mut zufall = rand::rng();
let mut bester_zug = zuege[0];
let mut bester_schnitt = i64::MIN;
for &zug in &zuege {
let mut summe = 0;
for _ in 0..DURCHLAEUFE {
let mut probe = spiel.clone();
verdecken(&mut probe, ich, &mut zufall);
probe.ausfuehren(zug);
summe += ausspielen(probe, ich, &mut zufall);
}
let schnitt = summe / DURCHLAEUFE as i64;
if schnitt > bester_schnitt {
bester_schnitt = schnitt;
bester_zug = zug;
}
}
bester_zug
}
// Der Hai schummelt nicht: Fremde Hände und den Stapel kennt er nicht.
// Also verteilt er alle Plättchen, die er nicht sehen kann, zufällig neu.
fn verdecken(spiel: &mut Spiel, ich: usize, zufall: &mut impl Rng) {
let mut unbekannt: Vec<Feld> = spiel.stapel.clone();
for (nr, spieler) in spiel.spieler.iter().enumerate() {
if nr != ich {
unbekannt.extend(&spieler.hand);
}
}
unbekannt.shuffle(zufall);
for (nr, spieler) in spiel.spieler.iter_mut().enumerate() {
if nr != ich {
let anzahl = spieler.hand.len();
spieler.hand = unbekannt.split_off(unbekannt.len() - anzahl);
}
}
spiel.stapel = unbekannt;
}
// Eine mögliche Zukunft: Alle spielen zufällig weiter, dann wird bewertet
fn ausspielen(mut spiel: Spiel, ich: usize, zufall: &mut impl Rng) -> i64 {
spiel.protokoll.clear();
for _ in 0..WEITSICHT {
if spiel.phase == Phase::Ende {
break;
}
let zuege = spiel.moegliche_zuege();
let Some(&zug) = zuege.choose(zufall) else {
break;
};
spiel.ausfuehren(zug);
}
bewerten(&spiel, ich)
}
// Wie gut steht Spieler ich da? Sein Vermögen minus das des stärksten Gegners
fn bewerten(spiel: &Spiel, ich: usize) -> i64 {
let mein = vermoegen(spiel, ich);
let bester_gegner = (0..spiel.spieler.len())
.filter(|&nr| nr != ich)
.map(|nr| vermoegen(spiel, nr))
.max()
.unwrap_or(0);
// Ganz ohne Bargeld kann man nicht mehr kaufen: Ein Polster ist Gold wert
let geld = spiel.spieler[ich].geld as i64;
let polster = if spiel.phase != Phase::Ende && geld < 800 {
800 - geld
} else {
0
};
mein - bester_gegner - polster
}
// Geld, Aktien zum Kurs und ein Teil der Prämien, die man erwarten darf
fn vermoegen(spiel: &Spiel, nr: usize) -> i64 {
let spieler = &spiel.spieler[nr];
let mut summe = spieler.geld as i64;
for kette in Kette::ALLE {
// Aktien einer Kette, die gerade geschluckt wird, zählen nicht mehr
if let Phase::Fusion { tot, .. } = spiel.phase {
if tot == kette {
continue;
}
}
let preis = spiel.preis(kette) as i64;
let meine = spieler.aktien[kette.nr()];
summe += meine as i64 * preis;
// Wer hält mehr Aktien als ich? Davon hängt meine Prämie ab.
let mehr = spiel
.spieler
.iter()
.filter(|anderer| anderer.aktien[kette.nr()] > meine)
.count();
if meine > 0 && mehr == 0 {
summe += preis * 5;
} else if meine > 0 && mehr == 1 {
summe += preis * 2;
}
}
summe
}
#[cfg(test)]
mod tests {
use super::*;
// Ein kleines Turnier: Wer gewinnt wie oft?
// Starten mit: cargo test turnier -- --ignored --nocapture
#[test]
#[ignore]
fn turnier() {
let arten = [Art::Hai, Art::Makler, Art::Makler, Art::Makler];
let mut siege = [0; 4];
for _ in 0..100 {
let teilnehmer: Vec<(&str, Art)> = arten.iter().map(|&art| ("KI", art)).collect();
let mut spiel = Spiel::neu(&teilnehmer);
while spiel.phase != Phase::Ende {
let zug = waehle_zug(&spiel);
spiel.ausfuehren(zug);
}
let sieger = (0..4).max_by_key(|&nr| spiel.spieler[nr].geld).unwrap();
siege[sieger] += 1;
}
println!("Siege: {siege:?}");
}
}
nachrichtenknopf lassen sich speziellere wie farbknopf bauen.
Animation heißt: einen angezeigten Wert Bild für Bild auf einen Zielwert zulaufen
lassen. window::frames() liefert den Takt, aber nur, solange er gebraucht wird, und
Subscription::batch verbindet mehrere Abos. Mit ein paar übereinandergelegten Formen
entsteht ein Leuchteffekt.Zum Ausprobieren
- Zeitlupe: Lass die Häuser viermal langsamer wachsen.
- Tagesreklame: Zeig die Reklamen auch tagsüber, dort aber mit heller Tafel und dunkler Schrift.
- Sichere Kronen: Ketten ab 11 Feldern sollen über ihrer Reklame einen kleinen
goldenen Stern bekommen. Tipp: Ein Stern ist ein
vieleckmit zehn Ecken, abwechselnd weit und nah an der Mitte.
Lösungen anzeigen
1. In update bei Message::Bild aus
0.12 0.03 machen.
2. In draw das if self.nacht
entfernen und zeichne_leuchtreklame die Farben je nach Modus wählen lassen, zum
Beispiel die Tafel mit if self.nacht { … } else { Color::WHITE } und die Schrift in
abdunkeln(farbe, 0.7).
3. Die zehn Ecken liegen auf zwei Kreisen:
let ecken: Vec<Point> = (0..10)
.map(|i| {
let winkel = i as f32 * std::f32::consts::PI / 5.0 - std::f32::consts::FRAC_PI_2;
let radius = if i % 2 == 0 { 7.0 } else { 3.0 };
Point::new(mitte.x + radius * winkel.cos(), mitte.y - 22.0 + radius * winkel.sin())
})
.collect();
frame.fill(&vieleck(&ecken), kettenfarbe(Kette::Koenigshof));
Das gehört in zeichne_leuchtreklame, eingepackt in
if self.spiel.groesse(kette) >= SICHER.
Du hast ein ganzes Spiel gebaut
Schau zurück, was in zehn Etappen entstanden ist: ein Spielbrett in Isometrie, gezeichnet ohne eine
einzige Bilddatei, sieben Hotelketten mit eigener Architektur, ein vollständiges Regelwerk mit Aktien,
Fusionen und Schlussabrechnung, und drei Computergegner, vom Zufall über Faustregeln bis zur
Monte-Carlo-Suche. Nebenbei hast du Module, enums mit Daten, Zustandsautomaten,
Rekursion, Tests, Tasks und Animationen kennengelernt.
Wie geht es weiter? Ein paar Ideen: Speichere Spielstände mit serde in einer Datei. Gib
dem Hai einen Regler für seine Denkzeit. Füge Geräusche hinzu, wenn eine Fusion knallt. Oder bau eine
Kameraschwenk-Animation beim Spielstart. Das Fundament dafür hast du.
Der Baulöwe ist fertig 🦁
Glückwunsch! Du hast ein komplettes Wirtschaftsspiel mit programmierter Grafik und drei Computergegnern gebaut, als echte Desktop-App für Linux, macOS und Windows. Zeig es her, spiel es mit Freunden, und bau weiter daran.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren