Compare commits

...

2 Commits

Author SHA1 Message Date
9e5e9eb91a ue6 2025-06-06 14:30:41 +02:00
4b73c1b5e3 correction ue7 2025-06-06 14:21:53 +02:00
11 changed files with 758 additions and 77 deletions

View File

@@ -0,0 +1,123 @@
package sudoku;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptySet;
/**
* A field of a Sudoku board.
*/
public class Field {
private static final int DEP_LENGTH = 2 * Sudoku.SIZE + 2;
public final Sudoku sudoku;
public final int x;
public final int y;
private Value value;
private List<Field> dependents;
private final Set<Value> domain = EnumSet.allOf(Value.class);
/**
* Creates a field of the specified Sudoku board at the specified coordinates.
*
* @param sudoku the Sudoku board
* @param x x coordinate (0 <= x < 9)
* @param y y coordinate (0 <= y < 9)
*/
public Field(Sudoku sudoku, int x, int y) {
if (x < 0 || x >= Sudoku.SIZE || y < 0 || y >= Sudoku.SIZE)
throw new IllegalArgumentException("Illegal coordinates " + x + ", " + y);
this.sudoku = sudoku;
this.x = x;
this.y = y;
}
/**
* Returns the value of this field, or null if it is empty.
*
* @return the value of this field, or null if it is empty.
*/
public Value getValue() {
return value;
}
/**
* Sets the value of this field
*
* @param v the value, or null if this field shall be empty.
*/
public void setValue(Value v) {
value = v;
}
/**
* Removes the value of this field so that it is empty.
*/
public void clearValue() {
setValue(null);
}
/**
* Checks whether this field is empty.
*
* @return true iff this field is empty.
*/
public boolean isEmpty() {
return value == null;
}
/**
* Returns a string representation of this field. As a matter of fact,
* its value is returned, or "." if it is empty.
*
* @return a string representation of this field.
*/
public String toString() {
return isEmpty() ? "." : value.toString();
}
/**
* Returns the set of all values possible for this field that do not violate the Sudoku constraints,
* if the field is empty. Otherwise, it returns the empty set. The returned set may be modified if
* the field is empty.
*
* @return the domain of this field.
*/
public Set<Value> getDomain() {
return isEmpty() ? domain : emptySet();
}
/**
* Returns the list of all other fields of this Sudoku field whose values are constrained by the
* value of this field.
*
* @return the list of all dependent fields.
*/
List<Field> getDependents() {
if (dependents == null) {
dependents = new ArrayList<>(DEP_LENGTH);
// fill the list with all fields in the given row and column
for (int i = 0; i < Sudoku.SIZE; i++) {
if (i != x)
dependents.add(sudoku.field(i, y));
if (i != y)
dependents.add(sudoku.field(x, i));
}
// determine the coordinates of the upper left corner of this field's block
final int x0 = 3 * (x / 3);
final int y0 = 3 * (y / 3);
// fill the list with the whole block
for (int x1 = x0; x1 < x0 + 3; x1++)
if (x1 != x)
for (int y1 = y0; y1 < y0 + 3; y1++)
if (y1 != y)
dependents.add(sudoku.field(x1, y1));
}
return dependents;
}
}

View File

@@ -0,0 +1,206 @@
package sudoku;
import java.util.ArrayList;
import java.util.List;
/**
* A Sudoku board as a matrix of {@linkplain sudoku.Field} instances.
* <p>
* Call {@linkplain #solve()} to solve it.
*/
public class Sudoku {
/**
* Edge size of the Sudoku board.
*/
public static final int SIZE = 9;
private static final String LS = System.lineSeparator();
private final Field[] board = new Field[SIZE * SIZE];
/**
* Creates a Sudoku board with all of its fields being empty.
*/
public Sudoku() {
for (int x = 0; x < SIZE; x++)
for (int y = 0; y < SIZE; y++)
board[SIZE * y + x] = new Field(this, x, y);
}
/**
* Returns the field at the specified coordinates
*
* @param x x coordinate (0 <= x < 9)
* @param y y coordinate (0 <= x < 9)
* @return the field at the specified coordinates
*/
public Field field(int x, int y) {
return board[SIZE * y + x];
}
/**
* Bulk operation to set values of the fields.
*
* @param values a sequence of integers where 0 means empty
*/
public void initialize(int... values) {
if (values.length != board.length)
throw new IllegalArgumentException("Wrong Sudoku board with " + values.length + " values");
for (int i = 0; i < values.length; i++)
if (values[i] != 0)
setValue(board[i], Value.of(values[i]));
}
/**
* Sets the specified value of the specified empty field
*
* @param field the empty field whose value is set
* @param value the value of the field
* @throws IllegalStateException if the field is not empty
* @throws IllegalArgumentException if setting this value would violate the Sudoku constraints
* @throws NullPointerException if the value is null
*/
public void setValue(Field field, Value value) {
if (value == null)
throw new NullPointerException("value is null");
if (!field.isEmpty())
throw new IllegalStateException("Value already set: " + field);
if (!field.getDomain().contains(value))
throw new IllegalArgumentException(value + " is not permitted");
for (Field dependent : field.getDependents())
dependent.getDomain().remove(value);
field.setValue(value);
}
/**
* Returns a string representation of this board.
*
* @return a string representation of this board
*/
public String toString() {
StringBuilder sb = new StringBuilder();
for (int y = 0; y < SIZE; y++) {
if (y == 3 || y == 6) sb.append("---------+---------+---------").append(LS);
for (int x = 0; x < SIZE; x++) {
if (x == 3 || x == 6) sb.append("|");
sb.append(" ").append(field(x, y)).append(" ");
}
sb.append(LS);
}
return sb.toString();
}
/**
* Looks for an empty field with minimal domain.
*
* @return a field with minimal domain, or null no field is empty
*/
private Field findBestCandidate() {
Field best = null;
int min = Integer.MAX_VALUE;
for (Field f : board)
if (f.isEmpty() && f.getDomain().size() < min) {
best = f;
min = best.getDomain().size();
}
return best;
}
/**
* Tries to solve the current board. The board is in the solved state if a solution is found,
* and unmodified if no solution is found.
*
* @return true iff a solution has been found.
*/
public boolean solve() {
final Field field = findBestCandidate();
if (field == null)
// A solution has been found
return true;
// There are empty fields. Try all values of its domain
final List<Field> revoke = new ArrayList<>();
for (Value v : field.getDomain()) {
field.setValue(v);
boolean consistent = true;
for (Field dependent : field.getDependents())
if (dependent.getDomain().contains(v)) {
if (dependent.getDomain().size() == 1) {
// This is a dead end because field 'dependent'
// would have an empty domain
consistent = false;
break;
}
revoke.add(dependent);
dependent.getDomain().remove(v);
}
if (consistent && solve())
return true;
// Choosing v as a value lead to a dead end.
// Revoke all modifications
field.clearValue();
for (Field f : revoke)
f.getDomain().add(v);
revoke.clear();
}
return false;
}
public List<Sudoku> solveAll() {
List<Sudoku> solutions = new ArrayList<>();
solveAll(solutions);
return solutions;
}
private void solveAll(List<Sudoku> solutions) {
final Field field = findBestCandidate();
if (field == null) {
// A solution has been found
Sudoku copy = new Sudoku();
copy.initialize(this.export());
solutions.add(copy);
return;
}
// There are empty fields. Try all values of its domain
final List<Field> revoke = new ArrayList<>();
for (Value v : field.getDomain()) {
field.setValue(v);
boolean consistent = true;
for (Field dependent : field.getDependents())
if (dependent.getDomain().contains(v)) {
if (dependent.getDomain().size() == 1) {
// This is a dead end because field 'dependent'
// would have an empty domain
consistent = false;
break;
}
revoke.add(dependent);
dependent.getDomain().remove(v);
}
if (consistent) solveAll(solutions);
// Revoke all modifications
field.clearValue();
for (Field f : revoke)
f.getDomain().add(v);
revoke.clear();
}
}
private int[] export() {
int[] values = new int[SIZE * SIZE];
int idx = 0;
while (idx < values.length) {
Field f = board[idx];
if (f != null) {
Value value = f.getValue();
if (value != null) values[idx] = value.getId();
else values[idx] = 0;
}
else throw new IllegalStateException("at least one field is not set");
++idx;
}
return values;
}
}

View File

@@ -0,0 +1,68 @@
package sudoku;
import java.util.List;
public class SudokuApp {
public static void main(String[] args) {
System.out.println("Sudoku Aufgabenblatt:");
System.out.println();
final Sudoku boardBlatt = new Sudoku();
boardBlatt.initialize(4, 5, 0, 0, 0, 0, 2, 0, 0,
6, 0, 0, 0, 2, 4, 8, 0, 0,
8, 0, 0, 0, 6, 1, 3, 0, 0,
0, 9, 0, 4, 0, 0, 0, 5, 0,
0, 1, 0, 2, 0, 8, 0, 7, 0,
0, 3, 0, 0, 0, 9, 0, 8, 0,
0, 0, 7, 1, 4, 0, 0, 0, 8,
0, 0, 2, 7, 9, 0, 0, 0, 6,
0, 0, 5, 0, 0, 0, 0, 2, 1);
printSolution(boardBlatt);
System.out.println("Sudoku Handzettel:");
System.out.println();
final Sudoku boardHandzettel = new Sudoku();
boardHandzettel.initialize(7, 0, 0, 0, 2, 5, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 9, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0,
4, 0, 9, 1, 0, 0, 0, 0, 0,
0, 0, 0, 6, 0, 0, 2, 0, 0,
0, 0, 0, 0, 0, 0, 0, 3, 0,
8, 2, 0, 0, 0, 0, 5, 0, 0,
0, 0, 0, 0, 3, 0, 6, 0, 0,
0, 0, 0, 9, 0, 0, 0, 0, 0);
printSolution(boardHandzettel);
System.out.println("Sudoku mit 2 Lösungen:");
System.out.println();
final Sudoku boardWithTwoSolutions = new Sudoku();
boardWithTwoSolutions.initialize(2, 9, 5, 7, 4, 3, 8, 6, 1,
4, 3, 1, 8, 6, 5, 9, 0, 0,
8, 7, 6, 1, 9, 2, 5, 4, 3,
3, 8, 7, 4, 5, 9, 2, 1, 6,
6, 1, 2, 3, 8, 7, 4, 9, 5,
5, 4, 9, 2, 1, 6, 7, 3, 8,
7, 6, 3, 5, 2, 4, 1, 8, 9,
9, 2, 8, 6, 7, 1, 3, 5, 4,
1, 5, 4, 9, 3, 8, 6, 0, 0);
System.out.println(boardWithTwoSolutions);
System.out.println("Solutions:");
System.out.println();
List<Sudoku> solutions = boardWithTwoSolutions.solveAll();
System.out.println(String.join(System.lineSeparator(), solutions.stream().map(Sudoku::toString).toList()));
}
private static void printSolution(Sudoku sudoku) {
System.out.println(sudoku);
System.out.println();
if (sudoku.solve()) {
System.out.println("Solution:");
System.out.println();
System.out.println(sudoku);
}
else
System.out.println("No solution!");
}
}

View File

@@ -0,0 +1,41 @@
package sudoku;
/**
* The enumeration type of all possible values of a Sudoku field.
*/
public enum Value {
ONE(1), TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8), NINE(9);
private final int id;
Value(int id) {
this.id = id;
}
/**
* Returns a string representation of this value. As a matter of fact, its id is returned.
*
* @return a string representation of this value.
*/
@Override
public String toString() {
return String.valueOf(id);
}
/**
* Returns the value with the specified id.
*
* @param id an id (1 <= id <= 9)
* @return the value with the specified id.
*/
public static Value of(int id) {
return values()[id - 1];
}
/**
* Returns the id of this instance.
*
* @return the id of this instance.
*/
public int getId() {return id;}
}

View File

@@ -0,0 +1,65 @@
package uebung06.test.sudoku;
import org.junit.Before;
import org.junit.Test;
import sudoku.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class FieldTest {
private static final int X = 8;
private static final int Y = 5;
private static final int NUL = -1;
private static final Value VAL = Value.EIGHT;
private static Sudoku sudoku;
private static Field field;
@Before
public void init() {
sudoku = new Sudoku();
field = new Field(sudoku, X, Y);
}
@Test(expected = IllegalArgumentException.class)
public void illegalXLowerBoundFieldTest() {
field = new Field(sudoku, NUL, Y);
}
@Test(expected = IllegalArgumentException.class)
public void illegalXUpperBoundFieldTest() {
field = new Field(sudoku, Sudoku.SIZE, Y);
}
@Test(expected = IllegalArgumentException.class)
public void illegalYLowerBoundFieldTest() {
field = new Field(sudoku, X, NUL);
}
@Test(expected = IllegalArgumentException.class)
public void illegalYUpperBoundFieldTest() {
field = new Field(sudoku, X, Sudoku.SIZE);
}
@Test
public void valueFieldTest() {
field.setValue(VAL);
assertEquals(field.getValue(), VAL);
}
@Test
public void clearValueFieldTest() {
field.setValue(VAL);
field.clearValue();
assertNull(field.getValue());
}
@Test
public void isValueEmptyFieldTest() {
field.setValue(VAL);
field.clearValue();
assertTrue(field.isEmpty());
}
}

View File

@@ -0,0 +1,129 @@
package uebung06.test.sudoku;
import org.junit.Before;
import org.junit.Test;
import sudoku.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.stream.IntStream;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SudokuAppTest {
private static final int MAX = 81;
private static final int MIN = 0;
private static final int SUDOKU_SIZE = 9;
private static Sudoku board;
private static int[] init;
private static int[] wrongSol;
@Before
public void init() {
board = new Sudoku();
init = IntStream.of(4, 5, 0, 0, 0, 0, 2, 0, 0,
6, 0, 0, 0, 2, 4, 8, 0, 0,
8, 0, 0, 0, 6, 1, 3, 0, 0,
0, 9, 0, 4, 0, 0, 0, 5, 0,
0, 1, 0, 2, 0, 8, 0, 7, 0,
0, 3, 0, 0, 0, 9, 0, 8, 0,
0, 0, 7, 1, 4, 0, 0, 0, 8,
0, 0, 2, 7, 9, 0, 0, 0, 6,
0, 0, 5, 0, 0, 0, 0, 2, 1).toArray();
wrongSol = IntStream.of(4, 5, 1, 8, 7, 3, 2, 6, 9,
6, 7, 3, 9, 2, 4, 8, 1, 5,
8, 2, 9, 5, 6, 1, 3, 4, 7,
2, 9, 8, 4, 1, 7, 6, 5, 3,
5, 1, 6, 2, 3, 8, 9, 7, 4,
7, 3, 4, 6, 5, 9, 1, 8, 2,
3, 6, 7, 1, 4, 2, 5, 9, 8,
1, 8, 2, 7, 9, 5, 4, 3, 6,
9, 5, 4, 3, 8, 6, 7, 2, 1).toArray();
}
//Testing first If-condition in Sudoku.init
@Test(expected = IllegalArgumentException.class)
public void illegalLowerBoundInitSudokuAppTest() {
board.initialize(IntStream.range(MIN, MIN+1).toArray());
}
//Testing first If-condition in Sudoku.init
@Test(expected = IllegalArgumentException.class)
public void illegalUpperBoundInitSudokuAppTest() {
board.initialize(IntStream.range(MIN, MAX+1).toArray());
}
//Testing ArrayIndex of Value
@Test(expected = ArrayIndexOutOfBoundsException.class)
public void illegalArrayIndexOutOfBoundInitSudokuAppTest() {
board.initialize(IntStream.range(MIN, MAX).toArray());
}
//Testing Sudoku.solve()
@Test
public void correctSolutionSudokuAppTest() {
board.initialize(init);
board.solve();
assertTrue(verifySudokuSol());
}
//Just testing verifySudokuSol-Method
@Test
public void wrongSolutionSudokuAppTest() {
try {
board.initialize(wrongSol);
} catch (final IllegalArgumentException e) {
//do nothing, just handling throwing exceptions from Sudoku-class
}
assertFalse(verifySudokuSol());
}
public Boolean verifySudokuSol() {
HashSet<Value> rows = new HashSet<>();
HashSet<Value> cols = new HashSet<>();
HashMap<Integer, HashSet<Value>> blocks = new HashMap<>();
IntStream.range(0, SUDOKU_SIZE)
.forEach(block -> blocks.put(block, new HashSet<>())); //init Hashsets for every block
for (int y = 0; y < SUDOKU_SIZE; y++) {
for (int x = 0; x < SUDOKU_SIZE; x++) {
//check column
Value val = board.field(x, y).getValue();
if (!cols.add(val))
return false;
if (cols.size() == SUDOKU_SIZE)
cols.clear();
//check row
val = board.field(y, x).getValue();
if (!rows.add(val))
return false;
if (rows.size() == SUDOKU_SIZE)
rows.clear();
//check 3x3 block
val = board.field(x, y).getValue();
if (!blocks.get(getBlockIndex(x, y)).add(val))
return false;
}
}
return true;
}
public int getBlockIndex(int x, int y) {
return getColIndex(x) + getRowIndex(y);
}
public int getColIndex(int x) {
return x / 3;
}
public int getRowIndex(int y) {
return (y/3)*3;
}
}

View File

@@ -5,30 +5,30 @@ import oop.ch05.secured.SecuredContent;
import oop.ch05.secured.SecurityClient;
public class AccountCard extends MensaCard {
private final SecuredContent<String> account;
private final SecuredContent<String> account;
public AccountCard(String key, String account, int password) {
super(key, Color.white);
this.account = new SecuredContent<>(password, account);
}
public AccountCard(String key, String account, int password) {
super(key, Color.white);
this.account = new SecuredContent<>(password, account);
}
public String getAccount() {
return account.getContent();
}
public String getAccount() {
return account.getContent();
}
public void setAccount(SecurityClient client, String account) throws AuthorizationException {
if (account == null || account.trim().length() == 0)
throw new IllegalArgumentException("Invalid account " + account);
this.account.setContent(client, account);
}
public void setAccount(SecurityClient client, String account) throws AuthorizationException {
if (account == null || account.trim().isEmpty())
throw new IllegalArgumentException("Invalid account " + account);
this.account.setContent(client, account);
}
@Override
public String toString() {
return super.toString() + " for account " + account.getContent();
}
@Override
public String toString() {
return super.toString() + " for account " + account.getContent();
}
@Override
public void pass(CashPoint cashPoint) {
cashPoint.charge(this);
}
@Override
public void pass(CashPoint cashPoint) throws RejectedException, AuthorizationException {
cashPoint.charge(this);
}
}

View File

@@ -0,0 +1,51 @@
package oop.ch05.mensa;
import oop.ch05.secured.AuthorizationException;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
public class AccountManagement {
private final int password;
public final String name;
private final Random random = new Random();
private final Map<String, Integer> accounts = new TreeMap<>();
public AccountManagement(int password, String name) {
this.password = password;
this.name = name;
}
public void deposit(String account, int euros) {
final int amount = accounts.getOrDefault(account, 0);
accounts.put(account, amount + 100 * euros);
}
public void pay(AccountCard card, int price, CashPoint cashPoint) throws RejectedException, AuthorizationException {
final int amount = accounts.getOrDefault(card.getAccount(), 0);
final int challenge = nextChallenge();
if (cashPoint.challengeResponse(challenge) != requiredResponse(challenge))
throw new AuthorizationException(cashPoint + " is not authorized to access accounts on " + name);
if (amount < price)
throw new RejectedException(card + " bounced");
accounts.put(card.getAccount(), amount - price);
}
public int getAmount(String account) {
return accounts.getOrDefault(account, 0);
}
private int nextChallenge() {
return random.nextInt();
}
private int requiredResponse(int challenge) {
return challenge ^ password;
}
@Override
public String toString() {
return "Account Management " + name + " " + accounts;
}
}

View File

@@ -4,67 +4,62 @@ import oop.ch05.secured.AuthorizationException;
import oop.ch05.secured.SecurityClient;
public class CashPoint implements SecurityClient {
private final int password;
public final String name;
private int counter;
private int cents;
private final int password;
public final String name;
private final AccountManagement accountMgmt;
private int counter;
private int cents;
public CashPoint(String name, int password) {
this.name = name;
this.password = password;
this.counter = 0;
this.cents = 0;
}
public CashPoint(String name, int password, AccountManagement accountMgmt) {
this.name = name;
this.password = password;
this.accountMgmt = accountMgmt;
}
@Override
public int challengeResponse(int challenge) {
return challenge ^ password;
}
public int challengeResponse(int challenge) {
return challenge ^ password;
}
@Override
public String toString() {
return "Cash point " + name + " (" + getCounter() + " meals, " + getCents() + " cents charged)";
}
@Override
public String toString() {
return "Cash point " + name + " (" + getCounter() + " meals, " + getCents() + " cents charged)";
}
public int getCounter() {
return counter;
}
public int getCounter() {
return counter;
}
public int getCents() {
return cents;
}
public int getCents() {
return cents;
}
private int getPrice(Color color) {
return switch (color) {
case green -> 267;
case blue -> 357;
case white -> 495;
default -> 0;
};
}
private int getPrice(Color color) {
return switch (color) {
case green -> 267;
case blue -> 357;
case white -> 495;
default -> 0;
};
}
void count(MensaCard card) {
counter++;
}
void count(MensaCard card) {
counter++;
}
void charge(CashCard cashCard) throws AuthorizationException, RejectedException {
final int price = getPrice(cashCard.color);
if (cashCard.getBalance() < price)
throw new RejectedException("insufficient payment");
cashCard.charge(this, price);
count(cashCard);
cents += price;
}
void charge(CashCard cashCard) throws AuthorizationException, RejectedException {
final int price = getPrice(cashCard.color);
if (cashCard.getBalance() < price)
throw new RejectedException("insufficient payment");
cashCard.charge(this, price);
count(cashCard);
cents += price;
}
void charge(AccountCard accountCard) {
final int price = getPrice(accountCard.color);
// zahlen methode
/*
zahlen bein acmgmt
*/
System.out.println("Charging " + price + " cents on account " + accountCard.getAccount());
cents += price;
}
void charge(AccountCard accountCard) throws RejectedException, AuthorizationException {
final int price = getPrice(accountCard.color);
System.out.println("Charging " + price + " cents on account " + accountCard.getAccount());
accountMgmt.pay(accountCard, price, this);
count(accountCard);
cents += price;
}
}

View File

@@ -4,10 +4,11 @@ import oop.ch05.secured.AuthorizationException;
public class MensaExample {
public static void main(String[] args) {
AccountManagement mgmt = new AccountManagement(4711, "Uni");
VendingMachine vm1 = new VendingMachine("left", 4711);
VendingMachine vm2 = new VendingMachine("right", 4711);
VendingMachine tumVM = new VendingMachine("TUM Mensa", 3141);
CashPoint unibwMensa = new CashPoint("UniBw Mensa", 4711);
CashPoint unibwMensa = new CashPoint("UniBw Mensa", 4711, mgmt);
AccountCard conf = new AccountCard("conference", "33-1298", 42);
MensaCard frankSmith = new CountCard("Frank Smith", Color.gray);

View File

@@ -1,6 +1,8 @@
package oop.ch05.generic.mensa;
import oop.ch05.generic.secured.AuthorizationException;
import oop.ch05.secured.AuthorizationException;
import oop.ch05.mensa.*;
import org.junit.Before;
import org.junit.Test;