From a6d7138af2a9d464b0efaa467cf7a79357a4562d Mon Sep 17 00:00:00 2001 From: Peet Date: Mon, 15 Jun 2026 21:31:37 +0200 Subject: [PATCH] done --- README.md | 3 +- src/Arena.java | 61 ++++++ src/ArenaTest.java | 445 +++++++++++++++++++++++++++++++++++++++++++ src/Attack.java | 20 ++ src/Character.java | 11 ++ src/Enemy.java | 61 ++++++ src/FireSpell.java | 20 ++ src/IceGolem.java | 34 ++++ src/Main.java | 166 ++++++++++++++++ src/Player.java | 54 ++++++ src/Skeleton.java | 41 ++++ src/SwordAttack.java | 20 ++ 12 files changed, 934 insertions(+), 2 deletions(-) create mode 100644 src/Arena.java create mode 100644 src/ArenaTest.java create mode 100644 src/Attack.java create mode 100644 src/Character.java create mode 100644 src/Enemy.java create mode 100644 src/FireSpell.java create mode 100644 src/IceGolem.java create mode 100644 src/Main.java create mode 100644 src/Player.java create mode 100644 src/Skeleton.java create mode 100644 src/SwordAttack.java diff --git a/README.md b/README.md index d317914..4b1d68d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# OOP -Example of relevant parts based on space. \ No newline at end of file +# OOP \ No newline at end of file diff --git a/src/Arena.java b/src/Arena.java new file mode 100644 index 0000000..4595635 --- /dev/null +++ b/src/Arena.java @@ -0,0 +1,61 @@ + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public final class Arena { + + private final Set 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 getCharacters() { + return Set.copyOf(characters); + } + + public List getLivingCharacters() { + return characters.stream() + .filter(character -> + !character.isDefeated() + ) + .toList(); + } + + public List getDefeatedCharacters() { + return characters.stream() + .filter(Character::isDefeated) + .toList(); + } +} \ No newline at end of file diff --git a/src/ArenaTest.java b/src/ArenaTest.java new file mode 100644 index 0000000..6962f0a --- /dev/null +++ b/src/ArenaTest.java @@ -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 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 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 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 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 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 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 living = + arena.getLivingCharacters(); + + List 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 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 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 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 arena = new Arena<>(); + + arena.add( + new Player( + "Held", + 100 + ) + ); + + Set characters = + arena.getCharacters(); + + // Act und Assert + assertThrows( + UnsupportedOperationException.class, + characters::clear + ); + } + + @Test + @DisplayName( + "null kann nicht hinzugefügt werden" + ) + void nullCharacterCannotBeAdded() { + Arena arena = new Arena<>(); + + assertThrows( + NullPointerException.class, + () -> arena.add(null) + ); + } + + @Test + @DisplayName( + "attackAll akzeptiert keinen null-Angriff" + ) + void nullAttackIsRejected() { + Arena arena = new Arena<>(); + + assertThrows( + NullPointerException.class, + () -> arena.attackAll(null) + ); + } + } +} \ No newline at end of file diff --git a/src/Attack.java b/src/Attack.java new file mode 100644 index 0000000..9cfc6df --- /dev/null +++ b/src/Attack.java @@ -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); +} \ No newline at end of file diff --git a/src/Character.java b/src/Character.java new file mode 100644 index 0000000..ac1c7d7 --- /dev/null +++ b/src/Character.java @@ -0,0 +1,11 @@ +public interface Character { + + String getName(); + + int getHitPoints(); + + boolean isDefeated(); + + void receive(FireSpell attack); + void receive(SwordAttack attack); +} \ No newline at end of file diff --git a/src/Enemy.java b/src/Enemy.java new file mode 100644 index 0000000..059d590 --- /dev/null +++ b/src/Enemy.java @@ -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() + ); + } +} \ No newline at end of file diff --git a/src/FireSpell.java b/src/FireSpell.java new file mode 100644 index 0000000..8f33170 --- /dev/null +++ b/src/FireSpell.java @@ -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); + } +} \ No newline at end of file diff --git a/src/IceGolem.java b/src/IceGolem.java new file mode 100644 index 0000000..b779ad6 --- /dev/null +++ b/src/IceGolem.java @@ -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); + } +} \ No newline at end of file diff --git a/src/Main.java b/src/Main.java new file mode 100644 index 0000000..27dc378 --- /dev/null +++ b/src/Main.java @@ -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 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 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 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, + * Arena und Arena. + */ + private static void printArena( + Arena 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 erlaubt sowohl + * Set als auch List. + */ + private static void printCharacters( + Iterable characters + ) { + for (Character character : characters) { + System.out.printf( + "%s: %d HP, besiegt: %s%n", + character.getName(), + character.getHitPoints(), + character.isDefeated() + ); + } + } +} \ No newline at end of file diff --git a/src/Player.java b/src/Player.java new file mode 100644 index 0000000..699e45e --- /dev/null +++ b/src/Player.java @@ -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() + ); + } +} \ No newline at end of file diff --git a/src/Skeleton.java b/src/Skeleton.java new file mode 100644 index 0000000..a9d9c4f --- /dev/null +++ b/src/Skeleton.java @@ -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); + } +} \ No newline at end of file diff --git a/src/SwordAttack.java b/src/SwordAttack.java new file mode 100644 index 0000000..529f723 --- /dev/null +++ b/src/SwordAttack.java @@ -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); + } +} \ No newline at end of file