├── README.md ├── session-01 ├── README.md ├── aftermath.jpeg └── connect_four │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ ├── lib.rs │ └── main.rs └── session-02 ├── README.md ├── aftermath.jpeg └── reversi ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src ├── lib.rs └── main.rs /README.md: -------------------------------------------------------------------------------- 1 | # Drink First Programming 2 | 3 | _Test First Programming_ ist ein alter Hut und kann heutzutage (_«Excuse me, wir 4 | haben 2024!?»_) als gängige technische Praktik angesehen werden. Eher umstritten 5 | (gerade bei Arbeitgebern und Ausbildungsanstalten) ist hingegen das sogenannte 6 | _Drink First Programming_, wobei das Schreiben von Code von der Einnahme eines 7 | alkoholischen Getränks bzw. mehrerer alkoholischer Getränke begleitet wird. 8 | 9 | Wie auch das _Test First Programming_ oft dahingehend falsch verstanden wird, 10 | dass man zuerst _alle_ Tests schreiben und sich erst dann mit dem Produktivcode 11 | befassen solle, könnte dieses Missverständnis auch die Haltung zum _Drink First 12 | Programming_ von negativen Vorurteilen überschatten: Es soll nicht zuerst 13 | ausgedehnt getrunken und erst dann im betrunkenen Zustand programmiert, sondern 14 | zuerst ein Schluck genommen und dann ein Stück Code geschrieben werden. 15 | 16 | Dieses Repository soll ein wissenschaftliches Experiment zur umstrittenen 17 | technischen Praktik des _Drink First Programmings_ dokumentieren, wobei die 18 | Frage untersucht werden soll, welchen Einfluss der begleitende Genuss von 19 | Alkohol auf das Schreiben von Programmcode habe. Hierzu einige 20 | Rahmenbedingungen: 21 | 22 | 1. Die Session soll auf Video aufgezeichnet werden, wobei das Schreiben des 23 | Codes via Screencast und die Zusichnahme von Alkohol per Webcam dokumentiert 24 | werden soll. 25 | 2. Der Vorgang wird ausführlich kommentiert, was in _Standardsprache_ 26 | (Hochdeutsch) geschehen soll. Das hat zwei Vorteile: Einerseits kann der 27 | Abfall der sprachlichen Fähigkeiten mitaufgezeichnet werden, andererseits 28 | kann die Verständlichkeit der Äusserungen mithilfe der automatischen 29 | Untertitelfunktion von YouTube objektiv beurteilt werden. 30 | 3. Beim Programmieren sollen keine fortegschrittenen Hilfsmittel wie Language 31 | Server, KI-Assistenten oder mächtige Entwicklungsumgebungen zum Einsatz 32 | kommen, sondern nur der Texteditor Vim ohne jegliche Plugins. Zu 33 | Suchmaschinen soll nur im äussersten Notfall gegriffen werden, ansonsten 34 | dient die offizielle Dokumentation der Programmiersprache und der verwendeten 35 | Libraries als einzige Hilfestellung. 36 | 4. Der Code soll im vorliegenden Repository festgehalten werden. Die beim 37 | Schreiben dieses Codes konsumierten Getränke und wahrgenommenen 38 | Geisteszustände werden in einem Protokoll ebenfalls in diesem Repository 39 | dokumentiert. 40 | 5. Die Programmiersprache und das darin zu lösende Problem sind so auszuwählen, 41 | dass einerseits schon beide bekannt aber in dieser Kombination noch nie 42 | verwendet worden sind. Es handelt sich somit um eine Transferaufgabe, die 43 | auch im nüchternen Zustand schon gewisse Schwierigkeiten bietet, ein 44 | Scheitern aufgrund des inhärenten Schwierigkeitsgrads aber mit hoher 45 | Wahrscheinlichkeit ausgeschlossen werden kann. 46 | 47 | Die Studie ist im Sinne der Wissenschaftlichkeit ergebnisoffen ausgestaltet und 48 | wird nicht teleologisch motiviert durchgeführt: Dem geneigten Betrachter soll 49 | weder von der technischen Praktik des _Drink First Programming_ abgeraten noch 50 | soll diesem das Vorgehen anempfohlen werden. Stattdessen soll jeder anhand des 51 | Dargebotenen selber entscheiden, ob der begleitende Konsum alkoholischer 52 | Getränke den Programmierfähigkeiten und dem Gemütszustand förderlich oder 53 | abträglich ist ‒ und welche Zielgrösse (Kompetenz, Heiterkeit) wie stark 54 | gewichtet werden soll. 55 | 56 | ## Sessions 57 | 58 | - [Session 01](session-01/): [YouTube](https://www.youtube.com/playlist?list=PLux6j39XOCC4AyKQpzlWTzK73PbjX1c1K) 59 | - [Session 02](session-02/): [YouTube](https://www.youtube.com/playlist?list=PLux6j39XOCC4YHp6Ql__Je0j2xZPifRw7) 60 | -------------------------------------------------------------------------------- /session-01/README.md: -------------------------------------------------------------------------------- 1 | # Aufgabenstellung 2 | 3 | Implementiere das Spiel _Vier Gewinnt_ für zwei menschliche Spieler als 4 | Kommandozeilenanwendung in Rust. Die Spiellogik soll als Library umgesetzt und 5 | mithilfe von Unittests geprüft werden. Der interaktive Teil kann manuell 6 | getestet werden. Das Spiel soll auf einem Gitter von sieben (Breite) mal sechs 7 | (Höhe) gespielt werden können. Der Spieler, der zuerst vier seiner Steine (`x` 8 | und `o`) in eine ununterbrochene horizontale, vertikale oder diagonale Reihe 9 | bringen kann, gewinnt das Spiel. Ist das Gitter komplett gefüllt, ohne dass 10 | eine der Siegbedingungen eingetreten ist, wird das Spiel als Unentschieden 11 | gewertet. 12 | 13 | # Protokoll 14 | 15 | 26.06.2024 16 | 17 | - Startzeit: 19:25 18 | - Getränke: 19 | - 6 * Luzerner Bier Légère (1.98 Liter à 2.9%) 20 | - Ereignisse und Erlebnisse: 21 | - ~20:00 Uhr: Erleuchtung wegen "rückläufigen Ranges": Der Compiler kann dies nicht prüfen! (Loop wurde nicht betreten) 22 | - man soll die Methode `rev()` von `Range` verwenden 23 | - ~20:08 Uhr: erste Pinkelpause (leichter Schwindel beim Aufstehen) 24 | - 20:14 Uhr: erstes Rülpsen 25 | - 20:35 Uhr: ranges mit exklusiven Obergrenzen werden beim Pattern Matching nur experimentell unterstütz 26 | - Die Compilermeldung wird mit Erstaunen und Entsetzen aufgenommen. Wut und Trauer machen sich breit. 27 | - 20:47 Uhr: Das DDD beeinträchtigt die Disziplin beim TDD. 28 | - ~20:50 Uhr: zweite Pinkelpause 29 | - 20:54 Uhr: Nachschenken 30 | - 20:57: zweites Rülpsen 31 | - 21:19: drittes Rülpsen 32 | - 21:30: Pinkelpause 33 | - 21:49: Unit Tests works, Integration Test fails 34 | - 21:51: alles funktioniert soweit 35 | 36 | ![Das Nebenprodukt der ersten Session](aftermath.jpeg) 37 | 38 | # Review 39 | 40 | 27.06.2024 (der Morgen danach) 41 | 42 | Eine kritische und _nüchterne_ Betrachtung des Dargebotenen führt folgendes ans 43 | Licht: 44 | 45 | 1. Der Fehler bei der Siegauswertung, dessen Analyse und Behebung ca. eine 46 | halbe Stunde in Anspruch genommen hat, hätte schneller und einfacher behoben 47 | werden können: 48 | - Die API hätte von Anfang konsequent mit 0-basierenden Indizes arbeiten 49 | sollen, also von 0 bis 6 statt von 1 bis 7. Zwar hätte man an einer 50 | Stelle auf Pattern Matching zugunsten einer `if`-Abfrage verzichten 51 | müssen, ansonsten wäre der Code aber klarer geworden. Bei der Anzeige 52 | hätte man weiterhin von 1 bis 7 durchnummerieren können. Die Umrechnung 53 | wäre aber in der Verantwortlichkeit des Client-Codes gewesen. 54 | - Das Inkrement/Dekrement der Schleifenvariablen `i` und `j` hätte wohl 55 | besser einmalig nach der Zuweisung von `r` und `c` erfolgen sollen, und 56 | dann nicht zu Beginn sondern am Ende beim Schleifendurchlauf. Der Code 57 | wäre dadurch klarer geworden. 58 | - Hätte ich die Ausgaben des Compilers und die Fehlermeldungen (panics) 59 | genauer angeschaut, wäre ich dem Fehler früher auf die Spur gekommen. 60 | Meine schlechte Angewohnheit wurde durch den Alkohol noch verstärkt. 61 | 2. Macht man Vim im Hintergrund aktiv, um Commits durchzuführen, muss man 62 | darauf achten, keine Swap-Files ins Repository aufzunehmen. Auch hier hätte 63 | ich die Ausgabe von `git status` genauer anschauen sollen, denn bei einem 64 | späteren Commit wurden diese Dateien gelöscht. (Ich hatte wohl vergessen, 65 | den Commit zu pushen.) 66 | 3. Sogar unter Windows kann man mit einem reinen aber mächtigen Texteditor wie 67 | Vim dank Cargo gut Rust programmieren. 68 | 4. Das Unentschieden hätte recht einfach geprüft und implementiert werden 69 | können, indem man die Felder mit dem Wert `EMPTY` zählt. Das Testen wäre 70 | aber etwas mühsam geworden und hätte dem Zuschauer auch kaum einen Mehrwert 71 | geboten. 72 | -------------------------------------------------------------------------------- /session-01/aftermath.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickbucher/drink-first-programming/f03ab6378b209c15f2bef08850e02a6598c688f9/session-01/aftermath.jpeg -------------------------------------------------------------------------------- /session-01/connect_four/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.swp 3 | -------------------------------------------------------------------------------- /session-01/connect_four/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "akshually" 7 | version = "0.2.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0575aaedadad1bcd31042ec19a18bb7aaeaf2e8db8c2cf6944886a1e14007d74" 10 | 11 | [[package]] 12 | name = "connect_four" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "akshually", 16 | ] 17 | -------------------------------------------------------------------------------- /session-01/connect_four/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "connect_four" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | akshually = "0.2.3" 8 | -------------------------------------------------------------------------------- /session-01/connect_four/src/lib.rs: -------------------------------------------------------------------------------- 1 | const CONNECT_N: usize = 4; 2 | const WIDTH: usize = 7; 3 | const HEIGHT: usize = 6; 4 | 5 | const EMPTY: char = '_'; 6 | pub const PLAYER_ONE: char = 'x'; 7 | pub const PLAYER_TWO: char = 'o'; 8 | 9 | #[derive(Debug)] 10 | pub enum Outcome { 11 | Win(char), 12 | Draw, 13 | Nothing { row: usize, col: usize }, 14 | } 15 | 16 | pub struct Board { 17 | fields: Vec>, 18 | } 19 | 20 | impl Board { 21 | pub fn new() -> Self { 22 | let mut fields = Vec::new(); 23 | for r in 0..HEIGHT { 24 | let mut row = Vec::new(); 25 | for c in 0..WIDTH { 26 | row.push(EMPTY); 27 | } 28 | fields.push(row); 29 | } 30 | Board { fields } 31 | } 32 | 33 | pub fn from(fields: Vec>) -> Self { 34 | Board { fields } 35 | } 36 | 37 | pub fn render(&self) -> String { 38 | let mut output = String::new(); 39 | for i in 1..=WIDTH { 40 | output.push_str(&format!("{i} ")); 41 | } 42 | output.push('\n'); 43 | for row in &self.fields { 44 | for col in row { 45 | output.push_str(&format!("{col} ")); 46 | } 47 | output.push('\n'); 48 | } 49 | output 50 | } 51 | 52 | pub fn drop_stone(&mut self, stone: char, slot: usize) -> Result { 53 | if stone != PLAYER_ONE && stone != PLAYER_TWO { 54 | return Err(format!("{stone} is not an allowed stone")); 55 | } 56 | match slot { 57 | 1..=WIDTH => { 58 | let slot_index = slot - 1; 59 | for r in (0..HEIGHT).rev() { 60 | let mut row = &mut self.fields[r]; 61 | if row[slot_index] == EMPTY { 62 | row[slot_index] = stone; 63 | if self.is_horizontal_win(r, stone) 64 | || self.is_vertical_win(slot_index, stone) 65 | || self.is_diagonal_win(r, slot_index, stone) 66 | { 67 | return Ok(Outcome::Win(stone)); 68 | } 69 | return Ok(Outcome::Nothing { 70 | row: r, 71 | col: slot_index, 72 | }); 73 | } 74 | } 75 | Err(format!("slot {slot} is already full")) 76 | } 77 | _ => Err(format!("slot {slot} is out of range [{};{}]", 1, WIDTH)), 78 | } 79 | } 80 | 81 | fn is_horizontal_win(&self, r: usize, p: char) -> bool { 82 | let mut matches = 0; 83 | let row = &self.fields[r]; 84 | println!("{:?}", row); 85 | for f in row { 86 | if f == &p { 87 | matches += 1; 88 | if matches == CONNECT_N { 89 | return true; 90 | } 91 | } else { 92 | matches = 0; 93 | } 94 | } 95 | matches == CONNECT_N 96 | } 97 | 98 | fn is_vertical_win(&self, c: usize, p: char) -> bool { 99 | let mut matches = 0; 100 | for row in &self.fields { 101 | if row[c] == p { 102 | matches += 1; 103 | if matches == CONNECT_N { 104 | return true; 105 | } 106 | } else { 107 | matches = 0; 108 | } 109 | } 110 | matches == CONNECT_N 111 | } 112 | 113 | fn is_diagonal_win(&self, r: usize, c: usize, p: char) -> bool { 114 | if self.fields[r][c] != p { 115 | return false; 116 | } 117 | let mut matches = 1; 118 | 119 | // falling top/left 120 | let (mut i, mut j) = (r, c); 121 | while i > 0 && j > 0 { 122 | i -= 1; 123 | j -= 1; 124 | if self.fields[i][j] == p { 125 | matches += 1; 126 | } else { 127 | break; 128 | } 129 | } 130 | println!("matches: {matches}"); 131 | 132 | // falling bottom/right 133 | let (mut i, mut j) = (r, c); 134 | while i < HEIGHT-1 && j < WIDTH-1 { 135 | i += 1; 136 | j += 1; 137 | if self.fields[i][j] == p { 138 | matches += 1; 139 | } else { 140 | break; 141 | } 142 | } 143 | 144 | if matches >= CONNECT_N { 145 | println!("matches: {matches}"); 146 | return true; 147 | } 148 | 149 | let mut matches = 1; 150 | 151 | // rising top/right 152 | let (mut i, mut j) = (r, c); 153 | while i > 0 && j < WIDTH-1 { 154 | i -= 1; 155 | j += 1; 156 | if self.fields[i][j] == p { 157 | matches += 1; 158 | } else { 159 | break; 160 | } 161 | } 162 | 163 | // rising bottom/left 164 | let (mut i, mut j) = (r, c); 165 | while i < HEIGHT-1 && j > 0 { 166 | i += 1; 167 | j -= 1; 168 | if self.fields[i][j] == p { 169 | matches += 1; 170 | } else { 171 | break; 172 | } 173 | } 174 | 175 | return matches >= CONNECT_N; 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | use super::*; 182 | 183 | #[test] 184 | fn test_initially_all_fields_empty() { 185 | let board = Board::new(); 186 | for row in board.fields { 187 | for field in row { 188 | assert_eq!(field, EMPTY); 189 | } 190 | } 191 | } 192 | 193 | #[test] 194 | fn test_number_of_fields() { 195 | let board = Board::new(); 196 | assert_eq!(board.fields.len(), HEIGHT); 197 | for row in board.fields { 198 | assert_eq!(row.len(), WIDTH); 199 | } 200 | } 201 | 202 | #[test] 203 | fn test_horizontal_not_win() { 204 | let board = Board::new(); 205 | let actual = board.is_horizontal_win(0, PLAYER_ONE); 206 | assert_eq!(actual, false); 207 | } 208 | 209 | #[test] 210 | fn test_horizontal_win() { 211 | let mut board = Board::new(); 212 | board.drop_stone(PLAYER_ONE, 1); 213 | board.drop_stone(PLAYER_ONE, 2); 214 | board.drop_stone(PLAYER_ONE, 3); 215 | board.drop_stone(PLAYER_ONE, 4); 216 | let actual = board.is_horizontal_win(HEIGHT - 1, PLAYER_ONE); 217 | assert_eq!(actual, true); 218 | } 219 | 220 | #[test] 221 | fn test_vertical_not_win() { 222 | let board = Board::new(); 223 | let actual = board.is_vertical_win(0, PLAYER_TWO); 224 | assert_eq!(actual, false); 225 | } 226 | 227 | #[test] 228 | fn test_vertical_win() { 229 | let mut board = Board::new(); 230 | board.drop_stone(PLAYER_TWO, 3); 231 | board.drop_stone(PLAYER_TWO, 3); 232 | board.drop_stone(PLAYER_TWO, 3); 233 | board.drop_stone(PLAYER_TWO, 3); 234 | let actual = board.is_vertical_win(2, PLAYER_TWO); 235 | assert_eq!(actual, true); 236 | } 237 | 238 | #[test] 239 | fn test_diagonal_not_win() { 240 | let board = Board::new(); 241 | let actual = board.is_diagonal_win(3, 4, PLAYER_ONE); 242 | assert_eq!(actual, false); 243 | } 244 | 245 | #[test] 246 | fn test_diagonal_win_falling() { 247 | let fields = vec![ 248 | vec!['_', 'x', '_', '_', '_', '_', '_'], 249 | vec!['_', '_', 'o', '_', '_', '_', '_'], 250 | vec!['_', '_', '_', 'o', '_', '_', '_'], 251 | vec!['_', '_', '_', '_', 'o', '_', '_'], 252 | vec!['_', '_', '_', '_', '_', 'o', '_'], 253 | vec!['_', '_', '_', '_', '_', '_', 'x'], 254 | ]; 255 | let board = Board::from(fields); 256 | let actual = board.is_diagonal_win(2, 3, PLAYER_TWO); 257 | assert_eq!(actual, true); 258 | } 259 | 260 | #[test] 261 | fn test_diagonal_win_rising() { 262 | let fields = vec![ 263 | vec!['_', '_', '_', '_', '_', 'o', '_'], 264 | vec!['_', '_', '_', '_', 'x', '_', '_'], 265 | vec!['_', '_', '_', 'x', '_', '_', '_'], 266 | vec!['_', '_', 'x', '_', '_', '_', '_'], 267 | vec!['_', 'x', '_', '_', '_', '_', '_'], 268 | vec!['o', '_', '_', '_', '_', '_', '_'], 269 | ]; 270 | let board = Board::from(fields); 271 | let actual = board.is_diagonal_win(4, 1, PLAYER_ONE); 272 | assert_eq!(actual, true); 273 | } 274 | 275 | #[test] 276 | fn test_diagonal_win_falling_left() { 277 | let fields = vec![ 278 | vec!['_', '_', '_', '_', '_', '_', '_'], 279 | vec!['_', '_', '_', '_', '_', '_', '_'], 280 | vec!['x', '_', '_', '_', '_', '_', '_'], 281 | vec!['o', 'x', '_', '_', '_', '_', '_'], 282 | vec!['x', 'x', 'x', 'o', '_', '_', '_'], 283 | vec!['o', 'o', 'o', 'x', '_', '_', '_'], 284 | ]; 285 | let board = Board::from(fields); 286 | let actual = board.is_diagonal_win(2, 0, PLAYER_ONE); 287 | assert_eq!(actual, true); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /session-01/connect_four/src/main.rs: -------------------------------------------------------------------------------- 1 | use akshually::io::prompt_line; 2 | use connect_four::{Board, Outcome, PLAYER_ONE, PLAYER_TWO}; 3 | 4 | fn main() { 5 | let mut board = Board::new(); 6 | let mut player = PLAYER_TWO; 7 | println!("{}", board.render()); 8 | loop { 9 | if player == PLAYER_ONE { 10 | player = PLAYER_TWO; 11 | } else { 12 | player = PLAYER_ONE; 13 | } 14 | let slot: usize = prompt_line(&format!("Player {player}, enter a slot: ")).unwrap(); 15 | match board.drop_stone(player, slot) { 16 | Ok(outcome) => match outcome { 17 | Outcome::Win(_) => { 18 | println!("{}", board.render()); 19 | println!("Player {player}, a winner is you!"); 20 | break; 21 | } 22 | _ => { 23 | println!("{}", board.render()); 24 | } 25 | }, 26 | Err(err) => { 27 | println!("{err}"); 28 | continue; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /session-02/README.md: -------------------------------------------------------------------------------- 1 | # Aufgabenstellung 2 | 3 | Implementiere das Spiel _Reversi_ für zwei menschliche Spieler als 4 | Kommandozeilenanwendung in Rust. Die Spiellogik soll als Library umgesetzt und 5 | mithilfe von Unittests geprüft werden. Der interaktive Teil kann manuell 6 | getestet werden. Das Spiel soll auf einem Gitter von acht (Breite) mal acht 7 | (Höhe) gespielt werden können. Die Spielregeln können auf 8 | [Wikipedia](https://de.wikipedia.org/wiki/Othello_(Spiel)) nachgelesen werden. 9 | 10 | # Protokoll 11 | 12 | 11.07.2024 13 | 14 | - Startzeit: ca. 18:04 Uhr 15 | - Getränke: 16 | - 6 * Luzerner Bier Original (1.98 l) 17 | - Ereignisse und Erlebnisse: 18 | - 18:19 Uhr: Schluckauf 19 | - 19:45 Uhr: Toilettengang 20 | - 19:51 Uhr: Feststellung erster Schwindelgefühle 21 | - 19:57 Uhr: erste Maß geleert 22 | - 20:00 Uhr: nachgeschenkt 23 | - 20:30 Uhr: erste spielbare Version fertig 24 | - 20:46 Uhr: Feststellung der offenen Punkte und starker Harndrang 25 | - 20:49 Uhr: Feedback des Bieres in Form eines Rülpsers 26 | 27 | ![Das Nebenprodukt der zweiten Session](aftermath.jpeg) 28 | 29 | # Review 30 | 31 | 12.07.2024 (der Morgen danach) 32 | 33 | Erneut soll die gestrige Leistung _nüchtern_ und kritisch betrachtet werden. 34 | Dabei sollen auch beim Experiment gemachte Erkenntnisse festgehalten werden: 35 | 36 | 1. Arbeitet man mit Indizes (`usize`) und relativen Verschiebungen dazu 37 | (`isize`), kommt man nicht um explizite Typkonvertierungen herum. Dies liegt 38 | daran, dass Indizes nur positive Zahlen inkl. 0 sein können, während 39 | relative Verschiebungen positiv oder negativ sein können. 40 | 2. Das Pattern Matching auf eine bereits initialisierte Variable funktioniert 41 | nicht so wie ursprünglich von mir gedacht. In diesem Fall muss man auf 42 | `if`/`else` ausweichen, was aber bei Rust auch wie ein Ausdruck verwendet 43 | werde kann. 44 | 3. Die Darstellung (mit den Feldwerten `.`, `X` und `O`) sollte besser komplett 45 | von der internen Repräsentation getrennt werden. Zur Testbarkeit wären 46 | numerische Feldwerte übersichtlicher als solche vom Typ `char`, was die 47 | Umsetzung eines Übersetzungsmechanismus in der Form der `from`-Methode nötig 48 | gemacht hat. 49 | 4. Das Programmieren ging auch unter dem Einfluss von einigen Bier noch recht 50 | gut, beim Testen des Spiels sind mir aber ein paar dumme Missgeschicke 51 | passiert, wodurch ich den Testlauf zweimal unnötigerweise abgebrochen habe. 52 | Der Alkohol wirkt offenbar sehr stark auf die Wahrnehmung, während das 53 | Wiedergeben von Code weniger darunter leidet. 54 | 5. Die Redundanz beim Validierungs- und Umdrehungscode wurde nicht nur in der 55 | Implementierung, sondern auch von der Clientseite her gut sichtbar. Diesen 56 | Code zu vereinheitlichen habe ich mir nicht mehr zugetraut. Es wäre wohl 57 | sinnvoll, die beiden Schritte zu einer Operation zusammenzufügen, wobei der 58 | Rückgabewert dieser Operation sogleich über Erfolg und Misserfolg (invalider 59 | Spielzug) informieren müsste. 60 | 6. Nach dieser zweiten Session war ich weniger erschöpft als nach der ersten, 61 | obwohl die Aufgabenstellung schwieriger, das Bier stärker und die Session 62 | länger waren. Vielleicht hat sich hier ein gewisser Gewöhnungseffekt 63 | eingestellt. (Nach meinem ersten Unterrichtstag war ich damals im Sommer 64 | 2021 heiser; mittlerweile haben sich meine Stimmbänder offenbar an die 65 | höhere Belastung im Schulzimmer gewöhnt.) 66 | -------------------------------------------------------------------------------- /session-02/aftermath.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickbucher/drink-first-programming/f03ab6378b209c15f2bef08850e02a6598c688f9/session-02/aftermath.jpeg -------------------------------------------------------------------------------- /session-02/reversi/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | */*.swp 3 | -------------------------------------------------------------------------------- /session-02/reversi/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "akshually" 7 | version = "0.2.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0575aaedadad1bcd31042ec19a18bb7aaeaf2e8db8c2cf6944886a1e14007d74" 10 | 11 | [[package]] 12 | name = "reversi" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "akshually", 16 | ] 17 | -------------------------------------------------------------------------------- /session-02/reversi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reversi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | akshually = "0.2.3" 8 | -------------------------------------------------------------------------------- /session-02/reversi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | const EMPTY: char = '.'; 4 | pub const BLACK: char = 'X'; 5 | pub const WHITE: char = 'O'; 6 | pub const SIDES: usize = 8; 7 | 8 | #[derive(Debug)] 9 | pub struct Board { 10 | fields: Vec>, 11 | } 12 | 13 | impl Board { 14 | pub fn new() -> Self { 15 | let mut rows: Vec> = Vec::new(); 16 | for i in 0..SIDES { 17 | let mut cols: Vec = Vec::new(); 18 | for j in 0..SIDES { 19 | match (i, j) { 20 | (3, 3) | (4, 4) => cols.push(WHITE), 21 | (3, 4) | (4, 3) => cols.push(BLACK), 22 | _ => cols.push(EMPTY), 23 | } 24 | } 25 | rows.push(cols); 26 | } 27 | Board { fields: rows } 28 | } 29 | 30 | pub fn from(fields: &Vec>, (e, b, w): (u8, u8, u8)) -> Self { 31 | let mut chars: Vec> = Vec::new(); 32 | for i in 0..fields.len() { 33 | let mut row: Vec = Vec::new(); 34 | for j in 0..fields[i].len() { 35 | let field = fields[i][j]; 36 | let chr: char = if field == e { 37 | EMPTY 38 | } else if field == b { 39 | BLACK 40 | } else if field == w { 41 | WHITE 42 | } else { 43 | EMPTY 44 | }; 45 | row.push(chr); 46 | } 47 | chars.push(row); 48 | } 49 | Board { fields: chars } 50 | } 51 | 52 | pub fn is_valid_move(&self, row: usize, col: usize, player: char) -> bool { 53 | let field = self.fields[row][col]; 54 | if field != EMPTY { 55 | return false; 56 | } 57 | let directions: &[(isize, isize)] = &[ 58 | (-1, 0), 59 | (-1, 1), 60 | (0, 1), 61 | (1, 1), 62 | (1, 0), 63 | (1, -1), 64 | (0, -1), 65 | (-1, -1), 66 | ]; 67 | let opponent = match player { 68 | BLACK => WHITE, 69 | WHITE => BLACK, 70 | _ => return false, 71 | }; 72 | for (rd, cd) in directions { 73 | let mut has_visited_opponent = false; 74 | let mut r = row as isize + rd; 75 | let mut c = col as isize + cd; 76 | let sides = SIDES as isize; 77 | while r >= 0 && r < sides && c >= 0 && c < sides { 78 | let field = self.fields[r as usize][c as usize]; 79 | if field == player && has_visited_opponent { 80 | return true; 81 | } else if field == opponent { 82 | has_visited_opponent = true; 83 | r += rd; 84 | c += cd; 85 | } else { 86 | break; 87 | } 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | pub fn apply_move(&mut self, row: usize, col: usize, player: char) { 94 | if !self.is_valid_move(row, col, player) { 95 | panic!("invalid move {row} {col} {player}"); 96 | } 97 | let directions: &[(isize, isize)] = &[ 98 | (-1, 0), 99 | (-1, 1), 100 | (0, 1), 101 | (1, 1), 102 | (1, 0), 103 | (1, -1), 104 | (0, -1), 105 | (-1, -1), 106 | ]; 107 | let opponent = match player { 108 | BLACK => WHITE, 109 | WHITE => BLACK, 110 | _ => panic!("invalid player {player}"), 111 | }; 112 | for (rd, cd) in directions { 113 | let mut opponent_trail: Vec<(usize, usize)> = Vec::new(); 114 | let mut r = row as isize + rd; 115 | let mut c = col as isize + cd; 116 | let sides = SIDES as isize; 117 | while r >= 0 && r < sides && c >= 0 && c < sides { 118 | let field = self.fields[r as usize][c as usize]; 119 | if field == opponent { 120 | opponent_trail.push((r as usize, c as usize)); 121 | r += rd; 122 | c += cd; 123 | } else if field == player && !opponent_trail.is_empty() { 124 | self.fields[row][col] = player; 125 | for (tr, tc) in &opponent_trail { 126 | self.fields[*tr][*tc] = player; 127 | } 128 | break; 129 | } else { 130 | break; 131 | } 132 | } 133 | } 134 | } 135 | 136 | pub fn get_standings(&self) -> (usize, usize) { 137 | let mut black = 0; 138 | let mut white = 0; 139 | for row in &self.fields { 140 | for field in row { 141 | match *field { 142 | BLACK => black += 1, 143 | WHITE => white += 1, 144 | _ => {} 145 | } 146 | } 147 | } 148 | (black, white) 149 | } 150 | } 151 | 152 | impl Display for Board { 153 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 154 | write!(f, " ")?; 155 | for i in 0..self.fields.len() { 156 | write!(f, "{i} ")?; 157 | } 158 | write!(f, "\n")?; 159 | for i in 0..self.fields.len() { 160 | let row = &self.fields[i]; 161 | let r = (('a' as u8) + (i as u8)) as char; 162 | write!(f, "{r} ")?; 163 | for j in 0..row.len() { 164 | let col = row[j]; 165 | write!(f, "{col} ")?; 166 | } 167 | write!(f, "\n")?; 168 | } 169 | Ok(()) 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod tests { 175 | use super::*; 176 | 177 | #[test] 178 | fn init_board() { 179 | let board = Board::new(); 180 | assert_eq!(board.fields.len(), SIDES); 181 | for i in 0..SIDES { 182 | let row = &board.fields[i]; 183 | assert_eq!(row.len(), SIDES); 184 | for j in 0..SIDES { 185 | let col = row[j]; 186 | match (i, j) { 187 | (3, 3) | (4, 4) => assert_eq!(col, WHITE), 188 | (3, 4) | (4, 3) => assert_eq!(col, BLACK), 189 | _ => assert_eq!(col, EMPTY), 190 | } 191 | } 192 | } 193 | } 194 | 195 | #[test] 196 | fn init_board_from() { 197 | let fields = vec![ 198 | vec![0, 0, 0, 0, 0, 0, 0, 0], 199 | vec![0, 0, 0, 0, 0, 0, 0, 0], 200 | vec![0, 0, 0, 0, 0, 0, 0, 0], 201 | vec![0, 0, 0, 2, 1, 0, 0, 0], 202 | vec![0, 0, 0, 1, 2, 0, 0, 0], 203 | vec![0, 0, 0, 0, 0, 0, 0, 0], 204 | vec![0, 0, 0, 0, 0, 0, 0, 0], 205 | vec![0, 0, 0, 0, 0, 0, 0, 0], 206 | ]; 207 | let board = Board::from(&fields, (0, 1, 2)); 208 | assert_eq!(board.fields[3][3], WHITE); 209 | assert_eq!(board.fields[4][4], WHITE); 210 | assert_eq!(board.fields[3][4], BLACK); 211 | assert_eq!(board.fields[4][3], BLACK); 212 | } 213 | 214 | #[test] 215 | fn validate_first_move() { 216 | let fields = vec![ 217 | vec![0, 0, 0, 0, 0, 0, 0, 0], 218 | vec![0, 0, 0, 0, 0, 0, 0, 0], 219 | vec![0, 0, 0, 0, 0, 0, 0, 0], 220 | vec![0, 0, 0, 2, 1, 0, 0, 0], 221 | vec![0, 0, 0, 1, 2, 0, 0, 0], 222 | vec![0, 0, 0, 0, 0, 0, 0, 0], 223 | vec![0, 0, 0, 0, 0, 0, 0, 0], 224 | vec![0, 0, 0, 0, 0, 0, 0, 0], 225 | ]; 226 | let board = Board::from(&fields, (0, 1, 2)); 227 | assert_eq!(board.is_valid_move(0, 0, BLACK), false); 228 | assert_eq!(board.is_valid_move(4, 4, BLACK), false); 229 | assert_eq!(board.is_valid_move(2, 2, BLACK), false); 230 | assert_eq!(board.is_valid_move(2, 3, BLACK), true); 231 | assert_eq!(board.is_valid_move(2, 4, BLACK), false); 232 | assert_eq!(board.is_valid_move(2, 4, WHITE), true); 233 | assert_eq!(board.is_valid_move(2, 5, BLACK), false); 234 | assert_eq!(board.is_valid_move(2, 5, WHITE), false); 235 | assert_eq!(board.is_valid_move(3, 5, BLACK), false); 236 | assert_eq!(board.is_valid_move(3, 5, WHITE), true); 237 | assert_eq!(board.is_valid_move(4, 5, BLACK), true); 238 | assert_eq!(board.is_valid_move(4, 5, WHITE), false); 239 | assert_eq!(board.is_valid_move(5, 5, BLACK), false); 240 | assert_eq!(board.is_valid_move(5, 5, WHITE), false); 241 | assert_eq!(board.is_valid_move(5, 4, BLACK), true); 242 | assert_eq!(board.is_valid_move(5, 4, WHITE), false); 243 | assert_eq!(board.is_valid_move(5, 3, BLACK), false); 244 | assert_eq!(board.is_valid_move(5, 3, WHITE), true); 245 | assert_eq!(board.is_valid_move(5, 2, BLACK), false); 246 | assert_eq!(board.is_valid_move(5, 2, WHITE), false); 247 | assert_eq!(board.is_valid_move(4, 2, BLACK), false); 248 | assert_eq!(board.is_valid_move(4, 2, WHITE), true); 249 | assert_eq!(board.is_valid_move(3, 2, BLACK), true); 250 | assert_eq!(board.is_valid_move(3, 2, WHITE), false); 251 | } 252 | 253 | #[test] 254 | fn validate_later_move() { 255 | let fields = vec![ 256 | vec![0, 0, 0, 0, 0, 0, 0, 0], 257 | vec![0, 2, 0, 0, 0, 0, 0, 0], 258 | vec![0, 1, 2, 1, 1, 1, 0, 0], 259 | vec![0, 0, 0, 2, 1, 0, 0, 0], 260 | vec![0, 0, 0, 1, 2, 0, 0, 0], 261 | vec![0, 0, 0, 0, 0, 0, 0, 0], 262 | vec![0, 0, 0, 0, 0, 0, 0, 0], 263 | vec![0, 0, 0, 0, 0, 0, 0, 0], 264 | ]; 265 | let board = Board::from(&fields, (0, 1, 2)); 266 | assert_eq!(board.is_valid_move(0, 0, BLACK), false); 267 | assert_eq!(board.is_valid_move(5, 4, BLACK), true); 268 | } 269 | 270 | #[test] 271 | fn apply_first_move() { 272 | let before = vec![ 273 | vec![0, 0, 0, 0, 0, 0, 0, 0], 274 | vec![0, 0, 0, 0, 0, 0, 0, 0], 275 | vec![0, 0, 0, 0, 0, 0, 0, 0], 276 | vec![0, 0, 0, 2, 1, 0, 0, 0], 277 | vec![0, 0, 0, 1, 2, 0, 0, 0], 278 | vec![0, 0, 0, 0, 0, 0, 0, 0], 279 | vec![0, 0, 0, 0, 0, 0, 0, 0], 280 | vec![0, 0, 0, 0, 0, 0, 0, 0], 281 | ]; 282 | let (row, col, player) = (2, 3, BLACK); 283 | let after = vec![ 284 | vec![0, 0, 0, 0, 0, 0, 0, 0], 285 | vec![0, 0, 0, 0, 0, 0, 0, 0], 286 | vec![0, 0, 0, 1, 0, 0, 0, 0], 287 | vec![0, 0, 0, 1, 1, 0, 0, 0], 288 | vec![0, 0, 0, 1, 2, 0, 0, 0], 289 | vec![0, 0, 0, 0, 0, 0, 0, 0], 290 | vec![0, 0, 0, 0, 0, 0, 0, 0], 291 | vec![0, 0, 0, 0, 0, 0, 0, 0], 292 | ]; 293 | let mut board = Board::from(&before, (0, 1, 2)); 294 | board.apply_move(row, col, player); 295 | let expected = Board::from(&after, (0, 1, 2)); 296 | assert_eq!(board.fields, expected.fields); 297 | } 298 | 299 | #[test] 300 | fn apply_later_move() { 301 | let before = vec![ 302 | vec![0, 0, 0, 0, 0, 0, 0, 0], 303 | vec![0, 1, 1, 1, 1, 1, 1, 0], 304 | vec![0, 1, 2, 2, 2, 2, 1, 0], 305 | vec![0, 1, 2, 2, 2, 2, 1, 0], 306 | vec![0, 1, 2, 0, 0, 2, 1, 0], 307 | vec![0, 1, 2, 2, 2, 2, 1, 0], 308 | vec![0, 1, 1, 1, 1, 1, 1, 0], 309 | vec![0, 0, 0, 0, 0, 0, 0, 0], 310 | ]; 311 | let (row, col, player) = (4, 3, BLACK); 312 | let after = vec![ 313 | vec![0, 0, 0, 0, 0, 0, 0, 0], 314 | vec![0, 1, 1, 1, 1, 1, 1, 0], 315 | vec![0, 1, 2, 1, 2, 1, 1, 0], 316 | vec![0, 1, 1, 1, 1, 2, 1, 0], 317 | vec![0, 1, 1, 1, 0, 2, 1, 0], 318 | vec![0, 1, 1, 1, 1, 2, 1, 0], 319 | vec![0, 1, 1, 1, 1, 1, 1, 0], 320 | vec![0, 0, 0, 0, 0, 0, 0, 0], 321 | ]; 322 | let mut board = Board::from(&before, (0, 1, 2)); 323 | board.apply_move(row, col, player); 324 | let expected = Board::from(&after, (0, 1, 2)); 325 | assert_eq!(board.fields, expected.fields); 326 | } 327 | 328 | #[test] 329 | fn initial_standings() { 330 | let board = Board::new(); 331 | let expected = (2, 2); 332 | let actual = board.get_standings(); 333 | assert_eq!(expected, actual); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /session-02/reversi/src/main.rs: -------------------------------------------------------------------------------- 1 | use akshually::io::prompt_line; 2 | use reversi::{Board, BLACK, SIDES, WHITE}; 3 | 4 | fn main() { 5 | let mut board = Board::new(); 6 | let mut player = WHITE; 7 | loop { 8 | player = match player { 9 | BLACK => WHITE, 10 | WHITE => BLACK, 11 | _ => panic!("invalid player"), 12 | }; 13 | let (black, white) = board.get_standings(); 14 | println!("black: {}, white: {}", black, white); 15 | println!("{}", board); 16 | loop { 17 | let row_col: String = prompt_line(&format!("Player {player}: ")).unwrap(); 18 | let row_col: Vec = row_col.chars().collect(); 19 | let row: usize = (row_col[0] as u8 - 'a' as u8) as usize; 20 | let col: usize = (row_col[1] as u8 - '0' as u8) as usize; 21 | if row > SIDES || col > SIDES { 22 | continue; 23 | } 24 | if board.is_valid_move(row, col, player) { 25 | board.apply_move(row, col, player); 26 | break; 27 | } 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------