diff --git a/topics/sw_concepts/CMakeLists.txt b/topics/sw_concepts/CMakeLists.txt
index a8aa080..4066080 100644
--- a/topics/sw_concepts/CMakeLists.txt
+++ b/topics/sw_concepts/CMakeLists.txt
@@ -1,7 +1,6 @@
include(js_document)
js_slides(sw_concept_slides sw_concept_slides.md)
-js_slides(sw_concept_code_examples sw_concept_code_examples.md)
file(GLOB_RECURSE code RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "code/*")
js_add_to_global_archive_file_list(${code})
diff --git a/topics/sw_concepts/code/pattern-examples/pom.xml b/topics/sw_concepts/code/pattern-examples/pom.xml
index 904fecd..447cf9d 100644
--- a/topics/sw_concepts/code/pattern-examples/pom.xml
+++ b/topics/sw_concepts/code/pattern-examples/pom.xml
@@ -17,6 +17,12 @@
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
org.junit.jupiter
junit-jupiter-api
@@ -35,6 +41,12 @@
5.10.2
test
+
+ org.mockito
+ mockito-core
+ 5.11.0
+ test
+
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/GameLogic.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/GameLogic.java
new file mode 100644
index 0000000..8446f7d
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/GameLogic.java
@@ -0,0 +1,127 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.*;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import ch.scs.jumpstart.pattern.examples.checkers.movevalidator.MoveValidator;
+import ch.scs.jumpstart.pattern.examples.checkers.movevalidator.NoOtherMoveToJumpPossible;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Console;
+import java.util.List;
+import java.util.Optional;
+
+@SuppressWarnings("ClassCanBeRecord")
+public class GameLogic {
+ private final Console console;
+ private final Board board;
+ private final List moveValidators;
+ private final MoveExecutor moveExecutor;
+ private final NoOtherMoveToJumpPossible noOtherMoveToJumpPossible;
+ private final WinCondition winCondition;
+
+ public GameLogic(
+ Console console,
+ Board board,
+ List moveValidators,
+ MoveExecutor moveExecutor,
+ NoOtherMoveToJumpPossible noOtherMoveToJumpPossible,
+ WinCondition winCondition) {
+ this.console = console;
+ this.board = board;
+ this.moveValidators = moveValidators;
+ this.moveExecutor = moveExecutor;
+ this.noOtherMoveToJumpPossible = noOtherMoveToJumpPossible;
+ this.winCondition = winCondition;
+ }
+
+ public void run() {
+ Player currentPlayer = Player.PLAYER_RED;
+ while (true) {
+ Player otherPlayer =
+ currentPlayer == Player.PLAYER_RED ? Player.PLAYER_WHITE : Player.PLAYER_RED;
+ console.print(
+ currentPlayer
+ + ", make your move. Or type 'undo' to go back to the start of the turn of "
+ + otherPlayer);
+ if (doPlayerMove(currentPlayer)) {
+ return;
+ }
+ if (currentPlayer == Player.PLAYER_WHITE) {
+ currentPlayer = Player.PLAYER_RED;
+ } else {
+ currentPlayer = Player.PLAYER_WHITE;
+ }
+ }
+ }
+
+ @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.CognitiveComplexity"})
+ private boolean doPlayerMove(Player player) {
+ BoardCoordinates startCoordinatesForMultipleJump = null;
+ while (true) {
+ String userInput = console.getUserInput();
+ if ("undo".equals(userInput.toLowerCase().trim())) {
+ try {
+ Player playerOfUndoneMove = board.undoLastTurn();
+ if (playerOfUndoneMove.equals(player)) {
+ continue;
+ } else {
+ return false;
+ }
+ } catch (Board.NoPreviousMovesException e) {
+ console.print("There were no previous moves to undo. Please make a move.");
+ continue;
+ }
+ }
+ Move move;
+ try {
+ move = Move.parse(player, userInput);
+ } catch (IllegalArgumentException e) {
+ console.print("Invalid input, try again");
+ continue;
+ }
+ if (startCoordinatesForMultipleJump != null
+ && !startCoordinatesForMultipleJump.equals(move.start())) {
+ console.print("For a multiple jump move, the same piece has to be used. Try again");
+ continue;
+ }
+ Optional pieceAtStart = board.getPieceAt(move.start());
+ if (!moveValidators.stream().allMatch(moveValidator -> moveValidator.validate(move, board))) {
+ console.print("Invalid move, try again");
+ continue;
+ }
+
+ Move executedMove = moveExecutor.executeMove(move);
+
+ boolean hasPlayerWon = winCondition.hasPlayerWon(player, board);
+ if (hasPlayerWon) {
+ console.print("Congratulations, player " + player + " has won");
+ return true;
+ }
+
+ if (executedMove.jumpGambleResult() == JumpGambleResult.WON) {
+ console.print("The gamble has been won, " + player + " can play again.");
+ console.print(
+ player + ", make your move. Or type 'undo' to go back to the start of your turn.");
+ continue;
+ }
+
+ Optional pieceAtEnd = board.getPieceAt(move.end());
+ boolean wasKingCreated = wasKingCreated(pieceAtStart.orElse(null), pieceAtEnd.orElse(null));
+ if (wasKingCreated) {
+ return false;
+ }
+ if (!move.isJumpMove()
+ || !noOtherMoveToJumpPossible.jumpMovePossibleFrom(move.end(), board)) {
+ return false;
+ }
+ console.print("Multiple jump move for " + player + ". Enter your next jump.");
+ console.print("Or type 'undo' to go back to the start of your turn.");
+ startCoordinatesForMultipleJump = move.end();
+ }
+ }
+
+ private boolean wasKingCreated(Piece pieceAtStart, Piece pieceAtEnd) {
+ if (pieceAtStart == null || pieceAtEnd == null) {
+ return false;
+ }
+ return !pieceAtStart.isKing() && pieceAtEnd.isKing();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/Main.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/Main.java
new file mode 100644
index 0000000..b741d19
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/Main.java
@@ -0,0 +1,63 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import ch.scs.jumpstart.pattern.examples.checkers.movevalidator.*;
+import ch.scs.jumpstart.pattern.examples.checkers.util.BoardPrinter;
+import ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Console;
+import ch.scs.jumpstart.pattern.examples.checkers.util.PointsCalculator;
+import java.util.List;
+
+public class Main {
+ public static void main(String[] args) {
+ GameLogic gameLogic =
+ createGameLogic(Console.getInstance(), new CoinTosser(new PointsCalculator()));
+
+ gameLogic.run();
+ }
+
+ public static GameLogic createGameLogic(Console console, CoinTosser coinTosser) {
+ Board board = new Board();
+ BoardPrinter boardPrinter = new BoardPrinter(console);
+ board.registerObserver(boardPrinter::printBoard);
+
+ MoveIsDiagonal moveIsDiagonal = new MoveIsDiagonal();
+ MoveLength moveLength = new MoveLength();
+ MoveIsForwardIfNotKing moveIsForwardIfNotKing = new MoveIsForwardIfNotKing();
+ OpponentPieceBetweenJump opponentPieceBetweenJump = new OpponentPieceBetweenJump();
+ TargetFieldEmpty targetFieldEmpty = new TargetFieldEmpty();
+ NoOtherMoveToJumpPossible noOtherMoveToJumpPossible =
+ new NoOtherMoveToJumpPossible(
+ moveIsForwardIfNotKing, opponentPieceBetweenJump, targetFieldEmpty);
+ StartPieceValid startPieceValid = new StartPieceValid();
+
+ List moveValidators =
+ List.of(
+ moveIsDiagonal,
+ moveIsForwardIfNotKing,
+ moveLength,
+ noOtherMoveToJumpPossible,
+ opponentPieceBetweenJump,
+ startPieceValid,
+ targetFieldEmpty);
+
+ WinCondition winCondition =
+ new WinCondition(
+ List.of(
+ moveIsDiagonal,
+ moveLength,
+ moveIsForwardIfNotKing,
+ opponentPieceBetweenJump,
+ targetFieldEmpty,
+ startPieceValid));
+
+ MoveExecutor moveExecutor = new MoveExecutor(board, coinTosser, console);
+ GameLogic gameLogic =
+ new GameLogic(
+ console, board, moveValidators, moveExecutor, noOtherMoveToJumpPossible, winCondition);
+
+ console.print("Welcome to checkers");
+ boardPrinter.printBoard(board);
+ return gameLogic;
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/MoveExecutor.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/MoveExecutor.java
new file mode 100644
index 0000000..61f0c6c
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/MoveExecutor.java
@@ -0,0 +1,73 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser.Result.HEADS;
+import static ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser.Result.TAILS;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Console;
+
+@SuppressWarnings("ClassCanBeRecord")
+public class MoveExecutor {
+ private final Board board;
+ private final CoinTosser coinTosser;
+ private final Console console;
+
+ public MoveExecutor(Board board, CoinTosser coinTosser, Console console) {
+ this.board = board;
+ this.coinTosser = coinTosser;
+ this.console = console;
+ }
+
+ public Move executeMove(Move move) {
+ if (!move.isJumpMove()) {
+ board.executeMove(move);
+ return move;
+ }
+ console.print(move.player() + " is making a jump move. Now " + move.player() + " may gamble.");
+ console.print("If " + move.player() + " does not gamble, the jump move is executed normally.");
+ console.print("If " + move.player() + " does gamble, a coin will be tossed.");
+ console.print(
+ "If the coin toss is "
+ + HEADS
+ + ", then the jump move will be executed, but "
+ + move.player()
+ + " gets another turn.");
+ console.print(
+ "If the coin toss is "
+ + TAILS
+ + ", then the jump move fails and the piece at "
+ + move.start()
+ + ", which would have been used for the jump move, is removed.");
+ GambleChoice gambleChoice = null;
+ do {
+ console.print(
+ move.player() + " type \"yes\" to gamble, \"no\" to execute the jump move normally");
+ String userInput = console.getUserInput();
+ try {
+ gambleChoice = GambleChoice.valueOf(userInput.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ console.print("The input of " + move.player() + " was invalid. Input was: " + userInput);
+ }
+ } while (gambleChoice == null);
+ if (gambleChoice == GambleChoice.NO) {
+ board.executeMove(move);
+ return move;
+ }
+ CoinTosser.Result tossResult = coinTosser.toss(board, move.player());
+ JumpGambleResult jumpGambleResult =
+ tossResult == HEADS ? JumpGambleResult.WON : JumpGambleResult.LOST;
+ console.print("Coin toss resulted in " + tossResult + ", the gamble was: " + jumpGambleResult);
+ Move newMove = move.withJumpGambleResult(jumpGambleResult);
+ board.executeMove(newMove);
+ return newMove;
+ }
+
+ private enum GambleChoice {
+ @SuppressWarnings("unused")
+ YES,
+ NO
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/README.md b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/README.md
new file mode 100644
index 0000000..f3a62aa
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/README.md
@@ -0,0 +1,4 @@
+# Checkers pattern examples
+
+These examples were taken from [Soco21-group8 checkersV3](https://github.com/soco21/soco21-group8/tree/main/assignment-3/checkersv3)
+with the permission of it's author [@BacLuc](https://github.com/BacLuc)
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/WinCondition.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/WinCondition.java
new file mode 100644
index 0000000..6b26655
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/WinCondition.java
@@ -0,0 +1,51 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import ch.scs.jumpstart.pattern.examples.checkers.movevalidator.MoveValidator;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Check if one player cannot move or has no pieces. This can be done in one go, as if one player
+ * has no pieces, he cannot move. Use MoveValidators to check if any move is possible.
+ */
+@SuppressWarnings("ClassCanBeRecord")
+public class WinCondition {
+ private final List moveValidators;
+
+ public WinCondition(List moveValidators) {
+ this.moveValidators = moveValidators;
+ }
+
+ public boolean hasPlayerWon(Player player, Board board) {
+ for (Row row : Row.values()) {
+ for (Column col : Column.values()) {
+ BoardCoordinates currentCoordinates = new BoardCoordinates(row, col);
+ Optional pieceAt = board.getPieceAt(currentCoordinates);
+ if (pieceAt.isEmpty()) {
+ continue;
+ }
+ Piece piece = pieceAt.get();
+ if (piece.owner() == player) {
+ continue;
+ }
+ List possibleMoves =
+ Move.generatePossibleMoves(currentCoordinates, piece.owner(), List.of(1, 2));
+ if (possibleMoves.stream()
+ .anyMatch(
+ move ->
+ moveValidators.stream()
+ .allMatch(moveValidator -> moveValidator.validate(move, board)))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardCoordinates.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardCoordinates.java
new file mode 100644
index 0000000..1d33db9
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardCoordinates.java
@@ -0,0 +1,52 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+public record BoardCoordinates(Row row, Column column) {
+
+ public enum Row {
+ ROW_1,
+ ROW_2,
+ ROW_3,
+ ROW_4,
+ ROW_5,
+ ROW_6,
+ ROW_7,
+ ROW_8;
+
+ public boolean isLastRow() {
+ return this == ROW_8;
+ }
+
+ public boolean isFirstRow() {
+ return this == ROW_1;
+ }
+
+ public int diffRow(Row row) {
+ return this.ordinal() - row.ordinal();
+ }
+
+ public Row getRowBetween(Row row) {
+ int indexBetween = (this.ordinal() + row.ordinal()) / 2;
+ return Row.values()[indexBetween];
+ }
+ }
+
+ public enum Column {
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H;
+
+ public int diffCol(Column column) {
+ return this.ordinal() - column.ordinal();
+ }
+
+ public Column getColBetween(Column column) {
+ int indexBetween = (this.ordinal() + column.ordinal()) / 2;
+ return Column.values()[indexBetween];
+ }
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardObserver.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardObserver.java
new file mode 100644
index 0000000..73be190
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardObserver.java
@@ -0,0 +1,7 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+
+public interface BoardObserver {
+ void boardChanged(Board board);
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/JumpGambleResult.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/JumpGambleResult.java
new file mode 100644
index 0000000..2cb6afe
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/JumpGambleResult.java
@@ -0,0 +1,7 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+public enum JumpGambleResult {
+ NO_GAMBLE,
+ WON,
+ LOST
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Move.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Move.java
new file mode 100644
index 0000000..fe4f855
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Move.java
@@ -0,0 +1,108 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.*;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+public record Move(
+ Player player,
+ BoardCoordinates start,
+ BoardCoordinates end,
+ JumpGambleResult jumpGambleResult) {
+ public static Move parse(Player player, String string) {
+ String replacedString = string.replace("X", "x").replace("[", "").replace("]", "");
+ String[] startAndEnd = replacedString.split("x");
+ if (startAndEnd.length < 2) {
+ throw new IllegalArgumentException("invalid input");
+ }
+ BoardCoordinates start = parseBoardCoordinates(startAndEnd[0]);
+ BoardCoordinates end = parseBoardCoordinates(startAndEnd[1]);
+
+ return Move.of(player, start, end);
+ }
+
+ public static Move of(Player player, BoardCoordinates start, BoardCoordinates end) {
+ return new Move(player, start, end, JumpGambleResult.NO_GAMBLE);
+ }
+
+ public static List generatePossibleMoves(
+ BoardCoordinates boardCoordinates, Player pieceOwner, List distances) {
+ int rowIndex = boardCoordinates.row().ordinal();
+ int colIndex = boardCoordinates.column().ordinal();
+ List possibleJumpMoves = new ArrayList<>();
+
+ for (Integer distance : distances) {
+ of(pieceOwner, boardCoordinates, rowIndex + distance, colIndex + distance)
+ .ifPresent(possibleJumpMoves::add);
+ of(pieceOwner, boardCoordinates, rowIndex + distance, colIndex - distance)
+ .ifPresent(possibleJumpMoves::add);
+ of(pieceOwner, boardCoordinates, rowIndex - distance, colIndex + distance)
+ .ifPresent(possibleJumpMoves::add);
+ of(pieceOwner, boardCoordinates, rowIndex - distance, colIndex - distance)
+ .ifPresent(possibleJumpMoves::add);
+ }
+ return possibleJumpMoves;
+ }
+
+ private static Optional of(
+ Player player, BoardCoordinates start, int rowIndex, int colIndex) {
+ Row[] rows = Row.values();
+ Column[] columns = Column.values();
+
+ if (rowIndex >= 0 && rowIndex < rows.length && colIndex >= 0 && colIndex < columns.length) {
+ return Optional.of(
+ Move.of(player, start, new BoardCoordinates(rows[rowIndex], columns[colIndex])));
+ }
+ return Optional.empty();
+ }
+
+ private static BoardCoordinates parseBoardCoordinates(String s) {
+ String[] columnAndRow = s.split("");
+ if (columnAndRow.length != 2) {
+ throw new IllegalArgumentException("invalid input");
+ }
+ String columnString = columnAndRow[0];
+ Column column = Column.valueOf(columnString.toUpperCase());
+
+ String rowString = columnAndRow[1];
+ for (Row row : Row.values()) {
+ int enumValue = row.ordinal() + 1;
+ if (rowString.equals(String.valueOf(enumValue))) {
+ return new BoardCoordinates(row, column);
+ }
+ }
+ throw new IllegalArgumentException("invalid input");
+ }
+
+ public Optional getCoordinatesBetween() {
+ int rowDiff = start.row().diffRow(end.row());
+ int colDiff = start.column().diffCol(end.column());
+
+ if (Math.abs(rowDiff) != 2 || Math.abs(colDiff) != 2) {
+ return Optional.empty();
+ }
+
+ return Optional.of(
+ new BoardCoordinates(
+ start.row().getRowBetween(end.row()), start.column().getColBetween(end.column())));
+ }
+
+ public boolean isJumpMove() {
+ int diffMoveColumn = start.column().diffCol(end.column());
+ int diffMoveRow = start.row().diffRow(end.row());
+ return Math.abs(diffMoveRow) == 2 && Math.abs(diffMoveColumn) == 2;
+ }
+
+ public Move withJumpGambleResult(JumpGambleResult jumpGambleResult) {
+ if (jumpGambleResult != JumpGambleResult.NO_GAMBLE && !isJumpMove()) {
+ throw new IllegalArgumentException(
+ "cannot create move with JumpGambleResult"
+ + jumpGambleResult
+ + ", when the move is no jump move");
+ }
+ return new Move(player, start, end, jumpGambleResult);
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Piece.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Piece.java
new file mode 100644
index 0000000..c2d3cb3
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Piece.java
@@ -0,0 +1,3 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+public record Piece(Player owner, boolean isKing) {}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Player.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Player.java
new file mode 100644
index 0000000..ff48a42
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/Player.java
@@ -0,0 +1,6 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+public enum Player {
+ PLAYER_WHITE,
+ PLAYER_RED
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Board.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Board.java
new file mode 100644
index 0000000..74d4949
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Board.java
@@ -0,0 +1,75 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.*;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Tuple;
+import java.util.*;
+
+public class Board {
+ private final List boardObservers = new ArrayList<>();
+ private final Store store = new Store();
+ private final List> executedCommands = new ArrayList<>();
+
+ public void executeMove(Move move) {
+ Command command = createCommand(move);
+ command.execute();
+ executedCommands.add(Tuple.of(move.player(), command));
+ boardObservers.forEach(boardObserver -> boardObserver.boardChanged(this));
+ }
+
+ public Player undoLastTurn() throws NoPreviousMovesException {
+ if (executedCommands.isEmpty()) {
+ throw new NoPreviousMovesException();
+ }
+ Tuple lastCommandTuple = executedCommands.get(executedCommands.size() - 1);
+ for (int i = executedCommands.size() - 1; i >= 0; i--) {
+ Tuple currentCommandTuple = executedCommands.get(i);
+ if (!currentCommandTuple.getKey().equals(lastCommandTuple.getKey())) {
+ break;
+ }
+ currentCommandTuple.getValue().undo();
+ executedCommands.remove(i);
+ }
+ boardObservers.forEach(boardObserver -> boardObserver.boardChanged(this));
+ return lastCommandTuple.getKey();
+ }
+
+ public Optional getPieceAt(BoardCoordinates boardCoordinates) {
+ return store.getPieceAt(boardCoordinates);
+ }
+
+ public void registerObserver(BoardObserver boardObserver) {
+ boardObservers.add(boardObserver);
+ }
+
+ private Command createCommand(Move move) {
+ Piece pieceAtStart = store.getPieceAt(move.start()).orElseThrow();
+ Tuple start = Tuple.of(move.start(), pieceAtStart);
+ if (move.jumpGambleResult() == JumpGambleResult.LOST) {
+ return new JumpGambleLostMove(store, start);
+ }
+
+ boolean convertToKing = isConvertToKing(move);
+ Piece pieceAtEnd = new Piece(move.player(), pieceAtStart.isKing() || convertToKing);
+ Tuple end = Tuple.of(move.end(), pieceAtEnd);
+ if (move.isJumpMove()) {
+ Piece pieceBetween = move.getCoordinatesBetween().flatMap(store::getPieceAt).orElseThrow();
+ return new JumpMove(
+ store, start, Tuple.of(move.getCoordinatesBetween().orElseThrow(), pieceBetween), end);
+ } else {
+ return new SimpleMove(store, start, end);
+ }
+ }
+
+ private boolean isConvertToKing(Move move) {
+ if (move.player() == Player.PLAYER_WHITE) {
+ return move.end().row().isLastRow();
+ }
+ return move.end().row().isFirstRow();
+ }
+
+ public static class NoPreviousMovesException extends Exception {
+ private NoPreviousMovesException() {
+ super();
+ }
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Command.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Command.java
new file mode 100644
index 0000000..e4ad021
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Command.java
@@ -0,0 +1,7 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+interface Command {
+ void execute();
+
+ void undo();
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/JumpGambleLostMove.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/JumpGambleLostMove.java
new file mode 100644
index 0000000..879fbd1
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/JumpGambleLostMove.java
@@ -0,0 +1,26 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Tuple;
+
+@SuppressWarnings("ClassCanBeRecord")
+class JumpGambleLostMove implements Command {
+ private final Store store;
+ private final Tuple start;
+
+ JumpGambleLostMove(Store store, Tuple start) {
+ this.store = store;
+ this.start = start;
+ }
+
+ @Override
+ public void execute() {
+ store.removePiece(start.getKey());
+ }
+
+ @Override
+ public void undo() {
+ store.addPiece(start.getKey(), start.getValue());
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/JumpMove.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/JumpMove.java
new file mode 100644
index 0000000..aad2cc7
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/JumpMove.java
@@ -0,0 +1,38 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Tuple;
+
+@SuppressWarnings("ClassCanBeRecord")
+public class JumpMove implements Command {
+ private final Store store;
+ private final Tuple start;
+ private final Tuple pieceBetween;
+ private final Tuple end;
+
+ public JumpMove(
+ Store store,
+ Tuple start,
+ Tuple pieceBetween,
+ Tuple end) {
+ this.store = store;
+ this.start = start;
+ this.pieceBetween = pieceBetween;
+ this.end = end;
+ }
+
+ @Override
+ public void execute() {
+ store.removePiece(start.getKey());
+ store.removePiece(pieceBetween.getKey());
+ store.addPiece(end.getKey(), end.getValue());
+ }
+
+ @Override
+ public void undo() {
+ store.addPiece(start.getKey(), start.getValue());
+ store.addPiece(pieceBetween.getKey(), pieceBetween.getValue());
+ store.removePiece(end.getKey());
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/SimpleMove.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/SimpleMove.java
new file mode 100644
index 0000000..caf6a7f
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/SimpleMove.java
@@ -0,0 +1,31 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Tuple;
+
+@SuppressWarnings("ClassCanBeRecord")
+public class SimpleMove implements Command {
+ private final Store store;
+ private final Tuple start;
+ private final Tuple end;
+
+ public SimpleMove(
+ Store store, Tuple start, Tuple end) {
+ this.store = store;
+ this.start = start;
+ this.end = end;
+ }
+
+ @Override
+ public void execute() {
+ store.removePiece(start.getKey());
+ store.addPiece(end.getKey(), end.getValue());
+ }
+
+ @Override
+ public void undo() {
+ store.addPiece(start.getKey(), start.getValue());
+ store.removePiece(end.getKey());
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Store.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Store.java
new file mode 100644
index 0000000..3b4bfb5
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/Store.java
@@ -0,0 +1,120 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.*;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+class Store {
+ final Map> boardMatrix = new HashMap<>();
+
+ Store() {
+ boardMatrix.put(
+ Row.ROW_1,
+ Map.of(
+ Column.A,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.C,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.E,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.G,
+ new Piece(Player.PLAYER_WHITE, false)));
+ boardMatrix.put(
+ Row.ROW_3,
+ Map.of(
+ Column.A,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.C,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.E,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.G,
+ new Piece(Player.PLAYER_WHITE, false)));
+ boardMatrix.put(
+ Row.ROW_2,
+ Map.of(
+ Column.B,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.D,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.F,
+ new Piece(Player.PLAYER_WHITE, false),
+ Column.H,
+ new Piece(Player.PLAYER_WHITE, false)));
+
+ boardMatrix.put(
+ Row.ROW_8,
+ Map.of(
+ Column.B,
+ new Piece(Player.PLAYER_RED, false),
+ Column.D,
+ new Piece(Player.PLAYER_RED, false),
+ Column.F,
+ new Piece(Player.PLAYER_RED, false),
+ Column.H,
+ new Piece(Player.PLAYER_RED, false)));
+ boardMatrix.put(
+ Row.ROW_6,
+ Map.of(
+ Column.B,
+ new Piece(Player.PLAYER_RED, false),
+ Column.D,
+ new Piece(Player.PLAYER_RED, false),
+ Column.F,
+ new Piece(Player.PLAYER_RED, false),
+ Column.H,
+ new Piece(Player.PLAYER_RED, false)));
+ boardMatrix.put(
+ Row.ROW_7,
+ Map.of(
+ Column.A,
+ new Piece(Player.PLAYER_RED, false),
+ Column.C,
+ new Piece(Player.PLAYER_RED, false),
+ Column.E,
+ new Piece(Player.PLAYER_RED, false),
+ Column.G,
+ new Piece(Player.PLAYER_RED, false)));
+ }
+
+ Optional getPieceAt(BoardCoordinates boardCoordinates) {
+ Map columnOptionalMap = boardMatrix.get(boardCoordinates.row());
+ if (columnOptionalMap == null) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(columnOptionalMap.get(boardCoordinates.column()));
+ }
+
+ void removePiece(BoardCoordinates start) {
+ boardMatrix.compute(
+ start.row(),
+ (row, columnPieceMap) -> {
+ if (columnPieceMap == null) {
+ return new HashMap<>();
+ } else {
+ HashMap columnPieceHashMap = new HashMap<>(columnPieceMap);
+ columnPieceHashMap.remove(start.column());
+ return columnPieceHashMap;
+ }
+ });
+ }
+
+ void addPiece(BoardCoordinates boardCoordinates, Piece piece) {
+ boardMatrix.compute(
+ boardCoordinates.row(),
+ (row, columnPieceMap) -> {
+ if (columnPieceMap == null) {
+ return Map.of(boardCoordinates.column(), piece);
+ } else {
+ HashMap columnPieceHashMap = new HashMap<>(columnPieceMap);
+ columnPieceHashMap.put(boardCoordinates.column(), piece);
+ return columnPieceHashMap;
+ }
+ });
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsDiagonal.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsDiagonal.java
new file mode 100644
index 0000000..bdbae8e
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsDiagonal.java
@@ -0,0 +1,18 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+
+public class MoveIsDiagonal implements MoveValidator {
+ @Override
+ public boolean validate(Move move, Board board) {
+ BoardCoordinates start = move.start();
+ BoardCoordinates end = move.end();
+ boolean bothDirectionsChange = start.row() != end.row() && start.column() != end.column();
+
+ int rowDiff = Math.abs(start.row().ordinal() - end.row().ordinal());
+ int colDiff = Math.abs(start.column().ordinal() - end.column().ordinal());
+ return bothDirectionsChange && rowDiff == colDiff;
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsForwardIfNotKing.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsForwardIfNotKing.java
new file mode 100644
index 0000000..68c790b
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsForwardIfNotKing.java
@@ -0,0 +1,28 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+
+public class MoveIsForwardIfNotKing implements MoveValidator {
+ @Override
+ public boolean validate(Move move, Board board) {
+ Optional pieceAt = board.getPieceAt(move.start());
+ if (pieceAt.isEmpty()) {
+ return true;
+ }
+ Piece piece = pieceAt.get();
+ if (piece.isKing()) {
+ return true;
+ }
+ if (piece.owner().equals(Player.PLAYER_WHITE)) {
+ return move.start().row().ordinal() < move.end().row().ordinal();
+
+ } else if (piece.owner().equals(Player.PLAYER_RED)) {
+ return move.start().row().ordinal() > move.end().row().ordinal();
+ }
+ return false;
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveLength.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveLength.java
new file mode 100644
index 0000000..3380c0f
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveLength.java
@@ -0,0 +1,19 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+
+public class MoveLength implements MoveValidator {
+ @Override
+ public boolean validate(Move move, Board board) {
+ BoardCoordinates start = move.start();
+ BoardCoordinates end = move.end();
+ int diffMoveColumn = start.column().diffCol(end.column());
+ int diffMoveRow = start.row().diffRow(end.row());
+ if (diffMoveColumn == 0 && diffMoveRow == 0) {
+ return false;
+ }
+ return Math.abs(diffMoveRow) <= 2 && Math.abs(diffMoveColumn) <= 2;
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveValidator.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveValidator.java
new file mode 100644
index 0000000..42dc6b2
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveValidator.java
@@ -0,0 +1,8 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+
+public interface MoveValidator {
+ boolean validate(Move move, Board board);
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/NoOtherMoveToJumpPossible.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/NoOtherMoveToJumpPossible.java
new file mode 100644
index 0000000..6db87cf
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/NoOtherMoveToJumpPossible.java
@@ -0,0 +1,65 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.*;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.List;
+import java.util.Optional;
+
+public class NoOtherMoveToJumpPossible implements MoveValidator {
+
+ private final List jumpValidators;
+
+ public NoOtherMoveToJumpPossible(
+ MoveIsForwardIfNotKing moveIsForwardIfNotKings,
+ OpponentPieceBetweenJump opponentPieceBetweenJump,
+ TargetFieldEmpty targetFieldEmpty) {
+ jumpValidators = List.of(moveIsForwardIfNotKings, opponentPieceBetweenJump, targetFieldEmpty);
+ }
+
+ @Override
+ public boolean validate(Move move, Board board) {
+ Row[] rows = Row.values();
+ Column[] columns = Column.values();
+ if (move.isJumpMove()) {
+ return true;
+ }
+ for (Row row : rows) {
+ for (Column col : columns) {
+ BoardCoordinates currentCoordinates = new BoardCoordinates(row, col);
+ Optional pieceAt = board.getPieceAt(currentCoordinates);
+ if (pieceAt.isEmpty()) {
+ continue;
+ }
+ if (pieceAt.get().owner() != move.player()) {
+ continue;
+ }
+ if (jumpMovePossibleFrom(currentCoordinates, board)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean jumpMovePossibleFrom(BoardCoordinates boardCoordinates, Board board) {
+ Optional pieceAt = board.getPieceAt(boardCoordinates);
+ if (pieceAt.isEmpty()) {
+ return false;
+ }
+ Player pieceOwner = pieceAt.get().owner();
+
+ List possibleJumpMoves =
+ Move.generatePossibleMoves(boardCoordinates, pieceOwner, List.of(2));
+
+ return possibleJumpMoves.stream()
+ .anyMatch(
+ move ->
+ jumpValidators.stream()
+ .allMatch(moveValidator -> moveValidator.validate(move, board)));
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/OpponentPieceBetweenJump.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/OpponentPieceBetweenJump.java
new file mode 100644
index 0000000..99e77f8
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/OpponentPieceBetweenJump.java
@@ -0,0 +1,22 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+
+public class OpponentPieceBetweenJump implements MoveValidator {
+ @Override
+ public boolean validate(Move move, Board board) {
+ Optional coordinatesBetween = move.getCoordinatesBetween();
+ if (coordinatesBetween.isEmpty()) {
+ return true;
+ }
+ Optional pieceBetweenJump = board.getPieceAt(coordinatesBetween.get());
+ if (pieceBetweenJump.isEmpty()) {
+ return false;
+ }
+ return pieceBetweenJump.get().owner() != move.player();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/StartPieceValid.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/StartPieceValid.java
new file mode 100644
index 0000000..aa7b22b
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/StartPieceValid.java
@@ -0,0 +1,25 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+
+public class StartPieceValid implements MoveValidator {
+ @Override
+ public boolean validate(Move move, Board board) {
+ BoardCoordinates start = move.start();
+
+ BoardCoordinates.Row[] row = BoardCoordinates.Row.values();
+ BoardCoordinates.Column[] col = BoardCoordinates.Column.values();
+
+ Optional startPiece = board.getPieceAt(start);
+
+ if (startPiece.isEmpty()) {
+ return false;
+ }
+
+ return startPiece.get().owner() == move.player();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/TargetFieldEmpty.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/TargetFieldEmpty.java
new file mode 100644
index 0000000..ae41a8d
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/TargetFieldEmpty.java
@@ -0,0 +1,14 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+
+public class TargetFieldEmpty implements MoveValidator {
+ @Override
+ public boolean validate(Move move, Board board) {
+ Optional pieceAt = board.getPieceAt(move.end());
+ return pieceAt.isEmpty();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/BoardPrinter.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/BoardPrinter.java
new file mode 100644
index 0000000..e8742c0
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/BoardPrinter.java
@@ -0,0 +1,60 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.*;
+
+public class BoardPrinter {
+ private final Console console;
+
+ public BoardPrinter(Console console) {
+ this.console = console;
+ }
+
+ @SuppressWarnings("PMD.CognitiveComplexity")
+ public void printBoard(Board board) {
+ StringBuilder header = new StringBuilder();
+ header.append(" ");
+ for (Column col : Column.values()) {
+ header.append(col.name().toLowerCase(Locale.ROOT)).append(" ");
+ }
+ console.print(header.toString());
+ console.print(" +-------------------------------------------------+");
+
+ List rows = Arrays.asList(Row.values());
+ Collections.reverse(rows);
+ for (Row row : rows) {
+ StringBuilder rowString = new StringBuilder((row.ordinal() + 1) + " |");
+ for (Column col : Column.values()) {
+ Optional pieceAt = board.getPieceAt(new BoardCoordinates(row, col));
+ rowString.append(" ");
+ if (pieceAt.isEmpty()) {
+ rowString.append("[ ]");
+ } else {
+ Piece piece = pieceAt.get();
+ rowString.append("[");
+ if (piece.owner() == Player.PLAYER_WHITE) {
+ rowString.append("W");
+ } else {
+ rowString.append("R");
+ }
+ rowString.append("_");
+ if (piece.isKing()) {
+ rowString.append("K");
+ } else {
+ rowString.append("P");
+ }
+ rowString.append("]");
+ }
+ }
+ console.print(rowString + " | " + (row.ordinal() + 1));
+ }
+
+ console.print(" +-------------------------------------------------+");
+ console.print(header.toString());
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/CoinTosser.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/CoinTosser.java
new file mode 100644
index 0000000..2eadbb6
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/CoinTosser.java
@@ -0,0 +1,33 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Map;
+import java.util.Random;
+
+public class CoinTosser {
+ private final PointsCalculator pointsCalculator;
+ private final Random random;
+
+ public CoinTosser(PointsCalculator pointsCalculator) {
+ this(pointsCalculator, new Random());
+ }
+
+ CoinTosser(PointsCalculator pointsCalculator, Random random) {
+ this.pointsCalculator = pointsCalculator;
+ this.random = random;
+ }
+
+ public Result toss(Board board, Player player) {
+ Map playerPointsMap = pointsCalculator.calculatePoints(board);
+ int totalPoints =
+ playerPointsMap.get(Player.PLAYER_RED) + playerPointsMap.get(Player.PLAYER_WHITE);
+ float winChance = 1 - playerPointsMap.get(player).floatValue() / totalPoints;
+ return random.nextFloat() <= winChance ? Result.HEADS : Result.TAILS;
+ }
+
+ public enum Result {
+ HEADS,
+ TAILS
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/Console.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/Console.java
new file mode 100644
index 0000000..a047b3e
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/Console.java
@@ -0,0 +1,30 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public class Console {
+ private static final Console instance = new Console();
+
+ public static Console getInstance() {
+ return instance;
+ }
+
+ private Console() {}
+
+ @SuppressWarnings("PMD.SystemPrintln")
+ public void print(String string) {
+ System.out.println(string);
+ }
+
+ public String getUserInput() {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+
+ try {
+ return reader.readLine();
+ } catch (IOException e) {
+ return "";
+ }
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/PointsCalculator.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/PointsCalculator.java
new file mode 100644
index 0000000..1bc27b9
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/PointsCalculator.java
@@ -0,0 +1,49 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.Player.PLAYER_RED;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.Player.PLAYER_WHITE;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class PointsCalculator {
+
+ @SuppressWarnings("PMD.CognitiveComplexity")
+ public Map calculatePoints(Board board) {
+ int pointsPlayerWhite = 0;
+ int pointsPlayerRed = 0;
+ BoardCoordinates.Row[] rows = BoardCoordinates.Row.values();
+ BoardCoordinates.Column[] columns = BoardCoordinates.Column.values();
+ for (BoardCoordinates.Row row : rows) {
+ for (BoardCoordinates.Column col : columns) {
+ BoardCoordinates currentCoordinates = new BoardCoordinates(row, col);
+ Optional pieceAt = board.getPieceAt(currentCoordinates);
+ if (pieceAt.isPresent()) {
+ if (pieceAt.get().owner() == PLAYER_WHITE) {
+ if (pieceAt.get().isKing()) {
+ pointsPlayerWhite += 2;
+ } else {
+ pointsPlayerWhite += 1;
+ }
+ } else {
+ if (pieceAt.get().isKing()) {
+ pointsPlayerRed += 2;
+ } else {
+ pointsPlayerRed += 1;
+ }
+ }
+ }
+ }
+ }
+ HashMap pointsOnBoard = new HashMap<>();
+ pointsOnBoard.put(PLAYER_WHITE, pointsPlayerWhite);
+ pointsOnBoard.put(PLAYER_RED, pointsPlayerRed);
+
+ return pointsOnBoard;
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/Tuple.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/Tuple.java
new file mode 100644
index 0000000..11c1fb3
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/checkers/util/Tuple.java
@@ -0,0 +1,13 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import java.util.AbstractMap;
+
+public class Tuple extends AbstractMap.SimpleImmutableEntry {
+ public static Tuple of(K key, V value) {
+ return new Tuple<>(key, value);
+ }
+
+ private Tuple(K key, V value) {
+ super(key, value);
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/Button.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/Button.java
new file mode 100644
index 0000000..4ba1e8d
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/Button.java
@@ -0,0 +1,3 @@
+package ch.scs.jumpstart.pattern.examples.gui;
+
+public interface Button {}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/ButtonFactory.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/ButtonFactory.java
new file mode 100644
index 0000000..63da870
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/ButtonFactory.java
@@ -0,0 +1,8 @@
+package ch.scs.jumpstart.pattern.examples.gui;
+
+@SuppressWarnings("unused")
+public interface ButtonFactory {
+ Button createBackButton();
+
+ Button createForwardButton();
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/FlatButton.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/FlatButton.java
new file mode 100644
index 0000000..64580b8
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/FlatButton.java
@@ -0,0 +1,3 @@
+package ch.scs.jumpstart.pattern.examples.gui;
+
+public record FlatButton(String buttonName) implements Button {}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/FlatButtonFactory.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/FlatButtonFactory.java
new file mode 100644
index 0000000..d1cf12b
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/FlatButtonFactory.java
@@ -0,0 +1,14 @@
+package ch.scs.jumpstart.pattern.examples.gui;
+
+@SuppressWarnings("unused")
+public class FlatButtonFactory implements ButtonFactory {
+ @Override
+ public Button createBackButton() {
+ return new FlatButton("back");
+ }
+
+ @Override
+ public Button createForwardButton() {
+ return new FlatButton("forward");
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/RoundedButton.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/RoundedButton.java
new file mode 100644
index 0000000..00751de
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/RoundedButton.java
@@ -0,0 +1,3 @@
+package ch.scs.jumpstart.pattern.examples.gui;
+
+public record RoundedButton(String buttonName) implements Button {}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/RoundedButtonFactory.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/RoundedButtonFactory.java
new file mode 100644
index 0000000..cc33a6d
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/gui/RoundedButtonFactory.java
@@ -0,0 +1,14 @@
+package ch.scs.jumpstart.pattern.examples.gui;
+
+@SuppressWarnings("unused")
+public class RoundedButtonFactory implements ButtonFactory {
+ @Override
+ public Button createBackButton() {
+ return new RoundedButton("back");
+ }
+
+ @Override
+ public Button createForwardButton() {
+ return new RoundedButton("forward");
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/Customer1ReceiptFactory.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/Customer1ReceiptFactory.java
new file mode 100644
index 0000000..930e22b
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/Customer1ReceiptFactory.java
@@ -0,0 +1,22 @@
+package ch.scs.jumpstart.pattern.examples.receipts;
+
+// details omitted
+public class Customer1ReceiptFactory implements ReceiptFactory {
+ @Override
+ public Receipt createEftReceipt() {
+ // details omitted
+ return new Receipt();
+ }
+
+ @Override
+ public Receipt createRefundReceipt() {
+ throw new UnsupportedOperationException(
+ "%s does not support refund".formatted(getClass().getSimpleName()));
+ }
+
+ @Override
+ public Receipt createContactlessReceipt() {
+ // details omitted
+ return new Receipt();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/Receipt.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/Receipt.java
new file mode 100644
index 0000000..76dc0f4
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/Receipt.java
@@ -0,0 +1,3 @@
+package ch.scs.jumpstart.pattern.examples.receipts;
+
+public class Receipt {}
diff --git a/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/ReceiptFactory.java b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/ReceiptFactory.java
new file mode 100644
index 0000000..6d3e9f4
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/main/java/ch/scs/jumpstart/pattern/examples/receipts/ReceiptFactory.java
@@ -0,0 +1,10 @@
+package ch.scs.jumpstart.pattern.examples.receipts;
+
+@SuppressWarnings("unused")
+public interface ReceiptFactory {
+ Receipt createEftReceipt();
+
+ Receipt createRefundReceipt();
+
+ Receipt createContactlessReceipt();
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/GameLogicTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/GameLogicTest.java
new file mode 100644
index 0000000..41d6d15
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/GameLogicTest.java
@@ -0,0 +1,409 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser;
+import ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser.Result;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Console;
+import java.util.List;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+
+class GameLogicTest {
+
+ private CoinTosser coinTosser;
+ private GameLogic gameLogic;
+ private Console console;
+
+ @BeforeEach
+ void setup() {
+ console = mock(Console.class);
+ doCallRealMethod().when(console).print(notNull());
+ coinTosser = mock(CoinTosser.class);
+ gameLogic = Main.createGameLogic(console, coinTosser);
+ }
+
+ @Test
+ void end_game_with_winner() {
+ // numbers were taken from: http://www.quadibloc.com/other/bo1211.htm
+ when(console.getUserInput())
+ // RED
+ .thenReturn(fromNumbers(11, 15))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(23, 19))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(8, 11))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(22, 17))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(11, 16))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(24, 20))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(16, 23))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(27, 18))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(18, 11))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(7, 16))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(20, 11))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(3, 7))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(28, 24))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(7, 16))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(24, 20))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(16, 19))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(25, 22))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(4, 8))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(29, 25))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(9, 14))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(22, 18))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(14, 23))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(17, 14))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(10, 17))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(21, 14))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(2, 7))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(31, 27))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(6, 10))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(27, 18))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(10, 17))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(25, 21))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(1, 6))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(21, 14))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(6, 10))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(30, 25))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(10, 17))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(25, 21))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(19, 23))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(26, 19))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(17, 22))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(19, 15))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(22, 26))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(18, 14))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(26, 31))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(15, 10))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(5, 9))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(10, 3))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(9, 18))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(21, 17))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(18, 22))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(17, 14))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(22, 26))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(20, 16))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(12, 19))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(3, 12))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(26, 30))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(12, 16))
+ .thenReturn("no")
+
+ // Added moves that white wins
+ // RED
+ .thenReturn(fromNumbers(30, 26))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(16, 23))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(23, 30))
+ .thenReturn("no")
+ // RED
+ .thenReturn(fromNumbers(31, 27))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(32, 23))
+ .thenReturn("no");
+
+ gameLogic.run();
+
+ verify(console).print("Congratulations, player " + Player.PLAYER_WHITE + " has won");
+ }
+
+ @Test
+ void only_allow_multiple_jump_with_same_piece() {
+ // numbers were taken from: http://www.quadibloc.com/other/bo1211.htm
+ when(console.getUserInput())
+ // RED
+ .thenReturn(fromNumbers(11, 15))
+ // WHITE
+ .thenReturn(fromNumbers(23, 19))
+ // RED
+ .thenReturn(fromNumbers(8, 11))
+ // WHITE
+ .thenReturn(fromNumbers(22, 17))
+ // RED
+ .thenReturn(fromNumbers(11, 16))
+ // WHITE
+ .thenReturn(fromNumbers(24, 20))
+ // RED
+ .thenReturn(fromNumbers(16, 23))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(27, 18))
+ .thenReturn("no")
+ // WHITE
+ .thenReturn(fromNumbers(26, 11))
+ .thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, gameLogic::run);
+
+ verify(console).print("For a multiple jump move, the same piece has to be used. Try again");
+ }
+
+ @Test
+ void let_same_player_play_again_if_jumpgamble_is_won() {
+ when(coinTosser.toss(notNull(), notNull())).thenReturn(Result.HEADS);
+ // numbers were taken from: http://www.quadibloc.com/other/bo1211.htm
+ when(console.getUserInput())
+ // RED
+ .thenReturn(fromNumbers(11, 15))
+ // WHITE
+ .thenReturn(fromNumbers(23, 19))
+ // RED
+ .thenReturn(fromNumbers(8, 11))
+ // WHITE
+ .thenReturn(fromNumbers(22, 17))
+ // RED
+ .thenReturn(fromNumbers(11, 16))
+ // WHITE
+ .thenReturn(fromNumbers(24, 20))
+ // RED
+ .thenReturn(fromNumbers(16, 23))
+ .thenReturn("yes")
+ // RED
+ .thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, gameLogic::run);
+
+ verify(console).print("The gamble has been won, PLAYER_RED can play again.");
+ }
+
+ @Test
+ void switch_player_if_jumpgamble_is_lost() {
+ when(coinTosser.toss(notNull(), notNull())).thenReturn(Result.TAILS);
+ InOrder inOrder = inOrder(console);
+ // numbers were taken from: http://www.quadibloc.com/other/bo1211.htm
+ when(console.getUserInput())
+ // RED
+ .thenReturn(fromNumbers(11, 15))
+ // WHITE
+ .thenReturn(fromNumbers(23, 19))
+ // RED
+ .thenReturn(fromNumbers(8, 11))
+ // WHITE
+ .thenReturn(fromNumbers(22, 17))
+ // RED
+ .thenReturn(fromNumbers(11, 16))
+ // WHITE
+ .thenReturn(fromNumbers(24, 20))
+ // RED
+ .thenReturn(fromNumbers(16, 23))
+ .thenReturn("yes")
+ // RED
+ .thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, gameLogic::run);
+
+ inOrder.verify(console).print("Coin toss resulted in TAILS, the gamble was: LOST");
+ inOrder.verify(console).print("5 | [ ] [ ] [ ] [ ] [R_P] [ ] [ ] [ ] | 5");
+ }
+
+ @Test
+ void show_error_message_if_undo_is_done_at_start_of_the_game() {
+ when(console.getUserInput())
+ // RED
+ .thenReturn("undo")
+ .thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, gameLogic::run);
+
+ verify(console).print("There were no previous moves to undo. Please make a move.");
+ }
+
+ @Test
+ void undo_previous_turn_if_undo_was_used_at_start_of_turn() {
+ InOrder inOrder = inOrder(console);
+ when(console.getUserInput())
+ // RED
+ .thenReturn(fromNumbers(11, 15))
+ // WHITE
+ .thenReturn(fromNumbers(23, 19))
+ // RED
+ .thenReturn("undo")
+ .thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, gameLogic::run);
+
+ inOrder.verify(console).print("4 | [ ] [ ] [ ] [ ] [ ] [W_P] [ ] [ ] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [W_P] [ ] [ ] [ ] [W_P] [ ] | 3");
+ inOrder
+ .verify(console)
+ .print(
+ "PLAYER_RED, make your move. Or type 'undo' to go back to the start of the turn of PLAYER_WHITE");
+ inOrder.verify(console).print("4 | [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] | 3");
+ }
+
+ @Test
+ void undo_current_move_if_player_inputs_undo_during_turn() {
+ when(coinTosser.toss(notNull(), notNull())).thenReturn(Result.HEADS);
+ InOrder inOrder = inOrder(console);
+ // numbers were taken from: http://www.quadibloc.com/other/bo1211.htm
+ when(console.getUserInput())
+ // RED
+ .thenReturn(fromNumbers(11, 15))
+ // WHITE
+ .thenReturn(fromNumbers(23, 19))
+ // RED
+ .thenReturn(fromNumbers(8, 11))
+ // WHITE
+ .thenReturn(fromNumbers(22, 17))
+ // RED
+ .thenReturn(fromNumbers(11, 16))
+ // WHITE
+ .thenReturn(fromNumbers(24, 20))
+ // RED
+ .thenReturn(fromNumbers(16, 23))
+ .thenReturn("yes")
+ // RED
+ .thenReturn("undo")
+ .thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, gameLogic::run);
+
+ // start of move
+ inOrder.verify(console).print("5 | [ ] [ ] [ ] [ ] [R_P] [ ] [R_P] [ ] | 5");
+ inOrder.verify(console).print("4 | [ ] [W_P] [ ] [ ] [ ] [W_P] [ ] [W_P] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 3");
+ // jump move
+ inOrder.verify(console).print("5 | [ ] [ ] [ ] [ ] [R_P] [ ] [ ] [ ] | 5");
+ inOrder.verify(console).print("4 | [ ] [W_P] [ ] [ ] [ ] [ ] [ ] [W_P] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [ ] [ ] [R_P] [ ] [ ] [ ] | 3");
+ // undo
+ inOrder
+ .verify(console)
+ .print("PLAYER_RED, make your move. Or type 'undo' to go back to the start of your turn.");
+ // board back at same state as start
+ inOrder.verify(console).print("5 | [ ] [ ] [ ] [ ] [R_P] [ ] [R_P] [ ] | 5");
+ inOrder.verify(console).print("4 | [ ] [W_P] [ ] [ ] [ ] [W_P] [ ] [W_P] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 3");
+ }
+
+ private static String fromNumbers(int start, int end) {
+ List numberCoordinatesMap =
+ List.of(
+ "B8", "D8", "F8", "H8", "A7", "C7", "E7", "G7", "B6", "D6", "F6", "H6", "A5", "C5",
+ "E5", "G5", "B4", "D4", "F4", "H4", "A3", "C3", "E3", "G3", "B2", "D2", "F2", "H2",
+ "A1", "C1", "E1", "G1");
+ return String.format(
+ "%sx%s", numberCoordinatesMap.get(start - 1), numberCoordinatesMap.get(end - 1));
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/MoveExecutorTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/MoveExecutorTest.java
new file mode 100644
index 0000000..8bde14d
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/MoveExecutorTest.java
@@ -0,0 +1,115 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser;
+import ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser.Result;
+import ch.scs.jumpstart.pattern.examples.checkers.util.Console;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MoveExecutorTest {
+
+ private static final Move NORMAL_MOVE =
+ Move.of(Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ private static final Move JUMP_MOVE =
+ Move.of(Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ private static final Move JUMP_WITH_WON_GAMBLE_RESULT =
+ JUMP_MOVE.withJumpGambleResult(JumpGambleResult.WON);
+ private static final Move JUMP_WITH_LOST_GAMBLE_RESULT =
+ JUMP_MOVE.withJumpGambleResult(JumpGambleResult.LOST);
+ private CoinTosser coinTosser;
+ private Board board;
+ private MoveExecutor moveExecutor;
+ private Console console;
+
+ @BeforeEach
+ void setup() {
+ coinTosser = mock(CoinTosser.class);
+ board = mock(Board.class);
+ console = mock(Console.class);
+ moveExecutor = new MoveExecutor(board, coinTosser, console);
+ }
+
+ @Test
+ void execute_move_normally_if_no_jump_move() {
+ Move move = NORMAL_MOVE;
+
+ Move executedMove = moveExecutor.executeMove(move);
+
+ verify(board).executeMove(same(move));
+ assertThat(executedMove).isSameAs(move);
+ }
+
+ @Test
+ void ask_player_if_he_wants_to_gamble_for_jump_move() {
+ when(console.getUserInput()).thenThrow(RuntimeException.class);
+
+ Assertions.assertThrows(RuntimeException.class, () -> moveExecutor.executeMove(JUMP_MOVE));
+
+ verify(console).getUserInput();
+ }
+
+ @Test
+ void ask_player_if_he_wants_to_gamble_until_he_types_yes() {
+ when(console.getUserInput()).thenReturn("blabla").thenReturn("invalid").thenReturn("yes");
+
+ moveExecutor.executeMove(JUMP_MOVE);
+
+ verify(console, times(3)).getUserInput();
+ }
+
+ @Test
+ void ask_player_if_he_wants_to_gamble_until_he_types_no() {
+ when(console.getUserInput()).thenReturn("blabla").thenReturn("invalid").thenReturn("no");
+
+ moveExecutor.executeMove(JUMP_MOVE);
+
+ verify(console, times(3)).getUserInput();
+ }
+
+ @Test
+ void execute_move_normally_if_player_types_no() {
+ when(console.getUserInput()).thenReturn("no");
+
+ Move executedMove = moveExecutor.executeMove(JUMP_MOVE);
+
+ verify(board).executeMove(same(JUMP_MOVE));
+ assertThat(executedMove).isSameAs(JUMP_MOVE);
+ }
+
+ @Test
+ void execute_gamble_win_move_if_player_types_yes_and_wins() {
+ when(console.getUserInput()).thenReturn("yes");
+ when(coinTosser.toss(notNull(), notNull())).thenReturn(Result.HEADS);
+
+ Move executedMove = moveExecutor.executeMove(JUMP_MOVE);
+
+ verify(board).executeMove(eq(JUMP_WITH_WON_GAMBLE_RESULT));
+ assertThat(executedMove).isEqualTo(JUMP_WITH_WON_GAMBLE_RESULT);
+ }
+
+ @Test
+ void execute_gamble_lost_move_if_player_types_yes_and_loses() {
+ when(console.getUserInput()).thenReturn("yes");
+ when(coinTosser.toss(notNull(), notNull())).thenReturn(Result.TAILS);
+
+ Move executedMove = moveExecutor.executeMove(JUMP_MOVE);
+
+ verify(board).executeMove(eq(JUMP_WITH_LOST_GAMBLE_RESULT));
+ assertThat(executedMove).isEqualTo(JUMP_WITH_LOST_GAMBLE_RESULT);
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/WinConditionTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/WinConditionTest.java
new file mode 100644
index 0000000..150b532
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/WinConditionTest.java
@@ -0,0 +1,66 @@
+package ch.scs.jumpstart.pattern.examples.checkers;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import ch.scs.jumpstart.pattern.examples.checkers.movevalidator.MoveValidator;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class WinConditionTest {
+
+ private static final Piece WHITE_PAWN = new Piece(Player.PLAYER_WHITE, false);
+ private static final Piece RED_PAWN = new Piece(Player.PLAYER_RED, false);
+ private MoveValidator moveValidator1;
+ private MoveValidator moveValidator2;
+ private WinCondition winCondition;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ moveValidator1 = mock(MoveValidator.class);
+ moveValidator2 = mock(MoveValidator.class);
+ winCondition = new WinCondition(List.of(moveValidator1, moveValidator2));
+
+ board = mock(Board.class);
+ }
+
+ @Test
+ void player_has_won_if_other_player_has_no_pieces() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.empty());
+ when(board.getPieceAt(new BoardCoordinates(Row.ROW_4, Column.E)))
+ .thenReturn(Optional.of(RED_PAWN));
+
+ assertThat(winCondition.hasPlayerWon(Player.PLAYER_RED, board)).isTrue();
+ }
+
+ @Test
+ void player_has_not_won_if_other_player_can_move_a_piece() {
+ when(board.getPieceAt(new BoardCoordinates(Row.ROW_4, Column.E)))
+ .thenReturn(Optional.of(WHITE_PAWN));
+ when(moveValidator1.validate(notNull(), notNull())).thenReturn(true);
+ when(moveValidator2.validate(notNull(), notNull())).thenReturn(true);
+
+ assertThat(winCondition.hasPlayerWon(Player.PLAYER_RED, board)).isFalse();
+ }
+
+ @Test
+ void player_has_won_if_other_player_has_a_piece_but_cannot_move_it() {
+ when(board.getPieceAt(new BoardCoordinates(Row.ROW_4, Column.E)))
+ .thenReturn(Optional.of(WHITE_PAWN));
+ when(moveValidator1.validate(notNull(), notNull())).thenReturn(false);
+ when(moveValidator2.validate(notNull(), notNull())).thenReturn(true);
+
+ assertThat(winCondition.hasPlayerWon(Player.PLAYER_RED, board)).isTrue();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardTest.java
new file mode 100644
index 0000000..3d07b8c
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/BoardTest.java
@@ -0,0 +1,180 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+import static java.util.Optional.empty;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+
+class BoardTest {
+ @Test
+ void move_a_piece_from_a_to_b() {
+ Board board = new Board();
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.A),
+ new BoardCoordinates(Row.ROW_4, Column.B));
+
+ board.executeMove(move);
+
+ assertThat(board.getPieceAt(move.start())).isEmpty();
+ assertThat(board.getPieceAt(move.end()))
+ .isEqualTo(Optional.of(new Piece(Player.PLAYER_WHITE, false)));
+ }
+
+ @Test
+ void remove_piece_between_jump_move() {
+ Board board = new Board();
+ Move move1 =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.A),
+ new BoardCoordinates(Row.ROW_4, Column.B));
+ Move move2 =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_6, Column.B),
+ new BoardCoordinates(Row.ROW_5, Column.C));
+ Move move3 =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.C),
+ new BoardCoordinates(Row.ROW_4, Column.D));
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_5, Column.C),
+ new BoardCoordinates(Row.ROW_3, Column.A));
+
+ List.of(move1, move2, move3, jumpMove).forEach(board::executeMove);
+
+ assertThat(board.getPieceAt(move1.start()))
+ .isEqualTo(Optional.of(new Piece(Player.PLAYER_RED, false)));
+ assertThat(board.getPieceAt(move1.end())).isEqualTo(empty());
+ }
+
+ @Test
+ void convert_piece_to_king_if_at_end_for_white() {
+ Board board = new Board();
+ // this move is not valid, but the board doesn't know that
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.A),
+ new BoardCoordinates(Row.ROW_8, Column.B));
+
+ board.executeMove(move);
+
+ assertThat(board.getPieceAt(move.start())).isEqualTo(empty());
+ assertThat(board.getPieceAt(move.end()))
+ .isEqualTo(Optional.of(new Piece(Player.PLAYER_WHITE, true)));
+ }
+
+ @Test
+ void convert_piece_to_king_if_at_end_for_red() {
+ Board board = new Board();
+ // this move is not valid, but the board doesn't know that
+ Move move =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_6, Column.B),
+ new BoardCoordinates(Row.ROW_1, Column.B));
+
+ board.executeMove(move);
+
+ assertThat(board.getPieceAt(move.start())).isEqualTo(empty());
+ assertThat(board.getPieceAt(move.end()))
+ .isEqualTo(Optional.of(new Piece(Player.PLAYER_RED, true)));
+ }
+
+ @Test
+ void piece_stays_king_if_it_was_king() {
+ Board board = new Board();
+ // this move is not valid, but the board doesn't know that
+ Move move1 =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_6, Column.B),
+ new BoardCoordinates(Row.ROW_1, Column.B));
+ Move move2 =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_1, Column.B),
+ new BoardCoordinates(Row.ROW_2, Column.C));
+
+ List.of(move1, move2).forEach(board::executeMove);
+
+ assertThat(board.getPieceAt(move1.start())).isEmpty();
+ assertThat(board.getPieceAt(move1.end())).isEmpty();
+ assertThat(board.getPieceAt(move2.end()))
+ .isEqualTo(Optional.of(new Piece(Player.PLAYER_RED, true)));
+ }
+
+ @Test
+ void remove_start_piece_when_JumpGambleResult_is_lost() {
+ Board board = new Board();
+ Move move1 =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.A),
+ new BoardCoordinates(Row.ROW_4, Column.B));
+ Move move2 =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_6, Column.B),
+ new BoardCoordinates(Row.ROW_5, Column.C));
+ Move move3 =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.C),
+ new BoardCoordinates(Row.ROW_4, Column.D));
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_5, Column.C),
+ new BoardCoordinates(Row.ROW_3, Column.A))
+ .withJumpGambleResult(JumpGambleResult.LOST);
+
+ List.of(move1, move2, move3, jumpMove).forEach(board::executeMove);
+
+ assertThat(board.getPieceAt(jumpMove.start())).isEmpty();
+ assertThat(board.getPieceAt(jumpMove.getCoordinatesBetween().orElseThrow())).isNotEmpty();
+ }
+
+ @Test
+ void execute_normal_jump_move_if_JumpGambleResult_is_won() {
+ Board board = new Board();
+ Move move1 =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.A),
+ new BoardCoordinates(Row.ROW_4, Column.B));
+ Move move2 =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_6, Column.B),
+ new BoardCoordinates(Row.ROW_5, Column.C));
+ Move move3 =
+ Move.of(
+ Player.PLAYER_WHITE,
+ new BoardCoordinates(Row.ROW_3, Column.C),
+ new BoardCoordinates(Row.ROW_4, Column.D));
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_RED,
+ new BoardCoordinates(Row.ROW_5, Column.C),
+ new BoardCoordinates(Row.ROW_3, Column.A))
+ .withJumpGambleResult(JumpGambleResult.WON);
+
+ List.of(move1, move2, move3, jumpMove).forEach(board::executeMove);
+
+ assertThat(board.getPieceAt(move1.start()))
+ .isEqualTo(Optional.of(new Piece(Player.PLAYER_RED, false)));
+ assertThat(board.getPieceAt(move1.end())).isEmpty();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/MoveTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/MoveTest.java
new file mode 100644
index 0000000..eabf88d
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/MoveTest.java
@@ -0,0 +1,168 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class MoveTest {
+ @Test
+ void throw_for_empty_string() {
+ assertThrows(IllegalArgumentException.class, () -> Move.parse(Player.PLAYER_WHITE, ""));
+ }
+
+ @Test
+ void throw_if_string_contains_only_delimiter() {
+ assertThrows(IllegalArgumentException.class, () -> Move.parse(Player.PLAYER_WHITE, "x"));
+ }
+
+ @Test
+ void throw_if_string_contains_only_move_start() {
+ assertThrows(IllegalArgumentException.class, () -> Move.parse(Player.PLAYER_WHITE, "a3x"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "a9xb3", "i9xb3", "y9xb3", "a9x33", "a9xä3",
+ })
+ void throw_if_string_contains_invalid_column(String input) {
+ assertThrows(IllegalArgumentException.class, () -> Move.parse(Player.PLAYER_WHITE, input));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "a9xb3", "a0xb3", "a-1xb3", "a0.5xb3", "a xb3",
+ })
+ void throw_if_string_contains_invalid_row(String input) {
+ assertThrows(IllegalArgumentException.class, () -> Move.parse(Player.PLAYER_WHITE, input));
+ }
+
+ @Test
+ void parse_correct_move() {
+ Move expectedMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_3, A), new BoardCoordinates(ROW_4, B));
+ assertThat(Move.parse(Player.PLAYER_WHITE, "a3xb4")).isEqualTo(expectedMove);
+ assertThat(Move.parse(Player.PLAYER_WHITE, "A3xB4")).isEqualTo(expectedMove);
+ assertThat(Move.parse(Player.PLAYER_WHITE, "a3Xb4")).isEqualTo(expectedMove);
+ assertThat(Move.parse(Player.PLAYER_WHITE, "[a3]Xb4")).isEqualTo(expectedMove);
+ assertThat(Move.parse(Player.PLAYER_WHITE, "[a3Xb4")).isEqualTo(expectedMove);
+ assertThat(Move.parse(Player.PLAYER_WHITE, "[a3]X[b4]")).isEqualTo(expectedMove);
+ }
+
+ @Test
+ void generate_possible_moves() {
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_4, D), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(8);
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_1, A), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(2);
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_8, A), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(2);
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_8, H), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(2);
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_1, H), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(2);
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_1, G), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(3);
+ assertThat(
+ Move.generatePossibleMoves(
+ new BoardCoordinates(ROW_1, F), Player.PLAYER_WHITE, List.of(1, 2)))
+ .hasSize(4);
+ }
+
+ @Test
+ void return_empty_for_getCoordinatesBetween_for_simple_move() {
+ Move simpleMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(simpleMove.getCoordinatesBetween()).isEmpty();
+ }
+
+ @Test
+ void calculate_correct_coordinates_between_for_jump_move() {
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ assertThat(jumpMove.getCoordinatesBetween())
+ .isEqualTo(Optional.of(new BoardCoordinates(ROW_2, B)));
+ }
+
+ @Test
+ void return_true_for_isJumpMove_on_jump_move() {
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ assertThat(jumpMove.isJumpMove()).isTrue();
+ }
+
+ @Test
+ void return_false_for_isJumpMove_on_move_from_same_position_to_same_position() {
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, A));
+ assertThat(jumpMove.isJumpMove()).isFalse();
+ }
+
+ @Test
+ void return_false_for_isJumpMove_on_simple_move() {
+ Move simpleMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(simpleMove.isJumpMove()).isFalse();
+ }
+
+ @Test
+ void throw_if_withJumpGambleResult_called_with_WON_on_simple_move() {
+ Move simpleMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThrows(IllegalArgumentException.class, () -> simpleMove.withJumpGambleResult(WON));
+ }
+
+ @Test
+ void throw_if_withJumpGambleResult_called_with_LOST_on_simple_move() {
+ Move simpleMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThrows(IllegalArgumentException.class, () -> simpleMove.withJumpGambleResult(LOST));
+ }
+
+ @Test
+ void not_change_anything_if_withJumpGambleResult_called_with_NO_GAMBLE_on_simple_move() {
+ Move simpleMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(simpleMove.withJumpGambleResult(NO_GAMBLE)).isEqualTo(simpleMove);
+ }
+
+ @Test
+ void change_to_jumpGambleResult_given_with_withJumpGambleResult() {
+ Move jumpMove =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ assertThat(jumpMove.withJumpGambleResult(WON).jumpGambleResult()).isEqualTo(WON);
+ assertThat(jumpMove.withJumpGambleResult(NO_GAMBLE).jumpGambleResult()).isEqualTo(NO_GAMBLE);
+ assertThat(jumpMove.withJumpGambleResult(LOST).jumpGambleResult()).isEqualTo(LOST);
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/StoreTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/StoreTest.java
new file mode 100644
index 0000000..37ab7f8
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/dom/board/StoreTest.java
@@ -0,0 +1,62 @@
+package ch.scs.jumpstart.pattern.examples.checkers.dom.board;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class StoreTest {
+ private static final Piece WHITE_PIECE = new Piece(Player.PLAYER_WHITE, false);
+ private static final BoardCoordinates EMPTY_COORDINATES_AT_START = new BoardCoordinates(ROW_4, A);
+ private static final BoardCoordinates OCCUPIED_COORDINATES_AT_START =
+ new BoardCoordinates(ROW_6, B);
+ private static final Piece RED_PIECE = new Piece(Player.PLAYER_RED, false);
+ private Store store;
+
+ @BeforeEach
+ void setup() {
+ store = new Store();
+ }
+
+ @Test
+ void add_piece_on_empty_place() {
+ assertThat(store.getPieceAt(EMPTY_COORDINATES_AT_START)).isEmpty();
+ store.addPiece(EMPTY_COORDINATES_AT_START, WHITE_PIECE);
+ assertThat(store.getPieceAt(EMPTY_COORDINATES_AT_START)).isEqualTo(Optional.of(WHITE_PIECE));
+ }
+
+ @Test
+ void add_piece_on_occupied_place() {
+ assertThat(store.getPieceAt(OCCUPIED_COORDINATES_AT_START)).isEqualTo(Optional.of(RED_PIECE));
+ store.addPiece(OCCUPIED_COORDINATES_AT_START, WHITE_PIECE);
+ assertThat(store.getPieceAt(OCCUPIED_COORDINATES_AT_START)).isEqualTo(Optional.of(WHITE_PIECE));
+ }
+
+ @Test
+ void remove_piece_at_empty_place() {
+ store.removePiece(EMPTY_COORDINATES_AT_START);
+ assertThat(store.getPieceAt(EMPTY_COORDINATES_AT_START)).isEmpty();
+ }
+
+ @Test
+ void remove_piece_at_occupied_place() {
+ store.removePiece(OCCUPIED_COORDINATES_AT_START);
+ assertThat(store.getPieceAt(OCCUPIED_COORDINATES_AT_START)).isEmpty();
+ }
+
+ @Test
+ void get_piece_of_empty_place() {
+ assertThat(store.getPieceAt(EMPTY_COORDINATES_AT_START)).isEmpty();
+ }
+
+ @Test
+ void get_piece_at_occupied_place() {
+ assertThat(store.getPieceAt(OCCUPIED_COORDINATES_AT_START)).isEqualTo(Optional.of(RED_PIECE));
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsDiagonalTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsDiagonalTest.java
new file mode 100644
index 0000000..3a2cc5c
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveIsDiagonalTest.java
@@ -0,0 +1,97 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MoveIsDiagonalTest {
+
+ private MoveIsDiagonal moveIsDiagonal;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ moveIsDiagonal = new MoveIsDiagonal();
+ board = new Board();
+ }
+
+ @Test
+ void return_true_for_diagonal_move_up_right() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_for_diagonal_move_up_left() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, B), new BoardCoordinates(ROW_2, A));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_for_diagonal_move_down_right() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_2, A), new BoardCoordinates(ROW_1, B));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_for_diagonal_move_down_left() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_2, B), new BoardCoordinates(ROW_1, A));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_for_diagonal_jump_move_up_right() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_for_diagonal_jump_move_down_left() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_3, C), new BoardCoordinates(ROW_1, A));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_for_move_to_the_right() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, B));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_false_for_move_up() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, A));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_false_for_move_one_up_and_two_right() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, C));
+
+ assertThat(moveIsDiagonal.validate(move, board)).isFalse();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveLengthTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveLengthTest.java
new file mode 100644
index 0000000..2b8cfe4
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/MoveLengthTest.java
@@ -0,0 +1,109 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MoveLengthTest {
+ private MoveLength moveLength;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ moveLength = new MoveLength();
+ board = new Board();
+ }
+
+ @Test
+ void return_true_if_player_moves_1_row() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, A));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_player_moves_2_row() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, A));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_player_moves_3_row() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_4, A));
+ assertThat(moveLength.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_true_if_player_moves_7_row() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_8, A));
+ assertThat(moveLength.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_true_if_player_moves_1_col() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, B));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_player_moves_2_col() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, C));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_player_moves_3_col() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, D));
+ assertThat(moveLength.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_true_if_player_moves_7_col() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, H));
+ assertThat(moveLength.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_true_if_player_moves_1_row_back() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_2, A), new BoardCoordinates(ROW_1, A));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_player_moves_1_col_back() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, B), new BoardCoordinates(ROW_1, A));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_player_moves_1_col_and_2_rows() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, B), new BoardCoordinates(ROW_3, A));
+ assertThat(moveLength.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_move_does_not_change_position() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_1, A));
+ assertThat(moveLength.validate(move, board)).isFalse();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/NoOtherMoveToJumpPossibleTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/NoOtherMoveToJumpPossibleTest.java
new file mode 100644
index 0000000..c599197
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/NoOtherMoveToJumpPossibleTest.java
@@ -0,0 +1,100 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class NoOtherMoveToJumpPossibleTest {
+ private static final Piece WHITE_PAWN = new Piece(Player.PLAYER_WHITE, false);
+ private static final Piece RED_PAWN = new Piece(Player.PLAYER_RED, false);
+ private static final Piece RED_KING = new Piece(Player.PLAYER_RED, true);
+
+ private NoOtherMoveToJumpPossible noOtherMoveToJumpPossible;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ noOtherMoveToJumpPossible =
+ new NoOtherMoveToJumpPossible(
+ new MoveIsForwardIfNotKing(), new OpponentPieceBetweenJump(), new TargetFieldEmpty());
+ board = mock(Board.class);
+ }
+
+ @Test
+ void return_true_if_no_other_pieces_exist() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(noOtherMoveToJumpPossible.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_move_is_jump_move() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ assertThat(noOtherMoveToJumpPossible.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void does_not_crash_if_piece_is_at_edge() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_1, A))).thenReturn(Optional.of(WHITE_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_8, A))).thenReturn(Optional.of(WHITE_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_1, G))).thenReturn(Optional.of(WHITE_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_8, G))).thenReturn(Optional.of(WHITE_PAWN));
+
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(noOtherMoveToJumpPossible.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_other_jump_move_possible_for_white() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_4, E))).thenReturn(Optional.of(WHITE_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_5, F))).thenReturn(Optional.of(RED_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_6, G))).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(noOtherMoveToJumpPossible.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_false_if_other_jump_move_possible_for_red() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_6, G))).thenReturn(Optional.of(RED_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_5, F))).thenReturn(Optional.of(WHITE_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_4, E))).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(noOtherMoveToJumpPossible.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_false_if_other_jump_move_with_king_possible_for_red() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_4, E))).thenReturn(Optional.of(RED_KING));
+ when(board.getPieceAt(new BoardCoordinates(ROW_5, F))).thenReturn(Optional.of(WHITE_PAWN));
+ when(board.getPieceAt(new BoardCoordinates(ROW_6, G))).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(noOtherMoveToJumpPossible.validate(move, board)).isFalse();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/OpponentPieceBetweenJumpTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/OpponentPieceBetweenJumpTest.java
new file mode 100644
index 0000000..f651157
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/OpponentPieceBetweenJumpTest.java
@@ -0,0 +1,86 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class OpponentPieceBetweenJumpTest {
+ private static final Piece WHITE_PAWN = new Piece(Player.PLAYER_WHITE, false);
+ private static final Piece RED_PAWN = new Piece(Player.PLAYER_RED, false);
+
+ private OpponentPieceBetweenJump opponentPieceBetweenJump;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ opponentPieceBetweenJump = new OpponentPieceBetweenJump();
+ board = mock(Board.class);
+ }
+
+ @Test
+ void return_true_if_no_jump() {
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+ assertThat(opponentPieceBetweenJump.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_no_piece_between_jump() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_2, B))).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ assertThat(opponentPieceBetweenJump.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_true_if_piece_between_belongs_to_opponent_white() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_2, B))).thenReturn(Optional.of(RED_PAWN));
+
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_3, A), new BoardCoordinates(ROW_1, C));
+ assertThat(opponentPieceBetweenJump.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_piece_between_belongs_to_opponent_red() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_2, B))).thenReturn(Optional.of(WHITE_PAWN));
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, C), new BoardCoordinates(ROW_3, A));
+ assertThat(opponentPieceBetweenJump.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_piece_between_belongs_to_same_player_white() {
+ when(board.getPieceAt(new BoardCoordinates(ROW_2, B))).thenReturn(Optional.of(WHITE_PAWN));
+
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_3, C), new BoardCoordinates(ROW_1, A));
+ assertThat(opponentPieceBetweenJump.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_false_if_piece_between_belongs_to_same_player_red() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.of(RED_PAWN));
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_3, C));
+ assertThat(opponentPieceBetweenJump.validate(move, board)).isFalse();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/StartPieceValidTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/StartPieceValidTest.java
new file mode 100644
index 0000000..a3dea96
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/StartPieceValidTest.java
@@ -0,0 +1,84 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class StartPieceValidTest {
+ static final Piece WHITE_PAWN = new Piece(Player.PLAYER_WHITE, false);
+ static final Piece RED_PAWN = new Piece(Player.PLAYER_RED, false);
+
+ private StartPieceValid startPieceValid;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ startPieceValid = new StartPieceValid();
+ board = mock(Board.class);
+ }
+
+ @Test
+ void return_false_if_no_piece() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+
+ assertThat(startPieceValid.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_true_if_piece_belongs_to_player_red() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.of(RED_PAWN));
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+
+ assertThat(startPieceValid.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_true_if_piece_belongs_to_player_white() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.of(WHITE_PAWN));
+
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+
+ assertThat(startPieceValid.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_piece_belongs_to_opponent_player_white() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.of(RED_PAWN));
+
+ Move move =
+ Move.of(
+ Player.PLAYER_WHITE, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+
+ assertThat(startPieceValid.validate(move, board)).isFalse();
+ }
+
+ @Test
+ void return_false_if_piece_belongs_to_opponent_player_red() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.of(WHITE_PAWN));
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, B));
+
+ assertThat(startPieceValid.validate(move, board)).isFalse();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/TargetFieldEmptyTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/TargetFieldEmptyTest.java
new file mode 100644
index 0000000..e290b25
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/movevalidator/TargetFieldEmptyTest.java
@@ -0,0 +1,49 @@
+package ch.scs.jumpstart.pattern.examples.checkers.movevalidator;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row.*;
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.JumpGambleResult.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Move;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TargetFieldEmptyTest {
+ public static final Piece PIECE = new Piece(Player.PLAYER_WHITE, false);
+
+ private TargetFieldEmpty targetFieldEmpty;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ targetFieldEmpty = new TargetFieldEmpty();
+ board = mock(Board.class);
+ }
+
+ @Test
+ void return_true_if_target_field_empty() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.empty());
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, A));
+ assertThat(targetFieldEmpty.validate(move, board)).isTrue();
+ }
+
+ @Test
+ void return_false_if_target_field_not_empty() {
+ when(board.getPieceAt(notNull())).thenReturn(Optional.of(PIECE));
+
+ Move move =
+ Move.of(Player.PLAYER_RED, new BoardCoordinates(ROW_1, A), new BoardCoordinates(ROW_2, A));
+ assertThat(targetFieldEmpty.validate(move, board)).isFalse();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/BoardPrinterTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/BoardPrinterTest.java
new file mode 100644
index 0000000..55db415
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/BoardPrinterTest.java
@@ -0,0 +1,71 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import static org.mockito.Mockito.*;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Column;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.BoardCoordinates.Row;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+
+class BoardPrinterTest {
+
+ private Board board;
+ private Console console;
+ private BoardPrinter boardPrinter;
+ private InOrder inOrder;
+
+ @BeforeEach
+ void setup() {
+ board = spy(new Board());
+ console = mock(Console.class);
+ boardPrinter = new BoardPrinter(console);
+ inOrder = inOrder(console);
+ }
+
+ @Test
+ void prints_initial_board_state() {
+ boardPrinter.printBoard(board);
+
+ inOrder.verify(console).print(" a b c d e f g h ");
+ inOrder.verify(console).print(" +-------------------------------------------------+");
+ inOrder.verify(console).print("8 | [ ] [R_P] [ ] [R_P] [ ] [R_P] [ ] [R_P] | 8");
+ inOrder.verify(console).print("7 | [R_P] [ ] [R_P] [ ] [R_P] [ ] [R_P] [ ] | 7");
+ inOrder.verify(console).print("6 | [ ] [R_P] [ ] [R_P] [ ] [R_P] [ ] [R_P] | 6");
+ inOrder.verify(console).print("5 | [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 5");
+ inOrder.verify(console).print("4 | [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] | 3");
+ inOrder.verify(console).print("2 | [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] [W_P] | 2");
+ inOrder.verify(console).print("1 | [W_P] [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] | 1");
+ inOrder.verify(console).print(" +-------------------------------------------------+");
+ inOrder.verify(console).print(" a b c d e f g h ");
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ void print_king() {
+ when(board.getPieceAt(new BoardCoordinates(Row.ROW_1, Column.A)))
+ .thenReturn(Optional.of(new Piece(Player.PLAYER_WHITE, true)));
+
+ boardPrinter.printBoard(board);
+
+ inOrder.verify(console).print(" a b c d e f g h ");
+ inOrder.verify(console).print(" +-------------------------------------------------+");
+ inOrder.verify(console).print("8 | [ ] [R_P] [ ] [R_P] [ ] [R_P] [ ] [R_P] | 8");
+ inOrder.verify(console).print("7 | [R_P] [ ] [R_P] [ ] [R_P] [ ] [R_P] [ ] | 7");
+ inOrder.verify(console).print("6 | [ ] [R_P] [ ] [R_P] [ ] [R_P] [ ] [R_P] | 6");
+ inOrder.verify(console).print("5 | [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 5");
+ inOrder.verify(console).print("4 | [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] | 4");
+ inOrder.verify(console).print("3 | [W_P] [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] | 3");
+ inOrder.verify(console).print("2 | [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] [W_P] | 2");
+ inOrder.verify(console).print("1 | [W_K] [ ] [W_P] [ ] [W_P] [ ] [W_P] [ ] | 1");
+ inOrder.verify(console).print(" +-------------------------------------------------+");
+ inOrder.verify(console).print(" a b c d e f g h ");
+ inOrder.verifyNoMoreInteractions();
+ }
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/CoinTosserTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/CoinTosserTest.java
new file mode 100644
index 0000000..d054c26
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/CoinTosserTest.java
@@ -0,0 +1,88 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.Player.PLAYER_RED;
+import static ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser.Result.HEADS;
+import static ch.scs.jumpstart.pattern.examples.checkers.util.CoinTosser.Result.TAILS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Player;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.IntStream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class CoinTosserTest {
+
+ private PointsCalculator pointsCalculator;
+ private Random random;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ pointsCalculator = mock(PointsCalculator.class);
+ random = mock(RandomWrapper.class);
+ board = mock(Board.class);
+ }
+
+ @Test
+ void return_heads_and_tails_evenly_distributed() {
+ when(pointsCalculator.calculatePoints(notNull()))
+ .thenReturn(Map.of(PLAYER_RED, 1, Player.PLAYER_WHITE, 1));
+ CoinTosser coinTosser = new CoinTosser(pointsCalculator);
+ long count =
+ IntStream.range(0, 100)
+ .mapToObj(__ -> coinTosser.toss(board, PLAYER_RED))
+ .filter(HEADS::equals)
+ .count();
+ // use assume because of the random element
+ assumeTrue(count < 70);
+ assumeTrue(count > 30);
+ }
+
+ @Test
+ void use_even_chances_if_points_are_evenly_distributed() {
+ when(pointsCalculator.calculatePoints(notNull()))
+ .thenReturn(Map.of(PLAYER_RED, 1, Player.PLAYER_WHITE, 1));
+ CoinTosser coinTosser = new CoinTosser(pointsCalculator, random);
+
+ when(random.nextFloat()).thenReturn(0.5f);
+ assertThat(coinTosser.toss(board, PLAYER_RED)).isEqualTo(HEADS);
+
+ when(random.nextFloat()).thenReturn(0.51f);
+ assertThat(coinTosser.toss(board, PLAYER_RED)).isEqualTo(TAILS);
+ }
+
+ @Test
+ void increase_the_chance_for_the_weaker_player() {
+ when(pointsCalculator.calculatePoints(notNull()))
+ .thenReturn(Map.of(PLAYER_RED, 1, Player.PLAYER_WHITE, 2));
+ CoinTosser coinTosser = new CoinTosser(pointsCalculator, random);
+
+ when(random.nextFloat()).thenReturn(0.6666f);
+ assertThat(coinTosser.toss(board, PLAYER_RED)).isEqualTo(HEADS);
+
+ when(random.nextFloat()).thenReturn(0.66667f);
+ assertThat(coinTosser.toss(board, PLAYER_RED)).isEqualTo(TAILS);
+ }
+
+ @Test
+ void decrease_the_chance_for_the_stronger_player() {
+ when(pointsCalculator.calculatePoints(notNull()))
+ .thenReturn(Map.of(PLAYER_RED, 2, Player.PLAYER_WHITE, 1));
+ CoinTosser coinTosser = new CoinTosser(pointsCalculator, random);
+
+ when(random.nextFloat()).thenReturn(0.3333f);
+ assertThat(coinTosser.toss(board, PLAYER_RED)).isEqualTo(HEADS);
+
+ when(random.nextFloat()).thenReturn(0.33334f);
+ assertThat(coinTosser.toss(board, PLAYER_RED)).isEqualTo(TAILS);
+ }
+
+ private static class RandomWrapper extends Random {}
+}
diff --git a/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/PointsCalculatorTest.java b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/PointsCalculatorTest.java
new file mode 100644
index 0000000..a6cbc08
--- /dev/null
+++ b/topics/sw_concepts/code/pattern-examples/src/test/java/ch/scs/jumpstart/pattern/examples/checkers/util/PointsCalculatorTest.java
@@ -0,0 +1,46 @@
+package ch.scs.jumpstart.pattern.examples.checkers.util;
+
+import static ch.scs.jumpstart.pattern.examples.checkers.dom.Player.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.*;
+
+import ch.scs.jumpstart.pattern.examples.checkers.dom.Piece;
+import ch.scs.jumpstart.pattern.examples.checkers.dom.board.Board;
+import java.util.Map;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class PointsCalculatorTest {
+ private PointsCalculator pointsCalculator;
+ private Board board;
+
+ @BeforeEach
+ void setup() {
+ pointsCalculator = new PointsCalculator();
+ board = spy(new Board());
+ }
+
+ @Test
+ void calculate_points_correctly_at_start() {
+ assertThat(pointsCalculator.calculatePoints(board))
+ .isEqualTo(Map.of(PLAYER_WHITE, 12, PLAYER_RED, 12));
+ }
+
+ @Test
+ void calculate_points_if_no_pieces_are_on_board() {
+ doReturn(Optional.empty()).when(board).getPieceAt(notNull());
+ assertThat(pointsCalculator.calculatePoints(board))
+ .isEqualTo(Map.of(PLAYER_WHITE, 0, PLAYER_RED, 0));
+ }
+
+ @Test
+ void use_2_as_value_for_kings() {
+ doReturn(Optional.of(new Piece(PLAYER_WHITE, true)), Optional.of(new Piece(PLAYER_RED, true)))
+ .when(board)
+ .getPieceAt(notNull());
+ assertThat(pointsCalculator.calculatePoints(board))
+ .isEqualTo(Map.of(PLAYER_WHITE, 2, PLAYER_RED, 126));
+ }
+}
diff --git a/topics/sw_concepts/sw_concept_code_examples.md b/topics/sw_concepts/sw_concept_code_examples.md
deleted file mode 100644
index 06e50d5..0000000
--- a/topics/sw_concepts/sw_concept_code_examples.md
+++ /dev/null
@@ -1,410 +0,0 @@
-<#include meta/slides.md>
-
----
-title: "SW Konzepte Code Beispiele"
-date: \today
----
-
-Code Beispiele
--------
-
-Überlege für die folgenden Code Beispiele folgendes:
-
-1. Hat das Beispiel etwas mit DRY, KISS, YAGNI oder NIH zu tun?
-2. Hat es mit den SOLID Principles zu tun?
-3. Welche Design Pattern siehst du?
-4. Was könnte sonst noch verbessert werden?
-
-OrganisationSupplier
--------
-
-```python
-class OrganisationSupplier:
- def get_current_organisation(self):
- pass
-
-class ConfigurationOrganisationSupplier(OrganisationSupplier):
- def __init__(self, device_location_dao, konfiguration_dao, default_organisation):
- self.konfiguration_dao = konfiguration_dao
- self.default_organisation = default_organisation
-
- def get_current_organisation(self):
- return self.konfiguration_dao.get_konfigurationKonfigurationDAO.ORGANISATION_ID, self.default_organisation)
-
-class DeviceLocationOrganisationSupplier(OrganisationSupplier):
- def __init__(self, konfiguration_dao, device_location_dao):
- self.konfiguration_dao = konfiguration_dao
- self.device_location_dao = device_location_dao
-
- def get_current_organisation(self):
- return Optional.ofNullable(self.konfiguration_dao.get_konfiguration(KonfigurationDAO.STANDORT_KENNUNG, None)) \
- .map(int) \
- .map(self.device_location_dao.get_device_location_by_id) \
- .map(lambda device_location_entity: device_location_entity.get_organisation())
-```
-
-OrganisationSupplier Auswertung
--------
-
-1. DRY...: Property Keys werden geteilt (e.g. STANDORT_KENNUNG)
-2. SOLID: nicht speziell
-3. Pattern: Strategy
-4. Verbesserung: -
-
-AtmelObservableFactory
--------
-
-```python
-class AtmelObservableFactory:
- @staticmethod
- def create_power_state(host, port, scheduler):
- return AtmelObservableFactory.create_power_state_with_interval(host, port, INTERVAL, scheduler)
-
- @staticmethod
- def create_power_state_with_interval(host, port, interval, scheduler):
- simple_request = SimpleRequest("inputGetIgnitionState")
- ignition_state_request_executor = AtmelRequestExecutor(host, port, simple_request, IgnitionStateResult)
- return PollingObservable.create(scheduler, lambda: ..., interval, scheduler)
-
- @staticmethod
- def create_system_state(host, port, scheduler):
- return AtmelObservableFactory.create_system_state_with_interval(host, port, INTERVAL, scheduler)
-
- @staticmethod
- def create_system_state_with_interval(host, port, interval, scheduler):
- simple_request = SimpleRequest("registerObject", ["InfovisionSystemState"])
- return RetryUntilSuccess.create(AtmelRequestExecutor(host, port, simple_request, SystemStateResult), lambda: interval, scheduler)
-```
-
-AtmelObservableFactory Auswertung
--------
-
-1. DRY...: YAGNI: muss das Interval konfigurierbar sein?
-2. SOLID: Single Responsibility: muss beides hier erstellt werden?
-3. Pattern: AbstractFactory, Factory method
-4. Verbesserung: -
-
-BrightnessParserTest
--------
-
-```python
-def test_parses_brightness_correctly(self):
- windows_brightness_output = """
- Power Setting GUID: 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e (Turn off display after)
- Minimum Possible Setting: 0x00000000
- Maximum Possible Setting: 0xffffffff
- Possible Settings increment: 0x00000001
- Current AC Power Setting Index: 0x00000000
- Current DC Power Setting Index: 0x00003840
-
- Power Setting GUID: aded5e82-b909-4619-9949-f5d71dac0bcb (Display brightness)
- Minimum Possible Setting: 0x00000000
- Maximum Possible Setting: 0x00000064
- Possible Settings increment: 0x00000001
- Possible Settings units: %
- Current AC Power Setting Index: 0x0000005a
- Current DC Power Setting Index: 0x0000005a
-"""
- self.assertEqual(parse(windows_brightness_output), "0000005a")
-```
-
-BrightnessParserTest Auswertung
--------
-
-1. DRY...: "0000005a" könnte geteilt und benannt werden
-2. SOLID: -
-3. Pattern: -
-4. Verbesserung: -
-
-DoorState
--------
-
-```java
-private synchronized void setDoorState(DoorState newDoorState,
- DoorComponent doorComponent) {
- doorContext.setDoorState(newDoorState, doorComponent);
- if (doorContext.isInconsistent()) {
- observers.forEach(DoorObserver::inconsistentSensors);
- } else if (DoorState.OPEN.equals(newDoorState)) {
- observers.forEach(DoorObserver::switchOpened);
- } else if (DoorState.CLOSED.equals(newDoorState) && doorContext.isDoorCompletelyClosed()) {
- observers.forEach(DoorObserver::doorClosed);
- }
- }
-```
-
-DoorState Auswertung
--------
-
-1. DRY...: -
-2. SOLID: Open/Closed: Für alle neuen Events muss das if statement und das DoorObserver Interface geändert werden.
-3. Pattern: Observer
-4. Verbesserung: -
-
-TransactionRepository
--------
-
-```java
-public interface TransactionRepository {
- Transaction addTransactions(Collection transactions);
-
- void updateTransactions(Collection transactions);
-
- Optional findTransaction(UUID uuid);
-}
-
-HibernateTransactionRepository implements TransactionRepository {}
-
-public class TransactionObserverTest {
- private TransactionObserver transactionObserver;
- private TransactionRepository transactionRepository;
- @Before
- public void givenTransactionObserver() {
- transactionRepository = mock(TransactionRepository.class);
- transactionObserver = new TransactionObserver(transactionRepository, createTerminalInfoExporter(), createLocationInfoExporter());
- }
-```
-
-TransactionRepository Auswertung
--------
-
-1. DRY...: KISS: Ein Mock kann auch einfach mit einer Klasse umgesetzt werden
-2. SOLID: Dependency Inversion
-3. Pattern: -
-4. Verbesserung: -
-
-CreditCardAppIdMapperProvider
--------
-
-```java
-public class CreditCardAppIdMapperProvider implements Provider {
- private final CreditCardAppIdMapper creditCardAppIdMapper;
-
- public CreditCardAppIdMapperProvider(CreditCardAppIdMappingConfiguration configuration,
- CreditCardAppIdCategoryMappingParser creditCardAppIdCategoryMappingParser) {
- if (configuration.isEnabled()) {
- List mappingEntries = creditCardAppIdCategoryMappingParser.parse(new File(configuration.getMappingFilePath()));
- creditCardAppIdMapper = new CreditCardAppIdMapper(createMap(mappingEntries));
- } else {
- creditCardAppIdMapper = new CreditCardAppIdMapper(new HashMap<>());
- }
- }
-
- public CreditCardAppIdMapper get() {
- return creditCardAppIdMapper;
- }
- //...
-}
-```
-
-CreditCardAppIdMapperProvider Auswertung
--------
-
-1. DRY...: -
-2. SOLID: -
-3. Pattern: Factory, [Null Object](https://github.com/iluwatar/java-design-patterns/tree/master/null-object)
-4. Verbesserung: Das parsen des Files (Input/Output oder I/O) im Konstruktor
-kann das aufstarten Verzögern. Das könnte man wenn möglich asynchron machen.
-
-Board
--------
-
-```java
-public class Board {
- private final List boardObservers = new ArrayList<>();
-
- public void executeMove(Move move) {
- Command command = createCommand(move);
- command.execute();
- boardObservers.forEach(boardObserver -> boardObserver.boardChanged(this));
- }
-
- public void registerObserver(BoardObserver boardObserver) {
- boardObservers.add(boardObserver);
- }
-}
-```
-
-Board Auswertung
--------
-
-1. DRY...: -
-2. SOLID: -
-3. Pattern: Observer
-4. Verbesserung: Keine Möglichkeit um Observer zu entfernen $\rightarrow$ Memory Leak
-
-Board2
--------
-
-\colBegin{0.5}
-
-```python
-class SimpleMove(Command):
- def execute(self):
- remove_piece(self.start)
- add_piece(self.end)
-
- def undo(self):
- add_piece(self.start)
- remove_piece(self.end)
-```
-
-\colNext{0.5}
-
-```python
-class Board:
- def __init__(self):
- self.executed_commands = []
-
- def execute_move(self, move):
- command = self.create_command(move)
- command.execute()
- self.executed_commands.append(command)
-
- def undo_last_turn(self):
- if len(self.executed_commands) == 0:
- raise NoPreviousMovesException()
-
- last_command = get_executed_commands(-1)
- last_command.undo()
- self.executed_commands.pop()
-```
-
-\colEnd
-
-Board2 Auswertung
--------
-
-1. DRY...: -
-2. SOLID: -
-3. Pattern: Command
-4. Verbesserung: -
-
-DoorEventHandler
--------
-
-```java
-public class DoorEventHandler {
- private static final Logger LOGGER = Logger.getLogger(DoorEventHandler.class);
-
- private final DoorContext doorContext;
-
- private final Set observers = new HashSet<>();
-
- public DoorEventHandler(DoorContext doorContext) {
- this.doorContext = doorContext;
- }
-
- public void addObserver(DoorObserver observer) {
- }
-
- @Handler
- public void handle(DoorEvent event) {
- }
-}
-```
-
-DoorEventHandler Auswertung
--------
-
-1. DRY...: -
-2. SOLID: -
-3. Pattern: Observer
-4. Verbesserung:
- 1. Liste der Observer muss je nach Bedarf Threadsafe sein
- 2. Keine Möglichkeit Observer zu entfernen -> Memory Leak
-
-ButtonFactory
--------
-
-```python
-class ButtonFactory:
- def get_bwd_button(self, name, dim, text):
- return BwdButton(name, dim, text)
-
- def get_circle_button(self, name, color, text):
- return CircleButton(name, color, text)
-
- def get_fwd_button(self, name, dim, text):
- return FwdButton(name, dim, text)
-
- def get_info_button(self, name, info_text):
- return InfoButton(name, info_text)
-
-class ButtonFactoryC(ButtonFactory):
- pass
-```
-
-ButtonFactory Auswertung
--------
-
-1. DRY...: -
-2. SOLID: -
-3. Pattern: AbstractFactory
-4. Verbesserung: Inheritance hat in diesem Fall zu Problemen geführt.
-Da man nicht Voraussagen kann, wo welche Buttons gebraucht werden,
-bietet sich hier Composition + Interface besser an.
-
-ReceiptFactory
--------
-
-```python
-class ReceiptFactory:
- def create_eft_receipt(self, receipt_lines):
- pass
-
- def create_refund_receipt(self, kunden_session_nr, fehl_betrag, beleg_nr, mfk):
- pass
-
-class Customer1ReceiptFactory(ReceiptFactory):
- def __init__(self, printer_config):
- self.printer_config = printer_config
-
- def create_eft_receipt(self, receipt_lines):
- pass
-
- def create_refund_receipt(self, kunden_session_nr, fehl_betrag, beleg_nr, mfk):
- print("No refund receipt implemented")
- return None
-```
-
-ReceiptFactory Auswertung
--------
-
-1. DRY...: -
-2. SOLID: create_refund_receipt verletzt Liskov's Substitution Principle
-3. Pattern: AbstractFactory
-4. Verbesserung: -
-
-OperatingPoint
------
-
-```python
-class OperatingPointNumberResolveStrategy:
- """
- Returns the vending location (Verkaufspunkt) assigned to the operating point number (Betriebspunkt)
- """
- def get_net_point(self, operating_point_number):
- pass
-
-class DidokStrategy(OperatingPointNumberResolveStrategy):
- def get_net_point(self, operating_point_number):
- return self.assortment_provider.get_current_netpoint_by_didok_number(
- operating_point_number
- )
-
-class SubstopNetpointIdStrategy(OperatingPointNumberResolveStrategy):
- def get_net_point(self, operating_point_number):
- return self.assortment_provider.get_current_netpoint_by_netpoint_id(
- operating_point_number,
- "SUBSTOP"
- )
-```
-
-OperatingPoint Auswertung
--------
-
-1. DRY...: KISS: Hätte auch ein IF Statement gereicht?
-2. SOLID: -
-3. Pattern: Strategy
-4. Verbesserung: -