diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/json/ingame/RequestSerializer.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/json/ingame/RequestSerializer.java new file mode 100644 index 0000000..04dd8ff --- /dev/null +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/json/ingame/RequestSerializer.java @@ -0,0 +1,83 @@ +package uulm.teamname.marvelous.gamelibrary.json.ingame; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import uulm.teamname.marvelous.gamelibrary.entities.EntityID; +import uulm.teamname.marvelous.gamelibrary.entities.EntityType; +import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest; +import uulm.teamname.marvelous.gamelibrary.requests.GameRequest; +import uulm.teamname.marvelous.gamelibrary.requests.Request; +import uulm.teamname.marvelous.gamelibrary.requests.RequestType; + +import java.io.IOException; + +public class RequestSerializer extends StdSerializer { + + public RequestSerializer() { + this(null); + } + + protected RequestSerializer(Class t) { + super(t); + } + + @Override + public void serialize(Request value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + gen.writeStartObject(); + gen.writeObjectField("requestType", value.type); + + + if (value instanceof GameRequest) { + serializeGameRequest((GameRequest) value, gen, provider); + } else if (value instanceof CharacterRequest) { + serializeCharacterRequest((CharacterRequest) value, gen, provider); + } + + gen.writeEndObject(); + } + + /** Method invoked after writing the RequestType, and used to write any additional values required */ + private void serializeGameRequest(GameRequest value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + return; // does nothing, but still there for consistency + } + + /** Method invoked after writing the RequestType, and used to write any additional values required */ + private void serializeCharacterRequest(CharacterRequest value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + // The idea here is: if ([GUARD]) {gen.write...} for all fields. + + boolean hasTargetEntity = + value.type == RequestType.MeleeAttackRequest || + value.type == RequestType.RangedAttackRequest || + value.type == RequestType.ExchangeInfinityStoneRequest; + + boolean hasStoneType = + value.type == RequestType.ExchangeInfinityStoneRequest || + value.type == RequestType.UseInfinityStoneRequest; + + boolean hasValue = + value.type == RequestType.MeleeAttackRequest || + value.type == RequestType.RangedAttackRequest; + + + gen.writeObjectField("originEntity", value.originEntity); + + if (hasTargetEntity) { + gen.writeObjectField("targetEntity", value.targetEntity); + } + + gen.writeObjectField("originField", value.originField); + gen.writeObjectField("targetField", value.targetField); + + if (hasStoneType) { + gen.writeObjectField("stoneType", new EntityID(EntityType.InfinityStones, value.stoneType.getID())); + } + + if (hasValue) { + gen.writeObjectField("value", value.value); + } + } +} diff --git a/src/main/java/uulm/teamname/marvelous/gamelibrary/requests/Request.java b/src/main/java/uulm/teamname/marvelous/gamelibrary/requests/Request.java index 70c88ed..136e5ac 100644 --- a/src/main/java/uulm/teamname/marvelous/gamelibrary/requests/Request.java +++ b/src/main/java/uulm/teamname/marvelous/gamelibrary/requests/Request.java @@ -7,11 +7,13 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import uulm.teamname.marvelous.gamelibrary.json.basic.EventMessage; import uulm.teamname.marvelous.gamelibrary.json.ingame.RequestDeserializer; +import uulm.teamname.marvelous.gamelibrary.json.ingame.RequestSerializer; import java.util.Objects; /** Represents an abstract request sent inside a {@link EventMessage} between client and server. */ @JsonDeserialize(using = RequestDeserializer.class) +@JsonSerialize(using = RequestSerializer.class) public abstract class Request { @JsonProperty("requestType") public RequestType type; diff --git a/src/test/java/uulm/teamname/marvelous/gamelibrary/json/ingame/RequestSerializerTest.java b/src/test/java/uulm/teamname/marvelous/gamelibrary/json/ingame/RequestSerializerTest.java new file mode 100644 index 0000000..7f3fd5e --- /dev/null +++ b/src/test/java/uulm/teamname/marvelous/gamelibrary/json/ingame/RequestSerializerTest.java @@ -0,0 +1,223 @@ +package uulm.teamname.marvelous.gamelibrary.json.ingame; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.jqwik.api.*; +import net.jqwik.api.lifecycle.BeforeProperty; +import uulm.teamname.marvelous.gamelibrary.IntVector2; +import uulm.teamname.marvelous.gamelibrary.entities.EntityID; +import uulm.teamname.marvelous.gamelibrary.entities.EntityType; +import uulm.teamname.marvelous.gamelibrary.entities.StoneType; +import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest; +import uulm.teamname.marvelous.gamelibrary.requests.GameRequest; +import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder; +import uulm.teamname.marvelous.gamelibrary.requests.RequestType; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RequestSerializerTest { + + private ObjectMapper mapper; + @BeforeProperty + void setup() { + mapper = new ObjectMapper(); + } + + + + @Property + void gameRequestsGetSerializedProperly( + @ForAll @From("gameRequests") GameRequest request) + throws JsonProcessingException { + var jsonRepresentingRequest = String.format( + "{\"requestType\":\"%s\"}", + request.type); + + assertThat(mapper.writeValueAsString(request)) + .isEqualTo(jsonRepresentingRequest); + } + + @Property + void characterRequestsGetSerializedProperly( + @ForAll @From("characterRequests") CharacterRequest request) + throws JsonProcessingException { + + var jsonRepresentingValue = request.type == RequestType.MeleeAttackRequest || + request.type == RequestType.RangedAttackRequest ? + String.format("\"value\":%d", request.value) : ""; + + var jsonRepresentingTargetEntity = request.type == RequestType.MeleeAttackRequest || + request.type == RequestType.RangedAttackRequest || + request.type == RequestType.ExchangeInfinityStoneRequest ? + String.format( + "\"targetEntity\":{\"entityID\":\"%s\",\"ID\":%d},", + request.targetEntity.type, + request.targetEntity.id) : ""; + + var jsonRepresentingStoneType = request.type == RequestType.UseInfinityStoneRequest || + request.type == RequestType.ExchangeInfinityStoneRequest ? + String.format( + "\"stoneType\":{\"entityID\":\"%s\",\"ID\":%d}", + EntityType.InfinityStones, + request.stoneType.getID()) : ""; + + var jsonRepresentingRequest = String.format( + """ + { + "requestType":"%s", + "originEntity":{"entityID":"%s","ID":%d}, + %s + "originField":[%d,%d], + "targetField":[%d,%d]%s + %s%s + %s + } + """, + request.type, + request.originEntity.type, + request.originEntity.id, + jsonRepresentingTargetEntity, + request.originField.getX(), + request.originField.getY(), + request.targetField.getX(), + request.targetField.getY(), + jsonRepresentingStoneType.length() != 0 ? "," : "", + jsonRepresentingStoneType, + jsonRepresentingValue.length() != 0 ? "," : "", + jsonRepresentingValue + ).replace("\n", ""); + + assertThat(mapper.writeValueAsString(request)) + .isEqualTo(jsonRepresentingRequest); + } + + // Note that everything that follows could be extracted into another class, + // but that's complicated, so I won't do it + + static Set characterRequestTypes; + static Set gameRequestTypes; + + static { + characterRequestTypes = new HashSet<>(); + characterRequestTypes.add(RequestType.MeleeAttackRequest); + characterRequestTypes.add(RequestType.RangedAttackRequest); + characterRequestTypes.add(RequestType.MoveRequest); + characterRequestTypes.add(RequestType.ExchangeInfinityStoneRequest); + characterRequestTypes.add(RequestType.UseInfinityStoneRequest); + + gameRequestTypes = new HashSet<>(); + gameRequestTypes.add(RequestType.PauseStopRequest); + gameRequestTypes.add(RequestType.PauseStartRequest); + gameRequestTypes.add(RequestType.EndRoundRequest); + gameRequestTypes.add(RequestType.DisconnectRequest); + gameRequestTypes.add(RequestType.Req); + } + + @Provide("gameRequests") + public static Arbitrary gameRequests() { + return requestBuilders() + .map(RequestBuilder::buildGameRequest) + .filter(request -> gameRequestTypes.contains(request.type)); + } + + @Provide("characterRequests") + public static Arbitrary characterRequests() { + return requestBuilders() + .map(RequestBuilder::buildCharacterRequest) + .filter(request -> characterRequestTypes.contains(request.type)); + } + + + @Provide("filledRequestBuilders") + public static Arbitrary requestBuilders() { + var emptyBuilders = Arbitraries.of(RequestType.class) + .map(RequestBuilder::new); + + var buildersWithEntitiesAndValue = Combinators.combine( + emptyBuilders, + entityIDs().filter(x -> x.type == EntityType.P1 || x.type == EntityType.P2).tuple2(), + entityIDs().filter(x -> x.type == EntityType.Rocks), + Arbitraries.integers()) + .as((builder, characterIDs, rockID, value) -> builder + .withOriginEntity(characterIDs.get1()) + .withTargetEntity(value % 7 == 0 ? characterIDs.get2() : rockID) + .withValue(value) + ); + + var buildersWithStoneTypes = Combinators.combine( + buildersWithEntitiesAndValue, + Arbitraries.of(StoneType.class)) + .as(RequestBuilder::withStoneType); + + return Combinators.combine( + buildersWithStoneTypes, + randomPositions(), + directions(), + attackPositions()) + .as((builder, position, direction, attackPos) -> { + var type = builder.buildGameRequest().type; // hacky but whatever + Tuple.Tuple2 originAndTargetPosition; + if (type == RequestType.RangedAttackRequest) { + originAndTargetPosition = attackPos; + } else { + originAndTargetPosition = Tuple.of(position, position.copy().add(direction)); + } + return builder.withOriginField(originAndTargetPosition.get1()) + .withTargetField(originAndTargetPosition.get2()); + }); + } + + @Provide("entityIDs") + public static Arbitrary entityIDs() { + var entityTypes = Arbitraries.of(EntityType.class); + return Combinators.combine(entityTypes, Arbitraries.integers().greaterOrEqual(0)) + .as((type, id) -> { + int actualID; + if (type == EntityType.P1 || type == EntityType.P2 || type == EntityType.InfinityStones) { + actualID = id % 6; + } else if (type == EntityType.NPC) { + actualID = id % 3; + } else { + actualID = id; + } + return new EntityID(type, id); + }); + } + + @Provide("randomPositions") + public static Arbitrary randomPositions() { + return Arbitraries.integers() + .greaterOrEqual(0) + .tuple2() + .map((pos) -> new IntVector2(pos.get1(), pos.get2())); + } + + @Provide("directions") + public static Arbitrary directions() { + return Arbitraries.integers() + .between(-1, 1) + .tuple2() + .filter(pos -> pos.get1() != 0 && pos.get2() != 0) // eliminate zero vectors + .map(pos -> new IntVector2(pos.get1(), pos.get2())); + } + + @Provide("attackPositions") + /** Returns tuples of origin vectors (of an attack), and the vector pointing to the attack dir */ + public static Arbitrary> attackPositions() { + return Combinators.combine(randomPositions(), directions().tuple5()) + .as((origin, dir) -> { + var attackField = origin.copy() + .add(dir.get1().scale(3)) + .add(dir.get2().scale(3)) + .add(dir.get3().scale(3)) + .add(dir.get4().scale(3)); + if (attackField.length() == 0) { + attackField.add(dir.get5().scale(3)); + } + return Tuple.of(origin, attackField); + }); + } +}