refactor: move event checks from builder to their respective classes

This commit is contained in:
punchready 2021-05-03 19:25:36 +02:00
parent d36466a5a0
commit 80c77b7956
8 changed files with 232 additions and 248 deletions

View File

@ -47,6 +47,50 @@ public class CharacterEvent extends Event {
return this; return this;
} }
@Override
public boolean check() {
if(!super.check()) {
return false;
}
switch(type) {
// Melee- and ranged attacks need the same properties (except for the type)
case MeleeAttackEvent:
case RangedAttackEvent:
if (this.originField == null ||
this.targetField == null ||
this.originEntity == null ||
this.targetEntity == null) {
return false;
}
break;
// Exchanging and using InfinityStones both uses the same keys. Hereby, using a stone like the
// RealityStone causes the stone to be used on oneself
case ExchangeInfinityStoneEvent:
case UseInfinityStoneEvent:
if (this.originField == null ||
this.targetField == null ||
this.originEntity == null ||
this.targetEntity == null ||
this.stoneType == null) {
return false;
}
break;
// MoveEvents take an originField, a targetField, and an originEntity.
case MoveEvent:
if (this.originField == null ||
this.targetField == null ||
this.originEntity == null) {
return false;
}
break;
}
return true;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -8,6 +8,24 @@ public class CustomEvent extends Event {
public String teamIdentifier; public String teamIdentifier;
public HashMap<String, Object> customContent; public HashMap<String, Object> customContent;
@Override
public boolean check() {
if(!super.check()) {
return false;
}
switch(type) {
// CustomEvent only requires the custom data.
case CustomEvent:
if (this.customContent == null) {
return false;
}
break;
}
return true;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -28,6 +28,43 @@ public class EntityEvent extends Event {
return this; return this;
} }
@Override
public boolean check() {
if(!super.check()) {
return false;
}
switch(type) {
// DestroyedEntityEvent takes an ID and a field, and destroys the entity if the field is correct
case DestroyedEntityEvent:
if (this.targetField == null || this.targetEntity == null) {
return false;
}
break;
// TakenDamage, ConsumedAP / MP and Healed all need a targetField, targetEntity and Amount.
case TakenDamageEvent:
case ConsumedAPEvent:
case ConsumedMPEvent:
case HealedEvent:
if (this.targetField == null ||
this.targetEntity == null ||
this.amount == null) {
return false;
}
break;
// SpawnEntity needs an entity, of course.
case SpawnEntityEvent:
if (this.entity == null) {
return false;
}
break;
}
return true;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -13,6 +13,14 @@ public abstract class Event {
return this; return this;
} }
/**
* Checks whether the event contains all necessary properties according to its {@link EventType}.
* @return True if the event has all required properties set, otherwise false
*/
public boolean check() {
return type != null;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -6,7 +6,6 @@ import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
import uulm.teamname.marvelous.gamelibrary.entities.StoneType; import uulm.teamname.marvelous.gamelibrary.entities.StoneType;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.StringJoiner; import java.util.StringJoiner;
@ -166,153 +165,6 @@ public class EventBuilder {
return this; return this;
} }
/**
* A method to check whether the current event is actually built correctly. If that is not the case, it
* throws an {@link IllegalStateException}.
* This occurs if for example a property is null even though it shouldn't be.
* The check is based on the EventType. <b>Using this method is strongly recommended when working with
* entities, as it prevents unnoticed bugs before they might happen!</b>
*
* @throws IllegalStateException if the current event is non-valid
*/
public EventBuilder check() throws IllegalStateException {
if (this.type == null) throw new IllegalStateException("The eventType is null");
else {
switch (this.type) {
// all of those events do not need any extra values except the EventType
case Ack:
case Nack:
case Req:
case PauseStartEvent:
case PauseStopEvent:
case TurnTimeoutEvent:
case DisconnectEvent:
break;
// GameState needs very specific properties
case GameStateEvent:
if (this.entities == null ||
this.turnOrder == null ||
this.activeCharacter == null ||
this.winCondition == null) {
throwException();
}
break;
// CustomEvent needs only... well, CustomContent. Who would've thought!
case CustomEvent:
if (this.customContent == null) {
throwException();
}
break;
// DestroyedEntityEvent takes an ID and a field, and destroys the entity if the field is correct
case DestroyedEntityEvent:
if (this.targetField == null || this.targetEntity == null) {
throwException();
}
break;
// TakenDamage, ConsumedAP / MP and Healed all need a targetField, targetEntity and Amount.
case TakenDamageEvent:
case ConsumedAPEvent:
case ConsumedMPEvent:
case HealedEvent:
if (this.targetField == null ||
this.targetEntity == null ||
this.amount == null) {
throwException();
}
break;
// SpawnEntity needs an entity, of course.
case SpawnEntityEvent:
if (this.entity == null) {
throwException();
}
break;
// Melee- and ranged attacks need the same properties (except for the type)
case MeleeAttackEvent:
case RangedAttackEvent:
if (this.originField == null ||
this.targetField == null ||
this.originEntity == null ||
this.targetEntity == null) {
throwException();
}
break;
// Exchanging and using InfinityStones both uses the same keys. Hereby, using a stone like the
// RealityStone causes the stone to be used on oneself
case ExchangeInfinityStoneEvent:
case UseInfinityStoneEvent:
if (this.originField == null ||
this.targetField == null ||
this.originEntity == null ||
this.targetEntity == null ||
this.stoneType == null) {
throwException();
}
break;
// MoveEvents take an originField, a targetField, and an originEntity.
case MoveEvent:
if (this.originField == null ||
this.targetField == null ||
this.originEntity == null) {
throwException();
}
break;
// RoundSetupEvents take a RoundCount and a CharacterOrder
case RoundSetupEvent:
if (this.roundCount == null || this.characterOrder == null) {
throwException();
}
break;
// TurnEvents take a TurnCount and a NextCharacter
case TurnEvent:
if (this.turnCount == null || this.nextCharacter == null) {
throwException();
}
break;
// A WinEvent needs to know what player has won
case WinEvent:
if (this.playerWon == null) {
throwException();
}
break;
// TimeoutEvents give a message back. As the only events to do so, this might be removed later on.
case TimeoutEvent:
if (this.message == null) {
throwException();
}
break;
// TimeoutWarnings carry a message and the amount of time left in seconds. The message might disappear.
case TimeoutWarningEvent:
if (this.message == null || this.timeLeft == null) {
throwException();
}
break;
}
}
return this;
}
/**
* Utility function for throwing a specific exception.
* @throws IllegalStateException if the builder hasn't received enough properties to construct the event
*/
private void throwException() throws IllegalStateException {
throw new IllegalStateException("Properties malformed for " + this.type + ".\n" + "Builder properties: " + this.notNullToString());
}
/** /**
* Builds a {@link GameEvent} from the values given to the builder. * Builds a {@link GameEvent} from the values given to the builder.
* <ul> * <ul>
@ -326,21 +178,9 @@ public class EventBuilder {
* <li>timeLeft describes the time left for a client to act before getting kicked in the TimeoutWarning event</li> * <li>timeLeft describes the time left for a client to act before getting kicked in the TimeoutWarning event</li>
* </ul> * </ul>
* *
* @param checked Determines if properties should be checked
* @return a {@link GameEvent} based on the builder * @return a {@link GameEvent} based on the builder
*/ */
public GameEvent buildGameEvent(boolean checked) throws IllegalStateException { public GameEvent buildGameEvent() throws IllegalStateException {
if(checked) {
this.check();
}
return buildGameEvent();
}
/**
* Builds a {@link GameEvent} from the values given to the builder without checking.
* @return a {@link GameEvent} based on the builder
*/
private GameEvent buildGameEvent() {
var gameEvent = new GameEvent(); var gameEvent = new GameEvent();
gameEvent.type = this.type; gameEvent.type = this.type;
gameEvent.roundCount = this.roundCount; gameEvent.roundCount = this.roundCount;
@ -364,21 +204,9 @@ public class EventBuilder {
* <li>amount is a generic amount, for example damage taken</li> * <li>amount is a generic amount, for example damage taken</li>
* </ul> * </ul>
* *
* @param checked Determines if properties should be checked
* @return an {@link EntityEvent} based on the builder * @return an {@link EntityEvent} based on the builder
*/ */
public EntityEvent buildEntityEvent(boolean checked) throws IllegalStateException { public EntityEvent buildEntityEvent() throws IllegalStateException {
if(checked) {
this.check();
}
return this.buildEntityEvent();
}
/**
* Builds an {@link EntityEvent} from the values given to the builder without checking.
* @return an {@link EntityEvent} based on the builder
*/
private EntityEvent buildEntityEvent() {
var entityEvent = new EntityEvent(); var entityEvent = new EntityEvent();
entityEvent.type = this.type; entityEvent.type = this.type;
entityEvent.targetEntity = this.targetEntity; entityEvent.targetEntity = this.targetEntity;
@ -398,21 +226,9 @@ public class EventBuilder {
* <li>targetField is the target field in the form of an {@link IntVector2}</li> * <li>targetField is the target field in the form of an {@link IntVector2}</li>
* </ul> * </ul>
* *
* @param checked Determines if properties should be checked
* @return a {@link CharacterEvent} based on the builder * @return a {@link CharacterEvent} based on the builder
*/ */
public CharacterEvent buildCharacterEvent(boolean checked) throws IllegalStateException { public CharacterEvent buildCharacterEvent() throws IllegalStateException {
if(checked) {
this.check();
}
return buildCharacterEvent();
}
/**
* Builds a {@link CharacterEvent} from the values given to the builder without checking.
* @return a {@link CharacterEvent} based on the builder
*/
private CharacterEvent buildCharacterEvent() {
var characterEvent = new CharacterEvent(); var characterEvent = new CharacterEvent();
characterEvent.type = this.type; characterEvent.type = this.type;
characterEvent.originEntity = this.originEntity; characterEvent.originEntity = this.originEntity;
@ -434,21 +250,9 @@ public class EventBuilder {
* <li>describes whether the win condition is in effect</li> * <li>describes whether the win condition is in effect</li>
* </ul> * </ul>
* *
* @param checked Determines if properties should be checked
* @return a {@link GameStateEvent} based on the builder * @return a {@link GameStateEvent} based on the builder
*/ */
public GameStateEvent buildGameStateEvent(boolean checked) throws IllegalStateException { public GameStateEvent buildGameStateEvent() throws IllegalStateException {
if(checked) {
this.check();
}
return this.buildGameStateEvent();
}
/**
* Builds a {@link GameStateEvent} from the values given to the builder without checking.
* @return a {@link GameStateEvent} based on the builder
*/
private GameStateEvent buildGameStateEvent() {
var gameStateEvent = new GameStateEvent(); var gameStateEvent = new GameStateEvent();
gameStateEvent.type = this.type; gameStateEvent.type = this.type;
gameStateEvent.entities = this.entities; gameStateEvent.entities = this.entities;
@ -465,21 +269,9 @@ public class EventBuilder {
* <li>customContent is a {@link HashMap}<{@link String}, {@link Object}> resembling the JSON keys in the event</li> * <li>customContent is a {@link HashMap}<{@link String}, {@link Object}> resembling the JSON keys in the event</li>
* </ul> * </ul>
* *
* @param checked Determines if properties should be checked
* @return a {@link CustomEvent} based on the builder * @return a {@link CustomEvent} based on the builder
*/ */
public CustomEvent buildCustomEvent(boolean checked) throws IllegalStateException { public CustomEvent buildCustomEvent() throws IllegalStateException {
if(checked) {
this.check();
}
return this.buildCustomEvent();
}
/**
* Builds a {@link CustomEvent} from the values given to the builder without checking.
* @return a {@link CustomEvent} based on the builder
*/
private CustomEvent buildCustomEvent() {
var customEvent = new CustomEvent(); var customEvent = new CustomEvent();
customEvent.type = this.type; customEvent.type = this.type;
customEvent.teamIdentifier = this.teamIdentifier; customEvent.teamIdentifier = this.teamIdentifier;

View File

@ -17,6 +17,59 @@ public class GameEvent extends Event {
public String message; public String message;
public Integer timeLeft; public Integer timeLeft;
@Override
public boolean check() {
if(!super.check()) {
return false;
}
switch(type) {
// all of those events do not need any extra values except the EventType
case PauseStartEvent:
case PauseStopEvent:
case TurnTimeoutEvent:
case DisconnectEvent:
break;
// RoundSetupEvents take a RoundCount and a CharacterOrder
case RoundSetupEvent:
if (this.roundCount == null || this.characterOrder == null) {
return false;
}
break;
// TurnEvents take a TurnCount and a NextCharacter
case TurnEvent:
if (this.turnCount == null || this.nextCharacter == null) {
return false;
}
break;
// A WinEvent needs to know what player has won
case WinEvent:
if (this.playerWon == null) {
return false;
}
break;
// TimeoutEvents give a message back. As the only events to do so, this might be removed later on.
case TimeoutEvent:
if (this.message == null) {
return false;
}
break;
// TimeoutWarnings carry a message and the amount of time left in seconds. The message might disappear.
case TimeoutWarningEvent:
if (this.message == null || this.timeLeft == null) {
return false;
}
break;
}
return true;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -13,6 +13,27 @@ public class GameStateEvent extends Event {
public EntityID activeCharacter; public EntityID activeCharacter;
public Boolean winCondition; public Boolean winCondition;
@Override
public boolean check() {
if(!super.check()) {
return false;
}
switch(type) {
// GameState needs very specific properties
case GameStateEvent:
if (this.entities == null ||
this.turnOrder == null ||
this.activeCharacter == null ||
this.winCondition == null) {
return false;
}
break;
}
return true;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -37,7 +37,7 @@ class EventBuilderTest {
new EntityID(EntityType.P2, 0), new EntityID(EntityType.P2, 0),
}; };
turn = new EntityID(EntityType.P1, 0); turn = turns[0];
entities = new Entity[]{ entities = new Entity[]{
new Character( new Character(
@ -79,7 +79,6 @@ class EventBuilderTest {
25); 25);
filled = new EventBuilder(EventType.CustomEvent) filled = new EventBuilder(EventType.CustomEvent)
// .withType(EventType.CustomEvent)
.withTargetEntity(new EntityID(EntityType.P1, 1)) .withTargetEntity(new EntityID(EntityType.P1, 1))
.withTargetField(new IntVector2(11, 13)) .withTargetField(new IntVector2(11, 13))
.withAmount(15) .withAmount(15)
@ -126,7 +125,11 @@ class EventBuilderTest {
var roundSetupEvent = new EventBuilder(EventType.RoundSetupEvent) var roundSetupEvent = new EventBuilder(EventType.RoundSetupEvent)
.withRoundCount(4) .withRoundCount(4)
.withCharacterOrder(turns) .withCharacterOrder(turns)
.buildGameEvent(true); .buildGameEvent();
assertThat(roundSetupEvent.check())
.isTrue()
.withFailMessage("RoundSetupEvent failed check");
var roundSetupEventBaseline = new GameEvent(); var roundSetupEventBaseline = new GameEvent();
roundSetupEventBaseline.type = EventType.RoundSetupEvent; roundSetupEventBaseline.type = EventType.RoundSetupEvent;
@ -139,13 +142,17 @@ class EventBuilderTest {
var turnEvent = new EventBuilder(EventType.TurnEvent) var turnEvent = new EventBuilder(EventType.TurnEvent)
.withNextCharacter(turn) .withNextCharacter(turn)
.withRoundCount(5) .withTurnCount(5)
.buildGameEvent(false); .buildGameEvent();
assertThat(turnEvent.check())
.isTrue()
.withFailMessage("TurnEvent failed check");
var turnEventBaseline = new GameEvent(); var turnEventBaseline = new GameEvent();
turnEventBaseline.type = EventType.TurnEvent; turnEventBaseline.type = EventType.TurnEvent;
turnEventBaseline.nextCharacter = turn; turnEventBaseline.nextCharacter = turn;
turnEventBaseline.roundCount = 5; turnEventBaseline.turnCount = 5;
assertThat(turnEvent) assertThat(turnEvent)
.isEqualTo(turnEventBaseline) .isEqualTo(turnEventBaseline)
@ -163,7 +170,11 @@ class EventBuilderTest {
.withPlayerWon(5912) .withPlayerWon(5912)
.withMessage("This message is very much not useful at all") .withMessage("This message is very much not useful at all")
.withTimeLeft(-144) .withTimeLeft(-144)
.buildGameEvent(false); .buildGameEvent();
assertThat(gameEvent.check())
.isTrue()
.withFailMessage("GameEvent failed check");
var baseline = new GameEvent(); var baseline = new GameEvent();
baseline.type = EventType.DisconnectEvent; baseline.type = EventType.DisconnectEvent;
@ -188,7 +199,11 @@ class EventBuilderTest {
.withTurnOrder(turns) .withTurnOrder(turns)
.withActiveCharacter(turn) .withActiveCharacter(turn)
.withWinCondition(true) .withWinCondition(true)
.buildGameStateEvent(false); .buildGameStateEvent();
assertThat(gameStateEvent.check())
.isTrue()
.withFailMessage("GameStateEvent failed check");
var baseline = new GameStateEvent(); var baseline = new GameStateEvent();
baseline.type = EventType.ConsumedAPEvent; baseline.type = EventType.ConsumedAPEvent;
@ -216,46 +231,42 @@ class EventBuilderTest {
@Test @Test
void buildGameStateEventWithTooManyProperties() { void buildGameStateEventWithTooManyProperties() {
assertThatNoException() assertThat(new EventBuilder(EventType.Ack) // too many properties is fine
.isThrownBy(() -> new EventBuilder(EventType.Ack) // too many properties is fine .withAmount(15) // also properties of different EventTypes, they just get ignored
.withAmount(15) // also properties of different EventTypes, they just get ignored .withEntities(entities) // properties belonging to the same eventType get incorporated into
.withEntities(entities) // properties belonging to the same eventType get incorporated into .withWinCondition(false) // the final event, so they have to be ignored
.withWinCondition(false) // the final event, so they have to be ignored .buildGameStateEvent() // by the programmer later on
.buildGameStateEvent(true)); // by the programmer later on .check()).isTrue();
} }
@Test @Test
void buildGameStateEvent() { void buildGameStateEvent() {
assertThatNoException() assertThat(new EventBuilder(EventType.Ack) // needs no properties
.isThrownBy(() -> new EventBuilder(EventType.Ack) // needs no properties .buildGameStateEvent()
.buildGameStateEvent(true)); .check()).isTrue();
assertThat(new EventBuilder(EventType.Nack).buildGameStateEvent().check()).isTrue();
assertThatNoException() assertThat(new EventBuilder(EventType.Req).buildGameStateEvent().check()).isTrue();
.isThrownBy(() -> new EventBuilder(EventType.Nack).buildGameStateEvent(true));
assertThatNoException() assertThat(new EventBuilder(EventType.GameStateEvent) // if properties missing throw exception
.isThrownBy(() -> new EventBuilder(EventType.Req).buildGameStateEvent(true)); .withTurnOrder(turns)
.withActiveCharacter(turn)
.buildGameStateEvent()
.check()).isFalse();
assertThatExceptionOfType(IllegalStateException.class) assertThat(new EventBuilder(EventType.GameStateEvent) // no exception if all properties present
.isThrownBy(() -> new EventBuilder(EventType.GameStateEvent) // if properties missing throw exception .withEntities(entities)
.withTurnOrder(turns) .withTurnOrder(turns)
.withActiveCharacter(turn) .withActiveCharacter(turn)
.buildGameStateEvent(true)); .withWinCondition(false)
.buildGameStateEvent()
assertThatNoException() .check()).isTrue();
.isThrownBy(() -> new EventBuilder(EventType.GameStateEvent) // no exception if all properties present
.withEntities(entities)
.withTurnOrder(turns)
.withActiveCharacter(turn)
.withWinCondition(false)
.buildGameStateEvent(true));
} }
@Test @Test
void buildCustomEvent() { void buildCustomEvent() {
// TODO: check CustomEvent validation for correctness // TODO: check CustomEvent validation for correctness
} }
} }