Plättchen ziehen
Bisher durftest du überall bauen. Damit ist jetzt Schluss, denn ab heute gelten Spielregeln: 108 Bauplättchen liegen gemischt im Stapel, sechs davon hast du auf der Hand, und gebaut wird nur, wo du ein Plättchen hast. Dafür ziehen die Regeln in eine eigene Datei um, und auf dem Brett stecken kleine Vermessungsfähnchen.
Die Regeln ziehen um
Bis jetzt steht alles in main.rs. Ab dieser Etappe bekommt das Spiel echte
Regeln, und die wollen wir von Anfang an sauber von der Grafik trennen. Der Grund liegt
einige Etappen voraus: Unsere Computergegner sollen später tausende Partien im Kopf
durchspielen. Dafür brauchen sie die Regeln ohne Fenster, ohne Maus, ohne Farben. Deshalb
bekommen die Regeln ein eigenes Modul, eine eigene Datei
src/spiel.rs.
Und weil wir gleich Plättchen mischen wollen, holen wir uns die Zufalls-Bibliothek
rand dazu:
cargo add rand@0.9
Leg jetzt neben main.rs eine neue Datei spiel.rs an. Hinein kommt
zuerst nur, was ganz klar zu den Regeln gehört: die Größe des Bretts und das Feld.
// Das Brett: 12 Spalten (1 bis 12) und 9 Reihen (A bis I)
pub const SPALTEN: usize = 12;
pub const REIHEN: usize = 9;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Feld {
pub spalte: usize,
pub reihe: usize,
}
impl Feld {
// 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)
}
}
Sieht aus wie vorher, mit einem Unterschied: überall steht pub davor. Alles in
einem Modul ist nämlich erst einmal privat, also nur innerhalb der eigenen
Datei sichtbar. pub (kurz für public, öffentlich) öffnet die Tür für
andere Dateien. Bei einer Struktur muss man das sogar für jedes einzelne Feld sagen.
In main.rs löschst du die alten Konstanten und das alte Feld.
Stattdessen melden wir das Modul ganz oben an und holen uns die drei Dinge heraus:
mod spiel;use iced::mouse;⋮alignment, font, };use spiel::{Feld, REIHEN, SPALTEN};// Das Brett: 12 Spalten (1 bis 12) und 9 Reihen (A bis I) const SPALTEN: usize = 12; const REIHEN: usize = 9;// So groß erscheint ein Feld auf dem Bildschirm
nacht: bool, }#[derive(Debug, Clone, Copy, PartialEq)] struct Feld { spalte: usize, reihe: usize, } impl Feld { // Der Name wie auf dem Spielplan: "1A" bis "12I" fn name(self) -> String { let buchstabe = (b'A' + self.reihe as u8) as char; format!("{}{}", self.spalte + 1, buchstabe) } }#[derive(Debug, Clone)] enum Message {
mod spiel; sagt Rust: „Es gibt eine Datei spiel.rs, nimm sie mit
ins Programm.“ Mit use spiel::{…} holen wir uns wie bei iced die Dinge heraus, die
wir mit kurzem Namen benutzen wollen.
cargo run
Das Programm sieht genauso aus wie am Ende von Etappe 1 und kann genauso viel. Das ist beim Umziehen genau richtig: Wir haben nur Möbel verschoben, nichts kaputt gemacht.Stapel und Hand
Im echten Spiel gibt es für jedes der 108 Felder genau ein Plättchen. Alle liegen verdeckt
im Stapel, und jeder hat sechs davon auf der Hand. Bauen darf man nur auf einem Feld, dessen
Plättchen man hat. Das bringen wir jetzt ins Spiel, und zwar in eine Struktur
Spiel, die alles zusammenhält, was die Regeln wissen müssen. Zuerst ganz oben in
spiel.rs das Mischwerkzeug und die Handgröße:
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)]
Und dann ans Ende der Datei das Spiel selbst:
format!("{}{}", self.spalte + 1, buchstabe) } }// Alles, was die Spielregeln wissen müssen pub struct Spiel { pub gebaut: Vec<Feld>, pub stapel: Vec<Feld>, pub hand: Vec<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 { gebaut: Vec::new(), stapel, hand: Vec::new(), }; spiel.nachziehen(); spiel } // 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 { let Some(stelle) = self.hand.iter().position(|&h| h == feld) else { return false; }; self.hand.remove(stelle); self.gebaut.push(feld); self.nachziehen(); true } }
Drei Listen merkt sich das Spiel: was schon gebaut ist, was noch im
stapel liegt und was du auf der hand hast. Die Methoden im
Einzelnen:
neulegt für jedes Feld ein Plättchen in den Stapel und mischt ihn mitshuffle.rand::rng()ist der Zufallsgenerator, denranduns fertig mitgibt. Das Mischen kommt vonSliceRandom, das wir oben mitusegeholt haben. Rust verlangt, dass man solche Zusatzfähigkeiten ausdrücklich einholt, sonst kennt die Listeshufflegar nicht.nachziehennimmt so lange Plättchen vom Stapel, bis die Hand wieder sechs hat.popnimmt das oberste Plättchen weg und gibtSome(feld)zurück, oderNone, wenn der Stapel leer ist. Dann beendetbreakdie Schleife. Zum Schluss sortieren wir die Hand, damit sie ordentlich aussieht.legensucht das Feld mitpositionin der Hand. Die Zeile mitlet … elseist ein Türsteher: Nur wennpositionetwas findet, geht es weiter, sonst verlässt die Funktion sofort mitfalse. Wer das Plättchen nicht hat, darf eben nicht bauen.
|&h| h == feld: Das & vor dem h ist kein
Tippfehler. iter() liefert Verweise auf die Felder, und mit &h
packen wir den Verweis gleich aus, sodass h ein ganz normales Feld
ist. Das geht, weil Feld Copy ist.In main.rs tauschen wir die Liste der Rohbauten gegen ein ganzes Spiel. Weil
Spiel::neu mischt, kann Rust es nicht mehr mit #[derive(Default)]
selbst erzeugen. Wir schreiben den Anfangszustand deshalb von Hand in einer Funktion
neu und geben die iced statt default:
use spiel::{Feld, REIHEN, SPALTEN};use spiel::{Feld, REIHEN, SPALTEN, Spiel};⋮iced::application(Bauloewe::default, Bauloewe::update, Bauloewe::view)iced::application(Bauloewe::neu, Bauloewe::update, Bauloewe::view)
#[derive(Default)]struct Bauloewe {spiel: Spiel,maus_feld: Option<Feld>,rohbauten: Vec<Feld>,nacht: bool, }
impl Bauloewe {fn neu() -> Bauloewe { Bauloewe { spiel: Spiel::neu(), maus_feld: None, nacht: false, } }fn update(&mut self, message: Message) {⋮Message::Geklickt(feld) => {if !self.rohbauten.contains(&feld) { self.rohbauten.push(feld); }self.spiel.legen(feld);}
if self.rohbauten.contains(&feld) {if self.spiel.gebaut.contains(&feld) {
cargo run
Klick wild auf dem Brett herum. Meistens passiert nichts, aber ab und zu wächst ein Haus. Das sind die Felder, deren Plättchen zufällig auf deiner Hand liegen. Die Hand ist noch unsichtbar, aber die Regel gilt schon. Bei jedem Start ist es eine andere Hand, weil jedes Mal neu gemischt wird.Vermessungsfähnchen
Blind raten macht keinen Spaß. Auf jedes Feld, dessen Plättchen du auf der Hand hast, stellen
wir deshalb ein kleines Vermessungsfähnchen, wie auf einem echten Bauplatz. Und nur diese
Parzellen heben sich unter der Maus an. Das Fähnchen ist eine senkrechte Linie mit einem
orangen Dreieck obendrauf. Es kommt als freier Helfer vor Iso:
// 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
Der Fuß steht ein Stück hinter der Feldmitte, damit er dem Namensschild nicht in die Quere
kommt. Von dort geht die Stange 20 Pixel gerade nach oben. Das dürfen wir einfach direkt in
Bildschirmpixeln rechnen, denn senkrecht bleibt in unserer Ansicht senkrecht.
Path::line ist eine fertige Abkürzung für eine Linie zwischen zwei Punkten.
In zeichne_parzelle fragen wir, ob das Feld zu deinem Bauplan gehört. Nur dann
darf es sich anheben, und nur dann bekommt es ein Fähnchen:
let r = feld.reihe as f32;let unter_maus = self.maus_feld == Some(feld);let im_plan = self.spiel.hand.contains(&feld); let unter_maus = im_plan && self.maus_feld == Some(feld);
if im_plan { zeichne_faehnchen(frame, iso, farben, feld, hoehe); }if self.spiel.gebaut.contains(&feld) {
&& heißt „und“: Angehoben wird nur, wenn das Feld im Plan ist
und unter der Maus liegt. Auch der Mauszeiger soll nur dort zur Hand werden,
wo man wirklich bauen kann. Dafür nehmen wir wieder einen match-Zweig mit
Bedingung:
) -> mouse::Interaction {if self.maus_feld.is_some() { mouse::Interaction::Pointer } else { mouse::Interaction::default()match self.maus_feld { Some(feld) if self.spiel.hand.contains(&feld) => mouse::Interaction::Pointer, _ => mouse::Interaction::default(),} }
Fehlt noch die Farbe der Stange, in beiden Paletten:
licht: Color,stange: Color,}⋮licht: Color::from_rgb8(0xff, 0xd2, 0x7a),stange: Color::WHITE,} } fn nacht() -> Farben {⋮licht: Color::from_rgb8(0xff, 0xd2, 0x7a),stange: Color::from_rgb8(0xcf, 0xd3, 0xdc),}
cargo run
Sechs Fähnchen stecken auf dem Brett. Fahr mit der Maus darüber: Nur diese Parzellen heben sich, und nur dort wird der Zeiger zur Hand. Klick auf eine davon, und das Haus steht. Gleichzeitig taucht irgendwo ein neues Fähnchen auf, denn du hast ein Plättchen nachgezogen.Die Hand im Fenster
Fehlt noch der Überblick: Welche Plättchen hast du eigentlich, und wie viele liegen noch im
Stapel? Unten ins Fenster kommt deshalb eine Leiste mit sechs kleinen Plättchen zum Anklicken.
Ein Klick darauf verschickt denselben Brief wie ein Klick aufs Brett, nämlich
Message::Geklickt. Der Rest des Programms merkt gar nicht, woher der Brief
kam.
Die Leiste bauen wir in view anstelle der letzten beiden Zeilen:
let brett = canvas(self).width(Fill).height(Fill);// Die Kopfzeile liegt wie eine Folie über dem Brett stack![brett, kopfzeile].into()// 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 let folie = column![kopfzeile, space::vertical(), fussleiste]; stack![brett, folie].into()}
- Eine Reihe, die wächst. Wir beginnen mit einer
row!, in der nur die Beschriftung steht, und hängen dann in einer Schleife für jedes Plättchen einen Knopf an. Weilpushdie Reihe verändert, muss sielet mutsein.for &feld in &self.spiel.handgeht die Hand durch, ohne sie zu verändern, und packt jeden Verweis gleich zu einemFeldaus. - Knöpfe wie Plättchen. Der Stil ähnelt dem Tag/Nacht-Knopf, bekommt aber
den
statusausgewertet: Schwebt die Maus über dem Knopf (Hovered), wird er hellgrün, sonst hat er die Farbe der Bordsteine. So passen die Plättchen farblich zum Brett, bei Tag und bei Nacht. - Die Folie. Statt nur der Kopfzeile legen wir jetzt eine
column!übers Brett: oben die Kopfzeile, unten die Fußleiste und dazwischenspace::vertical(), ein dehnbarer Leerraum, der die beiden auseinander an die Fensterränder schiebt.
Für die Spalte brauchen wir noch column aus iced:
use iced::widget::canvas::{Frame, Path, Stroke, Text, gradient};use iced::widget::{button, canvas, row, space, stack, text};use iced::widget::{button, canvas, column, row, space, stack, text};
cargo run
Unten links liegen jetzt deine sechs Plättchen, unten rechts steht, wie viele noch im Stapel sind. Klick auf ein Plättchen in der Leiste: Auf dem Brett wächst das Haus, das Plättchen verschwindet, ein neues kommt nach, und der Stapel wird um eins kleiner.
Der komplette Code nach Etappe 2
Hier ist der vollständige Stand nach Etappe 2. 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, row, space, stack, text};
use iced::{
Background, Border, Color, Element, Fill, Font, Point, Rectangle, Renderer, Size, Theme,
alignment, font,
};
use spiel::{Feld, 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,
}
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,
}
}
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
let folie = column![kopfzeile, space::vertical(), fussleiste];
stack![brett, folie].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);
}
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));
}
}
fn zeichne_rohbau(
&self,
frame: &mut Frame,
iso: &Iso,
farben: &Farben,
feld: Feld,
boden: f32,
) {
let s = feld.spalte as f32;
let r = feld.reihe as f32;
let stockwerke = 3;
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: farben.beton,
};
// 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);
}
}
}
// 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(farben.beton, 0.85));
}
}
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
}
}
}
// 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,
}
}
// 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,
}
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,
}
}
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),
}
}
}
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 {
// 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)
}
}
// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
pub gebaut: Vec<Feld>,
pub stapel: Vec<Feld>,
pub hand: Vec<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 {
gebaut: Vec::new(),
stapel,
hand: Vec::new(),
};
spiel.nachziehen();
spiel
}
// 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 {
let Some(stelle) = self.hand.iter().position(|&h| h == feld) else {
return false;
};
self.hand.remove(stelle);
self.gebaut.push(feld);
self.nachziehen();
true
}
}
mod melden wir es an, mit pub öffnen wir,
was andere Dateien sehen dürfen. Die Spielregeln wohnen jetzt in
spiel.rs, getrennt von der Grafik. Mit rand mischen wir, mit
pop ziehen wir, und let … else lässt eine Funktion früh aussteigen,
wenn etwas nicht passt. Ein Klick in der Leiste und ein Klick aufs Brett schicken denselben
Brief: Die Oberfläche fragt, die Regeln entscheiden.Zum Ausprobieren
- Große Hand: Wie bekommst du acht statt sechs Plättchen auf die Hand? Passen alle noch in die Fußleiste?
- Buchhaltung: Zeig in der Fußleiste neben dem Stapel auch an, wie viele Häuser schon stehen, etwa „Gebaut: 5 · Im Stapel: 95“.
- Der erste Test: Weil die Regeln nichts von Fenstern wissen, kann man sie
ohne Fenster prüfen. Schreib einen Test, der sicherstellt, dass ein neues Spiel sechs
Plättchen auf der Hand und 102 im Stapel hat, und starte ihn mit
cargo test.
Lösungen anzeigen
1. Eine Zeile in spiel.rs:
pub const HANDGROESSE: usize = 8;. Die Leiste wächst mit, weil die Schleife in
view einfach alle Plättchen der Hand durchgeht. Bei 980 Pixeln Fensterbreite
wird es aber eng.
2. In view die Zeile für den Stapeltext
ändern:
let stapel = text(format!(
"Gebaut: {} · Im Stapel: {}",
self.spiel.gebaut.len(),
self.spiel.stapel.len()
))
Jedes {} im Text wird der Reihe nach durch einen der Werte dahinter
ersetzt.
3. Ans Ende von spiel.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neues_spiel_verteilt_sechs_plaettchen() {
let spiel = Spiel::neu();
assert_eq!(spiel.hand.len(), 6);
assert_eq!(spiel.stapel.len(), 102);
}
}
#[cfg(test)] bedeutet: Dieser Teil wird nur beim Testen übersetzt.
use super::* holt alles aus dem umgebenden Modul herein, und
assert_eq! bricht den Test ab, wenn die beiden Werte nicht gleich sind.
cargo test meldet dann test result: ok. 1 passed. Mehr über
Tests steht im Buch, Kapitel 88.
Etappe 2 geschafft 🚩
Die Regeln haben ein eigenes Zuhause, der Stapel ist gemischt, und gebaut wird nur noch mit Plättchen. In der nächsten Etappe passiert das, worum sich im Spiel alles dreht: Liegen Häuser nebeneinander, gründest du eine Hotelkette, und aus Rohbauten werden Hoteltürme mit eigenem Baustil.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren