This commit is contained in:
2026-06-15 21:31:37 +02:00
parent 7c4cc39173
commit a6d7138af2
12 changed files with 934 additions and 2 deletions

View File

@@ -1,2 +1 @@
# OOP # OOP
Example of relevant parts based on space.

61
src/Arena.java Normal file
View File

@@ -0,0 +1,61 @@
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class Arena<C extends Character> {
private final Set<C> characters =
new HashSet<>();
public void add(C character) {
characters.add(
Objects.requireNonNull(
character,
"Character must not be null"
)
);
}
public boolean remove(C character) {
return characters.remove(character);
}
public void attackAll(Attack attack) {
Objects.requireNonNull(
attack,
"Attack must not be null"
);
for (C character : characters) {
attack.execute(character);
}
}
public int size() {
return characters.size();
}
public boolean isEmpty() {
return characters.isEmpty();
}
public Set<C> getCharacters() {
return Set.copyOf(characters);
}
public List<C> getLivingCharacters() {
return characters.stream()
.filter(character ->
!character.isDefeated()
)
.toList();
}
public List<C> getDefeatedCharacters() {
return characters.stream()
.filter(Character::isDefeated)
.toList();
}
}

445
src/ArenaTest.java Normal file
View File

@@ -0,0 +1,445 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("Generische Arena")
class ArenaTest {
@Nested
@DisplayName("Spieler-Arena")
class PlayerArenaTests {
@Test
@DisplayName(
"Ein Angriff wird auf alle Spieler angewendet"
)
void attackAllAttacksEveryPlayer() {
// Arrange
Arena<Player> arena = new Arena<>();
Player warrior = new Player(
"Krieger",
100
);
Player mage = new Player(
"Magier",
70
);
arena.add(warrior);
arena.add(mage);
// Act
arena.attackAll(new SwordAttack());
// Assert
assertAll(
() -> assertEquals(
70,
warrior.getHitPoints()
),
() -> assertEquals(
40,
mage.getHitPoints()
),
() -> assertFalse(
warrior.isDefeated()
),
() -> assertFalse(
mage.isDefeated()
)
);
}
@Test
@DisplayName(
"Dasselbe Objekt wird im Set nicht doppelt gespeichert"
)
void samePlayerIsNotAddedTwice() {
// Arrange
Arena<Player> arena = new Arena<>();
Player player = new Player(
"Held",
100
);
// Act
arena.add(player);
arena.add(player);
// Assert
assertEquals(
1,
arena.size()
);
}
}
@Nested
@DisplayName("Gegner-Arena")
class EnemyArenaTests {
@Test
@DisplayName(
"Double Dispatch wirkt für jeden Gegnertyp unterschiedlich"
)
void fireSpellAffectsEnemyTypesDifferently() {
// Arrange
Arena<Enemy> arena = new Arena<>();
Skeleton skeleton = new Skeleton(
"Knochensoldat",
100
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(skeleton);
arena.add(iceGolem);
// Act
arena.attackAll(new FireSpell());
// Assert
assertAll(
() -> assertEquals(
75,
skeleton.getHitPoints()
),
() -> assertEquals(
40,
iceGolem.getHitPoints()
)
);
}
@Test
@DisplayName(
"Ein Schwert wirkt ebenfalls abhängig vom Gegnertyp"
)
void swordAttackAffectsEnemyTypesDifferently() {
// Arrange
Arena<Enemy> arena = new Arena<>();
Skeleton skeleton = new Skeleton(
"Knochensoldat",
100
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(skeleton);
arena.add(iceGolem);
// Act
arena.attackAll(new SwordAttack());
// Assert
assertAll(
() -> assertEquals(
60,
skeleton.getHitPoints()
),
() -> assertEquals(
85,
iceGolem.getHitPoints()
)
);
}
}
@Nested
@DisplayName("Gemischte Arena")
class MixedArenaTests {
@Test
@DisplayName(
"Spieler und Gegner können gemeinsam angegriffen werden"
)
void mixedArenaAttacksAllCharacterTypes() {
// Arrange
Arena<Character> arena = new Arena<>();
Player player = new Player(
"Held",
100
);
Skeleton skeleton = new Skeleton(
"Knochensoldat",
100
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(player);
arena.add(skeleton);
arena.add(iceGolem);
// Act
arena.attackAll(new FireSpell());
// Assert
assertAll(
() -> assertEquals(
70,
player.getHitPoints()
),
() -> assertEquals(
75,
skeleton.getHitPoints()
),
() -> assertEquals(
40,
iceGolem.getHitPoints()
)
);
}
@Test
@DisplayName(
"Lebende und besiegte Figuren werden getrennt"
)
void livingAndDefeatedCharactersAreSeparated() {
// Arrange
Arena<Character> arena = new Arena<>();
Player player = new Player(
"Held",
20
);
Skeleton skeleton = new Skeleton(
"Schwaches Skelett",
20
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(player);
arena.add(skeleton);
arena.add(iceGolem);
// Act
arena.attackAll(new FireSpell());
List<Character> living =
arena.getLivingCharacters();
List<Character> defeated =
arena.getDefeatedCharacters();
// Assert
assertAll(
() -> assertEquals(
1,
living.size()
),
() -> assertTrue(
living.contains(iceGolem)
),
() -> assertEquals(
2,
defeated.size()
),
() -> assertTrue(
defeated.contains(player)
),
() -> assertTrue(
defeated.contains(skeleton)
)
);
}
@Test
@DisplayName(
"Besiegte Figuren können entfernt werden"
)
void defeatedCharactersCanBeRemoved() {
// Arrange
Arena<Character> arena = new Arena<>();
Player player = new Player(
"Held",
20
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(player);
arena.add(iceGolem);
arena.attackAll(new FireSpell());
// Act
for (Character defeated
: arena.getDefeatedCharacters()) {
arena.remove(defeated);
}
// Assert
assertAll(
() -> assertEquals(
1,
arena.size()
),
() -> assertTrue(
arena.getCharacters()
.contains(iceGolem)
),
() -> assertFalse(
arena.getCharacters()
.contains(player)
)
);
}
}
@Nested
@DisplayName("Verwaltung der Arena")
class ArenaManagementTests {
@Test
@DisplayName(
"remove entfernt eine vorhandene Figur"
)
void removeDeletesCharacter() {
// Arrange
Arena<Player> arena = new Arena<>();
Player player = new Player(
"Held",
100
);
arena.add(player);
// Act
boolean removed =
arena.remove(player);
// Assert
assertAll(
() -> assertTrue(removed),
() -> assertTrue(
arena.isEmpty()
),
() -> assertEquals(
0,
arena.size()
)
);
}
@Test
@DisplayName(
"remove liefert false für eine unbekannte Figur"
)
void removingUnknownCharacterReturnsFalse() {
// Arrange
Arena<Player> arena = new Arena<>();
Player storedPlayer = new Player(
"Held",
100
);
Player unknownPlayer = new Player(
"Fremder",
100
);
arena.add(storedPlayer);
// Act
boolean removed =
arena.remove(unknownPlayer);
// Assert
assertAll(
() -> assertFalse(removed),
() -> assertEquals(
1,
arena.size()
)
);
}
@Test
@DisplayName(
"getCharacters liefert ein unveränderliches Set"
)
void returnedCharacterSetIsUnmodifiable() {
// Arrange
Arena<Player> arena = new Arena<>();
arena.add(
new Player(
"Held",
100
)
);
Set<Player> characters =
arena.getCharacters();
// Act und Assert
assertThrows(
UnsupportedOperationException.class,
characters::clear
);
}
@Test
@DisplayName(
"null kann nicht hinzugefügt werden"
)
void nullCharacterCannotBeAdded() {
Arena<Character> arena = new Arena<>();
assertThrows(
NullPointerException.class,
() -> arena.add(null)
);
}
@Test
@DisplayName(
"attackAll akzeptiert keinen null-Angriff"
)
void nullAttackIsRejected() {
Arena<Character> arena = new Arena<>();
assertThrows(
NullPointerException.class,
() -> arena.attackAll(null)
);
}
}
}

20
src/Attack.java Normal file
View File

@@ -0,0 +1,20 @@
public abstract class Attack {
private final int baseDamage;
protected Attack(int baseDamage) {
if (baseDamage <= 0) {
throw new IllegalArgumentException(
"Base damage must be positive"
);
}
this.baseDamage = baseDamage;
}
public final int getBaseDamage() {
return baseDamage;
}
public abstract void execute(Character target);
}

11
src/Character.java Normal file
View File

@@ -0,0 +1,11 @@
public interface Character {
String getName();
int getHitPoints();
boolean isDefeated();
void receive(FireSpell attack);
void receive(SwordAttack attack);
}

61
src/Enemy.java Normal file
View File

@@ -0,0 +1,61 @@
public abstract class Enemy implements Character {
private final String name;
private int hitPoints;
protected Enemy(String name, int hitPoints) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(
"Name must not be blank"
);
}
if (hitPoints <= 0) {
throw new IllegalArgumentException(
"Hit points must be positive"
);
}
this.name = name;
this.hitPoints = hitPoints;
}
@Override
public final String getName() {
return name;
}
@Override
public final int getHitPoints() {
return hitPoints;
}
@Override
public final boolean isDefeated() {
return hitPoints == 0;
}
protected final void takeDamage(int damage) {
if (damage < 0) {
throw new IllegalArgumentException(
"Damage must not be negative"
);
}
hitPoints = Math.max(
0,
hitPoints - damage
);
}
@Override
public final String toString() {
return "%s{name='%s', hitPoints=%d, defeated=%s}"
.formatted(
getClass().getSimpleName(),
name,
hitPoints,
isDefeated()
);
}
}

20
src/FireSpell.java Normal file
View File

@@ -0,0 +1,20 @@
import java.util.Objects;
public final class FireSpell extends Attack {
private static final int BASE_DAMAGE = 30;
public FireSpell() {
super(BASE_DAMAGE);
}
@Override
public void execute(Character target) {
Objects.requireNonNull(
target,
"Target must not be null"
);
target.receive(this);
}
}

34
src/IceGolem.java Normal file
View File

@@ -0,0 +1,34 @@
import java.util.Objects;
public final class IceGolem extends Enemy {
public IceGolem(String name, int hitPoints) {
super(name, hitPoints);
}
@Override
public void receive(SwordAttack attack) {
Objects.requireNonNull(
attack,
"Attack must not be null"
);
int damage =
attack.getBaseDamage() / 2;
takeDamage(damage);
}
@Override
public void receive(FireSpell attack) {
Objects.requireNonNull(
attack,
"Attack must not be null"
);
int damage =
attack.getBaseDamage() * 2;
takeDamage(damage);
}
}

166
src/Main.java Normal file
View File

@@ -0,0 +1,166 @@
public final class Main {
private Main() {
}
public static void main(String[] args) {
demonstratePlayerArena();
demonstrateEnemyArena();
demonstrateMixedArena();
}
/*
* Homogene Arena:
* Es dürfen ausschließlich Player-Objekte
* hinzugefügt werden.
*/
private static void demonstratePlayerArena() {
Arena<Player> arena = new Arena<>();
Player warrior = new Player(
"Krieger",
100
);
Player mage = new Player(
"Magier",
70
);
arena.add(warrior);
arena.add(mage);
System.out.println("Spieler-Arena vor dem Angriff:");
printArena(arena);
arena.attackAll(new SwordAttack());
System.out.println("Spieler-Arena nach dem Schwertangriff:");
printArena(arena);
}
/*
* Arena mit unterschiedlichen Gegnerklassen.
*
* Skeleton und IceGolem sind beide Enemy,
* reagieren aber unterschiedlich auf FireSpell.
*/
private static void demonstrateEnemyArena() {
Arena<Enemy> arena = new Arena<>();
Skeleton skeleton = new Skeleton(
"Knochensoldat",
100
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(skeleton);
arena.add(iceGolem);
System.out.println("Gegner-Arena vor dem Angriff:");
printArena(arena);
arena.attackAll(new FireSpell());
System.out.println("Gegner-Arena nach dem Feuerzauber:");
printArena(arena);
}
/*
* Gemischte Arena:
* Spieler und Gegner sind gemeinsam möglich,
* weil alle Character implementieren.
*/
private static void demonstrateMixedArena() {
Arena<Character> arena = new Arena<>();
Player player = new Player(
"Held",
50
);
Skeleton skeleton = new Skeleton(
"Schwaches Skelett",
20
);
IceGolem iceGolem = new IceGolem(
"Frostwächter",
100
);
arena.add(player);
arena.add(skeleton);
arena.add(iceGolem);
System.out.println("Gemischte Arena vor dem Angriff:");
printArena(arena);
arena.attackAll(new FireSpell());
System.out.println("Gemischte Arena nach dem Feuerzauber:");
printArena(arena);
System.out.println("Lebende Figuren:");
printCharacters(arena.getLivingCharacters());
System.out.println("Besiegte Figuren:");
printCharacters(arena.getDefeatedCharacters());
/*
* Besiegte Figuren entfernen.
*
* getDefeatedCharacters() liefert eine neue Liste,
* daher wird das Set nicht während seiner Iteration
* verändert.
*/
for (Character character
: arena.getDefeatedCharacters()) {
arena.remove(character);
}
System.out.println("Arena nach dem Entfernen besiegter Figuren:");
printArena(arena);
}
/*
* Generische Hilfsmethode:
* Funktioniert für Arena<Player>,
* Arena<Enemy> und Arena<Character>.
*/
private static <C extends Character> void printArena(
Arena<C> arena
) {
System.out.printf(
"Größe: %d, lebend: %d, besiegt: %d%n",
arena.size(),
arena.getLivingCharacters().size(),
arena.getDefeatedCharacters().size()
);
printCharacters(arena.getCharacters());
System.out.println();
}
/*
* Iterable<? extends Character> erlaubt sowohl
* Set<C> als auch List<C>.
*/
private static void printCharacters(
Iterable<? extends Character> characters
) {
for (Character character : characters) {
System.out.printf(
"%s: %d HP, besiegt: %s%n",
character.getName(),
character.getHitPoints(),
character.isDefeated()
);
}
}
}

54
src/Player.java Normal file
View File

@@ -0,0 +1,54 @@
public class Player implements Character {
private final String name;
private int hitPoints;
public Player(String name, int hitPoints) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(
"Name must not be null or blank"
);
}
if (hitPoints <= 0) {
throw new IllegalArgumentException(
"Hit points must be positive"
);
}
this.name = name;
this.hitPoints = hitPoints;
}
@Override
public String getName() {
return name;
}
@Override
public int getHitPoints() {
return hitPoints;
}
@Override
public boolean isDefeated() {
return hitPoints <= 0;
}
@Override
public void receive(FireSpell attack) {
hitPoints = Math.max(
0,
hitPoints - attack.getBaseDamage()
);
}
@Override
public void receive(SwordAttack attack) {
hitPoints = Math.max(
0,
hitPoints - attack.getBaseDamage()
);
}
}

41
src/Skeleton.java Normal file
View File

@@ -0,0 +1,41 @@
import java.util.Objects;
public final class Skeleton extends Enemy {
private static final int SWORD_BONUS_DAMAGE = 10;
private static final int FIRE_RESISTANCE = 5;
public Skeleton(String name, int hitPoints) {
super(name, hitPoints);
}
@Override
public void receive(SwordAttack attack) {
Objects.requireNonNull(
attack,
"Attack must not be null"
);
int damage =
attack.getBaseDamage()
+ SWORD_BONUS_DAMAGE;
takeDamage(damage);
}
@Override
public void receive(FireSpell attack) {
Objects.requireNonNull(
attack,
"Attack must not be null"
);
int damage = Math.max(
0,
attack.getBaseDamage()
- FIRE_RESISTANCE
);
takeDamage(damage);
}
}

20
src/SwordAttack.java Normal file
View File

@@ -0,0 +1,20 @@
import java.util.Objects;
public final class SwordAttack extends Attack {
private static final int BASE_DAMAGE = 30;
public SwordAttack() {
super(BASE_DAMAGE);
}
@Override
public void execute(Character target) {
Objects.requireNonNull(
target,
"Target must not be null"
);
target.receive(this);
}
}