feat: implemented proper Request serialization

This commit is contained in:
Yannik Bretschneider 2021-05-30 18:07:28 +02:00
parent 6a6669ae5d
commit e6dc10e3f5
3 changed files with 308 additions and 0 deletions

View File

@ -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<Request> {
public RequestSerializer() {
this(null);
}
protected RequestSerializer(Class<Request> 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);
}
}
}

View File

@ -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;

View File

@ -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<RequestType> characterRequestTypes;
static Set<RequestType> 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<GameRequest> gameRequests() {
return requestBuilders()
.map(RequestBuilder::buildGameRequest)
.filter(request -> gameRequestTypes.contains(request.type));
}
@Provide("characterRequests")
public static Arbitrary<CharacterRequest> characterRequests() {
return requestBuilders()
.map(RequestBuilder::buildCharacterRequest)
.filter(request -> characterRequestTypes.contains(request.type));
}
@Provide("filledRequestBuilders")
public static Arbitrary<RequestBuilder> 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<IntVector2, IntVector2> 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<EntityID> 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<IntVector2> randomPositions() {
return Arbitraries.integers()
.greaterOrEqual(0)
.tuple2()
.map((pos) -> new IntVector2(pos.get1(), pos.get2()));
}
@Provide("directions")
public static Arbitrary<IntVector2> 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<Tuple.Tuple2<IntVector2, IntVector2>> 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.<IntVector2, IntVector2>of(origin, attackField);
});
}
}