Hotelketten gründen
Jetzt wird aus der Baustelle eine Stadt. Liegen zwei Häuser nebeneinander, gründest du eine Hotelkette, und jede weitere Parzelle daneben lässt sie wachsen. Sieben Ketten gibt es, jede mit eigener Farbe und eigener Dachform, vom Dachgarten bis zur goldenen Turmspitze. Und je größer eine Kette wird, desto höher ragen ihre Türme.
Was steht auf dem Feld?
Bisher merkt sich das Spiel in einer Liste, wo gebaut wurde. Das reicht nicht mehr, denn ab
jetzt kann auf einem Feld dreierlei stehen: nichts, ein Rohbau oder ein Hotel einer bestimmten
Kette. Für so ein „entweder, oder, oder“ ist in Rust das enum da. Wir beginnen
mit zwei Möglichkeiten und legen es in spiel.rs vor die Struktur
Spiel:
// Was auf einem Feld steht #[derive(Debug, Clone, Copy, PartialEq)] pub enum Belegung { Frei, Rohbau, }// Alles, was die Spielregeln wissen müssen
Statt einer Liste gebauter Felder bekommt das Spiel ein Brett: eine Liste mit
genau 108 Einträgen, einer pro Feld. Damit wir zu jedem Feld seinen Platz in dieser Liste
finden, bekommt Feld eine Methode index. Wir zählen dabei Reihe für
Reihe durch, wie beim Lesen: 1A ist Platz 0, 12A ist Platz 11, 1B ist Platz 12.
impl Feld {// Die Nummer des Feldes in einer Liste mit 108 Einträgen pub fn index(self) -> usize { self.reihe * SPALTEN + self.spalte }// Der Name wie auf dem Spielplan: "1A" bis "12I"
pub struct Spiel {pub gebaut: Vec<Feld>,pub brett: Vec<Belegung>,⋮let mut spiel = Spiel {gebaut: Vec::new(),brett: vec![Belegung::Frei; SPALTEN * REIHEN],
vec![Belegung::Frei; 108] baut eine Liste, in der 108-mal dasselbe steht. Damit
Rust den Wert so oft kopieren darf, trägt Belegung die Markierungen
Clone und Copy. Jetzt noch eine Methode zum Nachschauen und die
geänderte Zeile in legen:
// 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
self.hand.remove(stelle);self.gebaut.push(feld);self.brett[feld.index()] = Belegung::Rohbau;
In main.rs fragen wir beim Zeichnen jetzt nach der Belegung. Weil es bald drei
Fälle gibt, nehmen wir gleich ein match statt eines if. Der Code
für das Namensschild rückt dabei eine Stufe ein:
use spiel::{Feld, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, REIHEN, SPALTEN, Spiel};⋮if self.spiel.gebaut.contains(&feld) { self.zeichne_rohbau(frame, iso, farben, feld, hoehe); } else { 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)); }match self.spiel.belegung(feld) { Belegung::Rohbau => self.zeichne_rohbau(frame, iso, farben, feld, hoehe), 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)); } }}
cargo run
Alles wie vorher. Unter der Haube steht jetzt aber ein Brett statt einer Liste, und das ist genau die Grundlage für die Ketten.Ein Bauplan für Häuser
Unser Rohbau ist fest verdrahtet: immer drei Stockwerke, immer Beton. Hotels sollen aber je
nach Kette anders aussehen und mit der Kette wachsen. Statt für jede Art Haus eine eigene
Zeichenfunktion zu schreiben, beschreiben wir ein Haus durch einen Bauplan
und lassen eine einzige Funktion jeden Bauplan zeichnen. Der Bauplan kommt vor
Quader:
// Die Beschreibung eines Hauses: wie hoch, welche Farbe struct Bauplan { stockwerke: usize, fassade: Color, }// Ein Quader: von (s0, r0) bis (s1, r1) auf dem Brett, von z0 bis z1 in der Höhe
Aus zeichne_rohbau wird zeichne_haus, und die Funktion bekommt den
Bauplan als siebten Parameter. An drei Stellen ersetzen wir feste Werte durch Angaben aus dem
Plan:
fn zeichne_rohbau(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 = 3;let stockwerke = plan.stockwerke;⋮z1: boden + stockwerke as f32 * STOCKWERK,farbe: farben.beton,farbe: plan.fassade,};⋮];frame.fill(&vieleck(&dach), abdunkeln(farben.beton, 0.85));frame.fill(&vieleck(&dach), abdunkeln(plan.fassade, 0.85));}
Und in zeichne_parzelle schreiben wir den Bauplan für einen Rohbau:
match self.spiel.belegung(feld) {Belegung::Rohbau => self.zeichne_rohbau(frame, iso, farben, feld, hoehe),Belegung::Rohbau => { let plan = Bauplan { stockwerke: 3, fassade: farben.beton, }; self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan); }Belegung::Frei => {
Das nennt man Umbauen ohne Umzug oder, unter Programmierern, Refactoring: Das Programm tut danach genau dasselbe wie vorher, ist aber für das Kommende vorbereitet.
cargo run
Die Rohbauten sehen genauso aus wie bisher. Probier zum Spaß im Bauplan stockwerke: 6 aus, und stell es danach wieder auf 3.Sieben Hotelketten
Jetzt kommt der Kern des Spiels. Die Regel ist einfach: Legst du ein Plättchen direkt neben einen Rohbau, entsteht eine Hotelkette, und du darfst ihr einen Namen geben. Legst du ein Plättchen neben eine bestehende Kette, wächst sie, und zwar samt aller Rohbauten, die mit ihr verbunden sind. „Neben“ heißt dabei: links, rechts, oben oder unten, nicht schräg.
Es gibt sieben Ketten, jede mit eigenem Namen. Das ist wieder ein klarer Fall für ein
enum. Es kommt in spiel.rs vor die Belegung, und die Belegung bekommt
ihren dritten Fall, der sich merkt, zu welcher Kette ein Hotel gehört:
// 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, ]; 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⋮Frei, Rohbau,Hotel(Kette),}
ALLE ist eine Konstante innerhalb von impl Kette: eine Liste mit allen
sieben Ketten, damit wir sie später in Schleifen durchgehen können. &'static str
ist ein Text, der fest im Programm steht und nie verschwindet. Genau das sind die Namen.
Für die Nachbarschaft bekommt Feld eine Methode, die die bis zu vier Nachbarn
liefert. Am Rand gibt es weniger, deshalb prüfen wir jede Richtung, bevor wir sie in die Liste
packen:
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"
Das Spiel muss sich merken, wenn eine Gründung auf deine Entscheidung wartet:
pub hand: Vec<Feld>,// Hier wartet eine neue Kette darauf, dass man ihr einen Namen gibt pub gruendung: Option<Feld>,}⋮hand: Vec::new(),gruendung: None,};
Und jetzt die Regeln selbst. legen wird dafür komplett neu geschrieben, und
darunter kommen fünf neue Methoden. Ersetze also alles ab dem Kommentar über
legen bis zum Ende der Datei:
// Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht. pub fn legen(&mut self, feld: Feld) -> bool { let Some(stelle) = self.hand.iter().position(|&h| h == feld) else { return false; }; self.hand.remove(stelle); self.brett[feld.index()] = Belegung::Rohbau; self.nachziehen(); true } }// 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.gruendung.is_some() { 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); } else if rohbau_daneben { self.gruendung = Some(feld); } self.nachziehen(); true } // Gründet an der wartenden Stelle eine neue Kette 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); } } // 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() } // 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); } } } } }
Das ist ein großer Brocken, gehen wir ihn in Ruhe durch:
- Erst prüfen, dann handeln.
legensammelt zuerst alles, was es wissen muss: welche Ketten angrenzen und ob ein Rohbau daneben steht.anyfragt, ob irgendein Nachbar die Bedingung erfüllt. Erst wenn alle Prüfungen bestanden sind, wird das Plättchen wirklich aus der Hand genommen. So kann nie ein halb ausgeführter Zug entstehen. - Zwei Ketten auf einmal? Das wäre eine Fusion, die große Kette schluckt die kleine. Das bauen wir in Etappe 6. Bis dahin ist so ein Zug einfach nicht erlaubt.
- Drei Ausgänge. Grenzt eine Kette an, breitet sie sich aus. Grenzt nur ein Rohbau an, merken wir uns die Gründung und warten auf deine Wahl. Grenzt gar nichts an, bleibt es ein Rohbau.
gruendennimmt die wartende Stelle mittake()aus demOptionheraus. Danach steht dort wiederNone, die Gründung ist erledigt.groessezählt mitfilterundcountalle Felder einer Kette.freie_kettenfiltert ausALLEdiejenigen heraus, die noch nirgends stehen, undcollectsammelt sie in einer neuen Liste.ausbreitenist das Herzstück: Es färbt ein Feld ein und merkt sich alle Rohbau-Nachbarn in einer Listeoffen. Solange diese Liste nicht leer ist, nimmt es das nächste Feld heraus, färbt es ein und schaut sich wieder dessen Nachbarn an. So läuft die Farbe durch alle verbundenen Rohbauten wie Wasser durch Rinnen. Man nennt das eine Flutfüllung, das Füllwerkzeug in Malprogrammen funktioniert genauso.
ketten_neben und ausbreiten haben kein
pub. Sie sind Hilfsarbeiter der Regeln und gehen main.rs nichts an.
Von außen kann man eine Kette also nur über legen und gruenden
verändern, und genau dort stehen die Prüfungen. So kann die Oberfläche die Regeln gar nicht
aus Versehen umgehen.Weiter in main.rs. Jede Kette bekommt eine Erkennungsfarbe, und Hotels werden nach
einem eigenen Bauplan gezeichnet: Die Fassade ist leicht in der Kettenfarbe getönt, oben läuft
ein farbiges Band ums Haus, und die Höhe wächst mit der Kette. Die Wurzel
(sqrt) sorgt dafür, dass kleine Ketten schnell in die Höhe gehen, große aber nicht
endlos: Zwei Felder ergeben gut 5 Stockwerke, zwölf Felder etwa 8, vierzig Felder knapp 12.
Eine kleine Schwankung aus unserer Zufallsfunktion macht die Skyline lebendiger. Das alles
kommt unter den Bauplan, der dafür ein Feld band bekommt:
fassade: Color,band: Option<Color>,}// 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)), } } // 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
mischen ist ein neuer Farbhelfer. Er kommt zu den anderen:
// 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
Das Band zeichnen wir in zeichne_haus direkt vor dem Dach, auf beiden sichtbaren
Seiten, rechts wie immer etwas dunkler. if let Some(band) heißt: Nur wenn der Plan
ein Band hat, wird es gezeichnet. Rohbauten haben keins.
// 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)); }// Das Flachdach bekommt einen Rand
stockwerke: 3, fassade: farben.beton,band: None,}; 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 => {
Bleibt die Wahl der Kette. Dafür gibt es einen neuen Brief, und wenn eine Gründung wartet, legen wir ein Auswahlfenster über alles:
use iced::widget::canvas::{Frame, Path, Stroke, Text, gradient};use iced::widget::{button, canvas, column, row, space, stack, text}; use iced::{ Background, Border, Color, Element, Fill, Font, Point, Rectangle, Renderer, Size, Theme, alignment, font, };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, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, Kette, REIHEN, SPALTEN, Spiel};
TagNacht,Gruenden(Kette),}⋮Message::TagNacht => self.nacht = !self.nacht,Message::Gruenden(kette) => self.spiel.gruenden(kette),}
let folie = column![kopfzeile, space::vertical(), fussleiste];stack![brett, folie].into()// Wartet eine Gründung, kommt das Auswahlfenster obendrauf let mut ebenen = stack![brett, folie]; if self.spiel.gruendung.is_some() { 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 {
- Eine dritte Ebene.
stack!lässt sich wie eine Liste verlängern: Mitpushkommt nur dann eine Ebene obendrauf, wenn eine Gründung wartet. - Die Knöpfe entstehen in einer Schleife über die freien Ketten, jeder in
seiner Kettenfarbe. Ein Klick verschickt
Message::Gruendenmit genau dieser Kette. - Die Karte ist ein
containermit Innenabstand, runden Ecken und einem weichen Schatten (Shadow), der sie über dem Brett schweben lässt.Vector::new(0.0, 8.0)verschiebt den Schatten 8 Pixel nach unten,blur_radiusmacht ihn weich. - Der Schleier. Außen herum liegt ein zweiter
container, der das ganze Fenster mit halbdurchsichtigem Schwarz abdunkelt und mitcenter(Fill)die Karte genau in die Mitte setzt.
Zum Schluss eine Farbe für die Karte, bei Tag hell und bei Nacht dunkel:
stange: Color,tafel: Color,}⋮stange: Color::WHITE,tafel: Color::from_rgb8(0xfb, 0xf8, 0xf1),⋮stange: Color::from_rgb8(0xcf, 0xd3, 0xdc),tafel: Color::from_rgb8(0x1e, 0x22, 0x33),
cargo run
Leg zwei Plättchen nebeneinander. Beim zweiten erscheint die Karte „Neue Hotelkette!“. Such dir eine Kette aus, und beide Häuser verwandeln sich in Hotels mit farbigem Band. Leg weitere Plättchen daneben, und die Kette wächst, samt aller Rohbauten, die mit ihr verbunden sind. Mit jedem Feld werden die Häuser der Kette ein Stück höher.
Sieben Dächer
Farbe allein macht noch keinen Charakter. Jede Kette bekommt deshalb ihre eigene Dachform:
das schlichte Flachdach der günstigen Zugvogel-Pensionen, Dachgärten beim Ankerplatz,
Burgzinnen beim Bergfried, Funkmasten beim Uferblick, spitze Dächer bei Morgenrot, gestufte
Aufbauten wie bei alten Hochhäusern bei Aurora und eine goldene Turmspitze beim Königshof.
Die Formen kommen als enum in den Bauplan:
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
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
band: None,dach: Dach::Flach,};
In zeichne_haus ersetzen wir das feste Flachdach durch einen Aufruf, der je nach
Dachform zeichnet:
frame.fill(&vieleck(&rechts), abdunkeln(band, 0.75)); }// Das Flachdach bekommt einen Rand 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(plan.fassade, 0.85));self.zeichne_dach(frame, iso, &haus, plan);}
Die neue Methode zeichne_dach kommt ans Ende des zweiten
impl Bauloewe-Blocks, direkt dahinter zwei freie Helfer für das Flachdach (das
alte, jetzt als eigene Funktion) und für spitze Dächer:
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) {
Keine Angst vor der Länge, jeder Zweig ist ein kleines Bastelprojekt aus bekannten Teilen:
- Garten: Flachdach plus zwei grüne Kreise als Büsche, leicht versetzt, der hintere dunkler als der vordere.
- Zinnen: vier kleine Quader auf den Ecken. Sie werden in der Reihenfolge hinten, rechts, links, vorne gezeichnet, also wieder nach dem Maler-Algorithmus.
- Antenne: ein Mast wie beim Fähnchen, oben ein kleiner Kreis. Nachts wird er zur roten Warnleuchte, wie auf echten Hochhäusern.
- Pyramide: Von einem spitzen Dach sehen wir zwei Dreiecke, jeweils von den
vorderen Oberkanten bis zur Spitze über der Hausmitte. Das erledigt der Helfer
pyramide, den auch die Turmspitze benutzt. - Stufen: zwei immer schmalere Aufsätze übereinander. Die Schleife geht über Paare aus Einrückung und Höhe.
- Turmspitze: ein schmaler Sockel und darauf eine sehr hohe, schlanke Pyramide in Gold.
plan.band.unwrap_or(plan.fassade) holt die Bandfarbe aus dem
Option, und wenn es keine gibt, nimmt es stattdessen die Fassade. Dächer erscheinen
so in der Farbe ihrer Kette.
cargo run
Gründe mehrere Ketten und schau dir die Dächer an. Schalte auch mal auf Nacht: Die Funkmasten des Uferblicks blinken jetzt rot.Die Kettentafel
Wie groß ist eigentlich welche Kette? Das soll man auf einen Blick sehen, ohne Felder zu
zählen. Oben rechts kommt dafür eine Tafel hin, mit einem farbigen Punkt, dem Namen und der
Größe jeder Kette. Sie entsteht in view, direkt vor der Folie, und die Folie
bekommt sie als zweite Zeile:
.align_y(alignment::Vertical::Center);// 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); 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 = 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), ] .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, space::vertical(), fussleiste];let folie = column![kopfzeile, tafelzeile, space::vertical(), fussleiste];
- Der farbige Punkt ist ein leerer
containermitspace()als Inhalt, 12 mal 12 Pixel groß, mit Hintergrundfarbe und so stark gerundeten Ecken, dass ein Kreis daraus wird. - Feste Breite für den Namen. Mit
.width(90)bekommt jeder Name gleich viel Platz, dadurch stehen die Größenangaben ordentlich untereinander wie in einer Tabelle. format!("{groesse} Felder"): Steht der Name einer Variable direkt in den geschweiften Klammern, setzt Rust ihren Wert ein.- Halbdurchsichtig.
Color { a: 0.85, ..farben.tafel }lässt das Brett ein wenig durch die Tafel schimmern, falls ein hoher Turm dahinter steht.
cargo run
Oben rechts steht die Kettentafel. Freie Ketten sind als „frei“ markiert. Gründest du eine Kette oder lässt eine wachsen, zählt die Tafel sofort mit.
Der komplette Code nach Etappe 3
Hier ist der vollständige Stand nach Etappe 3. 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, Kette, 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((980.0, 680.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),
}
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),
}
}
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 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, 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.spiel.hand {
let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE))
.padding([8, 14])
.on_press(Message::Geklickt(feld))
.style(move |_theme: &Theme, status| {
let grund = match status {
button::Status::Hovered => farben.rasen_hell,
_ => 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);
let fussleiste = row![hand, space::horizontal(), stapel]
.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);
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 = 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),
]
.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 self.spiel.gruendung.is_some() {
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,
}
}
// 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;
#[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,
];
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),
}
// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
pub brett: Vec<Belegung>,
pub stapel: Vec<Feld>,
pub hand: Vec<Feld>,
// Hier wartet eine neue Kette darauf, dass man ihr einen Namen gibt
pub gruendung: Option<Feld>,
}
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(),
gruendung: None,
};
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.gruendung.is_some() {
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);
} else if rohbau_daneben {
self.gruendung = Some(feld);
}
self.nachziehen();
true
}
// Gründet an der wartenden Stelle eine neue Kette
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);
}
}
// 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()
}
// 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);
}
}
}
}
}
enum mit Daten wie
Hotel(Kette) merkt sich nicht nur was, sondern auch welches. Das
Brett ist eine Liste mit 108 Plätzen, index findet den richtigen. Die
Flutfüllung breitet eine Kette über alle verbundenen Felder aus. Ein
Bauplan beschreibt ein Haus als Daten, und eine einzige Funktion kann daraus
Rohbauten, Hotels und sieben Dachformen zeichnen. Private Hilfsfunktionen sorgen dafür, dass die
Oberfläche die Regeln nicht umgehen kann.Zum Ausprobieren
- Wolkenkratzer: Lass Hotels doppelt so stark wachsen. Welche Zahl änderst du?
- Eigene Handschrift: Gib den Zugvögeln statt des Flachdachs einen Dachgarten. Wie viele Zeilen sind das?
- Test für die Flutfüllung: Schreib einen Test: Ein Rohbau auf 1A, ein Rohbau
auf 2A, dann legt man 3A neben eine bestehende Kette auf 4A. Hinterher müssen alle vier Felder
zur Kette gehören. Tipp: Im Test darfst du
brettundhanddirekt vorbereiten.
Lösungen anzeigen
1. In hotel_bauplan die
1.5 durch 3.0 ersetzen. Die Wurzel bleibt, damit auch große Ketten
noch ins Fenster passen.
2. Eine Zeile in dachform:
Kette::Zugvogel => Dach::Garten,. Weil jede Kette ihr Dach an genau einer Stelle
bekommt, ist Umgestalten ganz einfach.
3. Ans Ende von spiel.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kette_schluckt_verbundene_rohbauten() {
let mut spiel = Spiel::neu();
let feld = |spalte| Feld { spalte, reihe: 0 };
spiel.brett[feld(0).index()] = Belegung::Rohbau;
spiel.brett[feld(1).index()] = Belegung::Rohbau;
spiel.brett[feld(3).index()] = Belegung::Hotel(Kette::Aurora);
spiel.hand = vec![feld(2)];
assert!(spiel.legen(feld(2)));
assert_eq!(spiel.groesse(Kette::Aurora), 4);
}
}
feld ist hier eine kleine Closure, die aus einer Spaltennummer ein Feld in
Reihe A macht. assert! prüft, dass legen geklappt hat.
Etappe 3 geschafft 🏨
Deine Stadt hat jetzt Hotelketten mit eigenem Gesicht. In der nächsten Etappe kommt Geld ins Spiel: Jede Kette hat einen Aktienkurs, der mit ihrer Größe steigt, und du kannst Anteile kaufen. Wer eine Kette gründet, bekommt eine Aktie geschenkt.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren