/*
This file is part of Fantom.
Fantom is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Fantom is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Fantom. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.matfyz.aai.fantom.game;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;
import cz.matfyz.aai.fantom.message.ClientType;
import cz.matfyz.aai.fantom.message.MessageMoveBase;
import cz.matfyz.aai.fantom.message.MessageUpdate;
import cz.matfyz.aai.fantom.message.MessageMoveBase.ActorMove;
import cz.matfyz.aai.fantom.message.MessageUpdate.ActorTickets;
import cz.matfyz.aai.fantom.server.Client;
import cz.matfyz.aai.fantom.server.ProtocolException;
import cz.matfyz.aai.fantom.utils.Parser;
import cz.matfyz.aai.fantom.utils.ParserException;
/**
* The game graph. Contains specification of the nodes, edges and transport
* types.
*/
public class Graph {
/**
* The name of the property that is used, when the line contains a node.
*/
public static final String PROPERTY_NODE = "node";
/**
* The name of the property that is used, when the line contains a node.
*/
public static final String PROPERTY_NODES = "nodes";
/**
* The name of the property that is used, when the line contains an edge.
*/
public static final String PROPERTY_EDGE = "edge";
/**
* The name of the property that is used, when the line contains
* a transport type.
*/
public static final String PROPERTY_TRANSPORT = "transport";
/**
* The name of the property that is used, when the line contains
* specification of the game.
*/
public static final String PROPERTY_GAME = "game";
/**
* The name of the property that is used, when the line contains
* information about the phantom.
*/
public static final String PROPERTY_PHANTOM = "phantom";
/**
* The name of the property that is used to store the current position
* of an actor.
*/
public static final String PROPERTY_POSITION = "position";
/**
* The name of the property that is used, when the line contains
* information about a detective.
*/
public static final String PROPERTY_DETECTIVE = "detective";
/**
* The name of a property that is used to specify an amount of double tickets of an actor.
*/
public static final String PROPERTY_DOUBLE_TICKET = "double";
/**
* The name of a property that is used to specify an amount of universal tickets of an actor.
*/
public static final String PROPERTY_UNIVERSAL_TICKET = "universal";
/**
* The name of the property that contains numbers of the turns, in
* which the phantom is revealed.
*/
public static final String PROPERTY_REVEALS = "reveal";
/**
* Multiple nodes in the graph.
*/
public class Nodes {
/** The name of the property that contains the lowest node id in the range. */
public static final String PROPERTY_FROM = "from";
/** The name of the property that contains the highest node id in the range. */
public static final String PROPERTY_TO = "to";
}
/**
* A single node in the graph.
*/
public class Node {
/**
* The name of the property that contains the X position of the node.
*/
public static final String PROPERTY_POSITION_X = "x";
/**
* The name of the property that contains the Y position of the node.
*/
public static final String PROPERTY_POSITION_Y = "y";
/**
* The ID of this node.
*/
private String id;
/**
* Other (custom) properties of the node.
*/
private Properties properties;
/**
* The list of edges going to this node.
*/
private List<Edge> incomingEdges;
private List<Edge> incomingEdgesReadOnly;
/**
* The list of edges going from this node.
*/
private List<Edge> outgoingEdges;
private List<Edge> outgoingEdgesReadOnly;
@Override
public int hashCode() {
return this.id.hashCode();
}
/**
* Returns the ID of the node.
* @return the ID of the node.
*/
public String getId() {
return this.id;
}
/**
* Returns the list of edges coming to the node.
* @return the list of edges coming to the node.
*/
public List<Edge> getIncomingEdges() {
return this.incomingEdgesReadOnly;
}
/**
* Returns the list of edges going from the node.
* @return the list of edges going from the node.
*/
public List<Edge> getOutgoingEdges() {
return this.outgoingEdgesReadOnly;
}
/**
* Returns the other properties of the node.
* @return the other properties of the node.
*/
public Properties getProperties() {
return this.properties;
}
/**
* Checks if this node is occupied by an actor.
* @return <code>true</code> if this node is occupied by
* an actor; otherwise, <code>false</code>.
*/
public boolean isOccupied() {
for(Actor a : Graph.this.actors) {
if(a.getCurrentPosition() == this)
return true;
}
return false;
}
/**
* Checks if this node is occupied by an actor of the given type.
* @param type the type of actor, for which the check is performed.
* @return <code>true</code> if this node is occupied by an actor
* of type <code>type</code>; otherwise, <code>false</code>.
*/
public boolean isOccupied(ClientType type) {
for(Actor a : Graph.this.actors) {
if(a.getCurrentPosition() == this
&& a.getClientType() == type) {
return true;
}
}
return false;
}
/**
* Initializes the node according to the specification in <code>properties</code>.
* @param properties the specification of the node.
* @throws GraphFormatException if there is a problem when parsing the specification.
*/
protected void loadFromProperties(Properties properties) throws GraphFormatException {
this.id = properties.getProperty(PROPERTY_NODE);
if(this.id == null || this.id.isEmpty())
throw new GraphFormatException("The ID of the graph was not specified");
this.properties = properties;
}
/**
* Serializes the node to the format, from which it was loaded.
* @return the properties collection with the specification of the node.
*/
public Properties serialize() {
Properties res = new Properties();
res.setProperty(PROPERTY_NODE, getId());
return res;
}
/**
* Converts the node to string representation in the same format, that can be
* decoded using {@link Parser#parseLine(String)} and {@link Node#Node(Properties)}.
* @return the string representation of the node.
*/
@Override
public String toString() {
Properties props = (Properties)this.properties.clone();
props.setProperty(PROPERTY_NODE, this.id);
return Parser.encodeProperties(props);
}
/**
* Initializes a new node.
*/
private Node() {
this.incomingEdges = new ArrayList<Edge>();
this.incomingEdgesReadOnly = Collections.unmodifiableList(this.incomingEdges);
this.outgoingEdges = new ArrayList<Edge>();
this.outgoingEdgesReadOnly = Collections.unmodifiableList(this.outgoingEdges);
}
/**
* Initializes a new node according to the specification.
* @param properties the specification of the node.
* @throws GraphFormatException if there is a problem when parsing the specification.
*/
protected Node(Properties properties) throws GraphFormatException {
this();
loadFromProperties(properties);
}
/**
* Initializes a new node using the given id.
* @param id
*/
private Node(String id) {
this();
this.id = id;
}
}
/**
* An edge between two nodes.
*/
public class Edge {
/**
* The source node of the edge.
*/
private Node source;
/**
* The target node of the edge.
*/
private Node target;
/**
* The transport type of this edge.
*/
private TransportType transportType;
/**
* Returns the target node of this edge.
* @return the target node of this edge.
*/
public Node getTarget() {
return target;
}
/**
* Returns the source node of this edge.
* @return the source node of this edge.
*/
public Node getSource() {
return source;
}
/**
* Returns the transport type of this edge.
* @return the transport type of this edge.
*/
public TransportType getTransportType() {
return transportType;
}
/**
* Loads the information about the edge from <code>properties</code>.
* @param properties the specification of the edge.
* @throws GraphFormatException if there is a problem when processing the properties.
*/
protected void loadProperties(Properties properties) throws GraphFormatException {
String description = properties.getProperty(PROPERTY_EDGE);
if(description == null || description.isEmpty())
throw new GraphFormatException("The description of the node was not specified.");
String[] descr = description.split("@");
if(descr == null || descr.length != 2)
throw new GraphFormatException("Invalid description of an edge: " + description);
String[] route = descr[0].split("-");
if(route == null || route.length != 2 || route[0] == null || route[0].isEmpty() || route[1] == null || route[1].isEmpty())
throw new GraphFormatException("Invalid description of a route: " + descr[0]);
String src_id = route[0].trim();
String dst_id = route[1].trim();
if(src_id == null || src_id.isEmpty())
throw new GraphFormatException("The ID of the source node was not specified.");
this.source = Graph.this.nodes.get(src_id);
if(this.source == null)
throw new GraphFormatException(String.format("The node '%s' was not found.", src_id));
if(dst_id == null || dst_id.isEmpty())
throw new GraphFormatException("The ID of the destination node was not specified.");
this.target = Graph.this.nodes.get(dst_id);
if(this.target == null)
throw new GraphFormatException(String.format("The node '%s' was not found", dst_id));
String type_str = descr[1];
if(type_str == null || type_str.isEmpty())
throw new GraphFormatException("The transport type was not specified.");
if(type_str.contains("+"))
throw new GraphFormatException(String.format("Multi-transport description of an edge passed to a single-transport edge constructor: %s", description));
this.transportType = Graph.this.transportTypes.get(type_str);
if(this.transportType == null)
throw new GraphFormatException(String.format("The transport type '%s' was not found.", type_str));
}
/**
* Serializes the edge to the same format, in from which it was loaded.
* @return the specification of the edge.
*/
public Properties serialize() {
Properties res = new Properties();
String edgeSpec = String.format("%s-%s@%s", source.getId(), target.getId(), transportType.getName());
res.setProperty(PROPERTY_EDGE, edgeSpec);
return res;
}
@Override
public String toString() {
Properties props = new Properties();
props.setProperty(PROPERTY_EDGE, String.format("%s-%s@%s", this.source.getId(), this.target.getId(), this.transportType.getName()));
return Parser.encodeProperties(props);
}
/**
* Creates a new edge.
* @param source the source node of the edge.
* @param target the target node of the edge.
* @param transportType the transport type of the edge.
*/
protected Edge(Node source, Node target, TransportType transportType) {
this.source = source;
this.target = target;
this.transportType = transportType;
}
/**
* Creates a new edge according to the specification in <code>properties</code>.
* @param properties the specification of the edge.
* @throws GraphFormatException if there is a problem when parsing the specification.
*/
protected Edge(Properties properties) throws GraphFormatException {
loadProperties(properties);
}
}
/**
* An edge between two nodes, which allows multiple trasport types in the same time.
* This is an internal helper structure.
*/
private class MultiEdge {
/**
* The source node of the edge.
*/
private Node source;
/**
* The target node of the edge.
*/
private Node target;
/**
* The transport type of this edge.
*/
private Collection<TransportType> transportTypes;
private boolean isOriented;
/**
* Returns the target node of this edge.
* @return the target node of this edge.
*/
public Node getTarget() {
return target;
}
/**
* Returns the source node of this edge.
* @return the source node of this edge.
*/
public Node getSource() {
return source;
}
/**
* Returns the list of edges represented by this multi-edge.
* @return the list of edges.
*/
public Edge[] getEdges() {
int count = (isOriented ? 1 : 2) * transportTypes.size();
Edge[] res = new Edge[count];
int pos = 0;
for(TransportType ttype : transportTypes) {
res[pos++] = new Edge(getSource(), getTarget(), ttype);
if(!isOriented)
res[pos++] = new Edge(getTarget(), getSource(), ttype);
}
return res;
}
/**
* Returns the transport type of this edge.
* @return the transport type of this edge.
*/
public Collection<TransportType> getTransportTypes() {
return transportTypes;
}
/**
* Loads the information about the edge from <code>properties</code>.
* @param properties the specification of the edge.
* @throws GraphFormatException if there is a problem when processing the properties.
*/
protected void loadProperties(Properties properties) throws GraphFormatException {
String description = properties.getProperty(PROPERTY_EDGE);
if(description == null || description.isEmpty())
throw new GraphFormatException("The description of the node was not specified.");
String[] descr = description.split("@");
if(descr == null || descr.length != 2)
throw new GraphFormatException("Invalid description of an edge: " + description);
String[] route;
isOriented = descr[0].indexOf('-') >= 0;
if(isOriented)
route = descr[0].split("-");
else
route = descr[0].split("=");
if(route == null || route.length != 2 || route[0] == null || route[0].isEmpty() || route[1] == null || route[1].isEmpty())
throw new GraphFormatException("Invalid description of a route: " + descr[0]);
String src_id = route[0].trim();
String dst_id = route[1].trim();
if(src_id == null || src_id.isEmpty())
throw new GraphFormatException("The ID of the source node was not specified.");
this.source = Graph.this.nodes.get(src_id);
if(this.source == null)
throw new GraphFormatException(String.format("The node '%s' was not found.", src_id));
if(dst_id == null || dst_id.isEmpty())
throw new GraphFormatException("The ID of the destination node was not specified.");
this.target = Graph.this.nodes.get(dst_id);
if(this.target == null)
throw new GraphFormatException(String.format("The node '%s' was not found", dst_id));
String[] types = descr[1].split("\\+");
if(types == null || types.length == 0)
throw new GraphFormatException("Missing a transport type: " + description);
this.transportTypes = new ArrayList<TransportType>(types.length);
for(String type_str : types) {
if(type_str == null || type_str.isEmpty())
throw new GraphFormatException("The transport type was not specified.");
TransportType transportType = Graph.this.transportTypes.get(type_str);
if(transportType == null)
throw new GraphFormatException(String.format("The transport type '%s' was not found.", type_str));
this.transportTypes.add(Graph.this.transportTypes.get(type_str));
}
}
@Override
public String toString() {
StringBuilder transports = new StringBuilder();
for(TransportType tt : this.getTransportTypes()) {
if(transports.length() != 0) {
transports.append('+');
}
transports.append(tt.getName());
}
Properties props = new Properties();
props.setProperty(PROPERTY_EDGE, String.format("%s%s%s@%s", this.source.getId(), this.isOriented ? "-" : "=", this.target.getId(), transports));
return Parser.encodeProperties(props);
}
/**
* Creates a new edge according to the specification in <code>properties</code>.
* @param properties the specification of the edge.
* @throws GraphFormatException if there is a problem when parsing the specification.
*/
protected MultiEdge(Properties properties) throws GraphFormatException {
loadProperties(properties);
}
}
/**
* The list of actors in the game.
*/
private List<Actor> actors;
private List<Actor> actorsReadOnly;
/**
* The list of edges in the graph.
*/
private List<Edge> edges;
private List<Edge> edgesReadOnly;
/**
* The list of nodes in the graph.
*/
private Map<String, Node> nodes;
private Map<String, Node> nodesReadOnly;
/**
* The list of transport types used in this graph.
*/
private Map<String, TransportType> transportTypes;
private Map<String, TransportType> transportTypesReadOnly;
/**
* The number of turns after which the game ends.
*/
private int gameLength;
/**
* The numbers of turns since the beginning of the game, after
* which the phanom is revealed.
*/
private Set<Integer> phantomReveals;
private Set<Integer> phantomRevealsReadonly;
/**
* Adds universal edges between all nodes, where they did not exist.
*/
protected void addUniversalEdges() {
TransportType universal = getTransportType(PROPERTY_UNIVERSAL_TICKET);
for(Node source : nodes.values()) {
HashSet<Node> reachableNodes = new HashSet<Node>();
HashSet<Node> reachableByUniversals = new HashSet<Node>();
for(Edge e : source.outgoingEdges) {
if(e.getTransportType() == universal)
reachableByUniversals.add(e.getTarget());
else
reachableNodes.add(e.getTarget());
}
for(Node target : reachableNodes) {
if(reachableByUniversals.contains(target))
continue;
Edge e = new Edge(source, target, universal);
edges.add(e);
source.outgoingEdges.add(e);
target.incomingEdges.add(e);
}
}
}
/**
* Finds out which of the given phantoms are captured in the current
* state of the game.
* @return A collection of currently captured phantoms.
*/
public Collection<Actor> capturedPhantoms() {
Collection<Actor> captured = new ArrayList<Actor>(getActors().size());
for(Actor a : getActors()) {
if(a.isDetective()) {
continue;
}
assert(a.getCurrentPosition() != null);
for(Actor b : getActors()) {
if(b.isPhantom()) {
continue;
}
assert(b.getCurrentPosition() != null);
if(b.getCurrentPosition().equals(a.getCurrentPosition())) {
captured.add(a);
break;
}
}
}
return captured;
}
/**
* Creates a clone of the graph. Does not clone the histories
* of movements of the actors.
*/
public Object clone() {
try {
return new Graph(this.serialize());
}
catch(Exception e) {
throw new IllegalStateException("Failed to clone the graph", e);
}
}
/**
* Returns the actor object with the given ID.
*
* @param actorId the ID of the actor.
* @return the object representing the actor; returns <code>null</code>
* if no such actor was found.
*/
public Actor getActor(String actorId) {
if(actorId == null)
throw new IllegalArgumentException("actorId must not be null");
for(Actor actor : actors) {
if(actor.getId().equals(actorId))
return actor;
}
return null;
}
/**
* Returns the list of actors in the game.
* @return the list of actors in the game.
*/
public List<Actor> getActors() {
return this.actorsReadOnly;
}
/**
* Returns the list of actors of the given type.
* @param type the type of client, for which the actors are returned.
* @return the list of actors of the given type.
*/
public List<Actor> getActors(ClientType type) {
ArrayList<Actor> res = new ArrayList<Actor>();
for(Actor actor : actors) {
if(actor.isDetective() && type == ClientType.DETECTIVE)
res.add(actor);
else if(actor.isPhantom() && type == ClientType.PHANTOM)
res.add(actor);
}
return res;
}
/**
* Returns the list of edges in the game graph.
* @return the list of edges in the game graph.
*/
public List<Edge> getEdges() {
return this.edgesReadOnly;
}
/**
* Returns the number of turns after which the game ends.
* @return the number of turns after which the game ends.
*/
public int getGameLength() {
return this.gameLength;
}
/**
* Returns the node with the given ID.
* @param nodeId the ID of the node.
* @return the node with the given ID; returns <code>null</code>
* if no such node was found.
*/
public Node getNode(String nodeId) {
if(nodeId == null)
throw new IllegalArgumentException("nodeId must not be null");
return this.nodes.get(nodeId);
}
/**
* Returns the list of nodes in the game graph.
* @return the list of nodes in the game graph.
*/
public Map<String, Node> getNodes() {
return this.nodesReadOnly;
}
/**
* Returns numbers of the turns since the beginning of the game, in
* which the phantom is revealed to the detectives.
* @return a set that contains the numbers of turns.
*/
public Set<Integer> getPhantomReveals() {
return this.phantomRevealsReadonly;
}
/**
* Returns the list of available transport types in the game graph.
* @return the list of available transport types in the game graph.
*/
public Map<String, TransportType> getTransportTypes() {
return this.transportTypesReadOnly;
}
/**
* Returns the transport type with the given name.
* @param name the name of the transport type.
* @return the transport type with the given name.
*/
public TransportType getTransportType(String name) {
if(name == null)
throw new IllegalArgumentException("name must not be null");
return transportTypes.get(name);
}
/**
* Checks if an actor is mobile.
* @param actor The checked actor.
* @param other List of other actors of the same kind.
* @return <code>True</code> if the given actor can perform a move.
*/
protected boolean isMobile(Actor actor, List<Actor> others) {
Graph.Node currentPosition = actor.getCurrentPosition();
assert(currentPosition != null);
for(Graph.Edge edge : currentPosition.getOutgoingEdges()) {
if(actor.hasTicket(edge.getTransportType())) {
boolean canThere = true;
Graph.Node target = edge.getTarget();
for(Actor other : others) {
if(other == actor) {
continue;
}
if(target.equals(other.getCurrentPosition())) {
canThere = false;
break;
}
}
if(canThere) {
return true;
}
}
}
return false;
}
/**
* Loads the parameters of the game (number of turns, and turns, in
* which the phantom is revealed).
*
* @param gameSpec contains specification of the game.
* @throws GraphFormatException if there is a problem with loading the
* parameters of the game.
*/
protected void loadGameProperties(Properties gameSpec) throws GraphFormatException {
String turns_str = gameSpec.getProperty(PROPERTY_GAME);
if(turns_str == null || turns_str.isEmpty())
throw new GraphFormatException("The number of turns of the game was not specified");
try {
this.gameLength = Integer.parseInt(turns_str);
if(this.gameLength <= 0)
throw new GraphFormatException("The number of turns must be a positive number");
}
catch(NumberFormatException e) {
throw new GraphFormatException("THe number of turns is not a valid number");
}
String reveals_str = gameSpec.getProperty(PROPERTY_REVEALS);
if(reveals_str == null || reveals_str.isEmpty())
throw new GraphFormatException("The turns in which the phantom is revealed were not specified");
String[] reveals = reveals_str.split("\\s+");
for(String reveal_str : reveals) {
reveal_str = reveal_str.trim();
if(reveal_str.isEmpty())
continue;
try {
int reveal = Integer.parseInt(reveal_str);
this.phantomReveals.add(reveal);
}
catch(NumberFormatException e) {
throw new GraphFormatException(String.format("Turn number %d is not a valid number", reveal_str));
}
}
}
/**
* Loads the game graph from a list of strings. Each string specifies
* a single object in the game graph.
* @param records specifications of the objects in the game graph.
* @throws GraphFormatException if there is a problem when parsing the graph.
*/
protected void loadGraph(String[] lines) throws GraphFormatException {
if(lines == null)
throw new IllegalArgumentException("lines must not be null");
Properties[] records = new Properties[lines.length];
for(int i = 0; i < lines.length; i++) {
try {
records = Parser.parseLines(lines);
}
catch(Exception e) {
throw new GraphFormatException(e.getMessage(), i);
}
}
loadGraph(records);
}
/**
* Loads the game graph from a list of records. Each record specifies
* a single object in the game graph.
* @param records specifications of the objects in the game graph.
* @throws GraphFormatException if there is a problem when parsing the graph.
*/
protected void loadGraph(Properties[] records) throws GraphFormatException {
for(int i = 0; i < records.length; i++) {
try {
Properties properties = records[i];
if(properties.containsKey(PROPERTY_EDGE)) {
MultiEdge me = new MultiEdge(properties);
for(Edge e : me.getEdges()) {
e.getSource().outgoingEdges.add(e);
e.getTarget().incomingEdges.add(e);
edges.add(e);
}
}
else if(properties.containsKey(PROPERTY_NODE)) {
Node nd = new Node(properties);
nodes.put(nd.getId(), nd);
}
else if(properties.containsKey(PROPERTY_NODES)) {
if(!properties.containsKey(Nodes.PROPERTY_FROM)) {
throw new GraphFormatException("Invalid specification of nodes. Missin the \"from:\" part.");
}
if(!properties.containsKey(Nodes.PROPERTY_TO)) {
throw new GraphFormatException("Invalid specification of nodes. Missin the \"to:\" part.");
}
int start = Integer.parseInt(properties.getProperty(Nodes.PROPERTY_FROM));
int end = Integer.parseInt(properties.getProperty(Nodes.PROPERTY_TO));
for(int j = start; j <= end; j++) {
String id = Integer.toString(j);
Node nd = new Node(id);
nodes.put(id, nd);
}
}
else if(properties.containsKey(PROPERTY_TRANSPORT)) {
TransportType transport = new TransportType(properties);
transportTypes.put(transport.getName(), transport);
}
else if(properties.containsKey(PROPERTY_DETECTIVE) || properties.containsKey(PROPERTY_PHANTOM)) {
Actor actor = new Actor();
actor.loadProperties(this, properties, this.getTransportTypes());
this.actors.add(actor);
}
else if(properties.containsKey(PROPERTY_GAME)) {
loadGameProperties(properties);
}
}
catch(Exception err) {
throw new GraphFormatException(err.getMessage(), i);
}
}
// Verify that there is at least one phantom, and at least one detective
int phantoms = 0;
int detectives = 0;
for(Actor actor : this.actors) {
if(actor.isDetective())
detectives++;
else if(actor.isPhantom())
phantoms++;
else
throw new IllegalStateException("Actor is neither a detective nor a phantom: " + actor.getId());
}
if(phantoms == 0)
throw new GraphFormatException("No phantom objects were found");
if(detectives == 0)
throw new GraphFormatException("No detective objects were found");
addUniversalEdges();
}
/**
* Move the actors according to the request.
* @param actors Actors in this list are moved in the order they appear in the list.
* @param message The move request.
* @return A collection of actors which could not move.
* @throws ProtocolException If something undesirable not according to rules is requested.
*/
public Collection<Actor> moveActors(List<Actor> actors, MessageMoveBase message) throws ProtocolException {
assert(!actors.isEmpty());
List<Actor> immobile = new ArrayList<Actor>(actors.size());
List<Actor> moved = new ArrayList<Actor>(actors.size());
for(Actor mover : actors) {
MessageMoveBase.ActorMove[] moves = {null, null};
boolean didMove = false;
for(MessageMoveBase.ActorMove m : message.getMoves()) {
if(mover == m.getActor()) {
if(moves[0] == null) {
moves[0] = m;
} else if(moves[1] == null && mover.hasDoubleMove()) {
moves[1] = m;
} else {
throw new ProtocolException("Too many moves for a single actor.", null);
}
}
}
if(!isMobile(mover, actors)) {
immobile.add(mover);
}
for(MessageMoveBase.ActorMove move : moves) {
if(move == null) {
break;
}
assert(mover.equals(move.getActor()));
if(!isMobile(mover, actors)) {
throw new ProtocolException("Actor is immobile: " + mover.getId(), null);
}
Graph.Node target = move.getTargetNode();
TransportType tt = move.getTransportType();
for(Actor a : getActors()) {
assert(a.getCurrentPosition() != null);
// Note: It may be possible to move back to the origin.
if(a.getCurrentPosition() == target
&& !a.equals(mover)
&& a.isDetective() == mover.isDetective()) { // Cannot place two actors of the same type to the same node.
String errorMessage = String.format("Cannot place actor '%s' to node '%s', node is already occupied by actor '%s'",
mover.getId(), target.getId(), a.getId());
throw new ProtocolException(errorMessage, null);
}
}
mover.moveTo(target, tt);
didMove = true;
}
if(didMove) {
moved.add(mover);
}
}
if(moved.size() + immobile.size() != actors.size()) {
StringBuilder sb = new StringBuilder();
for(Actor a : actors) {
if(!moved.contains(a) && !immobile.contains(a)) {
if(sb.length() > 0) sb.append(", ");
sb.append(a.getId());
}
}
throw new ProtocolException("Some actor did not move: " + sb, null);
}
return immobile;
}
/**
* Place the actors according to the request. When using this method on
* server, be sure to call {@link #verifyActorPlacement(MessageMoveBase, Client)}
* beforehand.
*
* @param message The move request.
* @throws ProtocolException If something undesirable not according to rules is requested.
*/
public void placeActors(MessageMoveBase message) throws ProtocolException {
for(MessageMoveBase.ActorMove move : message.getMoves()) {
Actor actor = move.getActor();
Graph.Node target = move.getTargetNode();
for(Actor a : getActors()) {
if(a.getCurrentPosition() != null
&& a.getCurrentPosition().equals(target)
&& a.isDetective() == actor.isDetective()) { // Cannot place two actors of the same type to the same node.
throw new ProtocolException("Cannot place an actor to an already full node.", null);
}
}
actor.setCurrentPosition(target);
}
}
/**
* Verifies that the actor placement message is valid. Checks that exactly all
* actors of the client are placed.
*
* @param message the placement message.
* @param client the client that sent the message.
* @throws ProtocolException if the placement is not valid.
*/
public void verifyActorPlacement(MessageMoveBase message, Client client) throws ProtocolException {
if(message == null)
throw new IllegalArgumentException("message must not be null");
if(client == null)
throw new IllegalArgumentException("client must not be null");
HashSet<Actor> placedActors = new HashSet<Actor>();
HashSet<Node> usedNodes = new HashSet<Node>();
HashSet<Actor> allActors = new HashSet<Actor>(getActors(client.getClientType()));
for(ActorMove move : message.getMoves()) {
Actor actor = move.getActor();
if(actor.getCurrentPosition() != null)
throw new ProtocolException("Position was already assigned to actor: " + actor.getId(), client);
if(placedActors.contains(actor))
throw new ProtocolException("Position was specified multiple times to actor: " + actor.getId(), client);
if(!allActors.contains(actor))
throw new ProtocolException("Client placed other player's actor: " + actor.getId(), client);
placedActors.add(actor);
if(move.getTargetNode() == null)
throw new ProtocolException("The target node was not specified for actor: " + actor.getId(), client);
if(move.getTargetNode().isOccupied(client.getClientType()))
throw new ProtocolException(String.format("Actor %s cannot be placed to node %s, the node is already occupied",
actor.getId(), move.getTargetNode().getId()),
client);
if(move.getTransportType() != null)
throw new ProtocolException("Transport type was specified in the placement message for actor: " + actor.getId(), client);
if(usedNodes.contains(move.getTargetNode()))
throw new ProtocolException(String.format("Actor %s cannot be placed to node %s, the node is already occupied",
actor.getId(), move.getTargetNode().getId()),
client);
usedNodes.add(move.getTargetNode());
}
for(Actor actor : allActors) {
if(!placedActors.contains(actor))
throw new ProtocolException("Actor was not placed: " + actor.getId(), client);
}
}
/**
* Updates positions and tickets according to the 'update'
* message. This method should never be used on the server, as it does not check
* that the moves are valid, only updates positions of the actors and their tickets.
*
* @param message the message, according to which the actors are updated.
*/
public void updateActors(MessageMoveBase message) {
for(MessageMoveBase.ActorMove move : message.getMoves()) {
Actor actor = move.getActor();
Graph.Node target = move.getTargetNode();
TransportType transport = move.getTransportType();
// Update position of the actor
actor.setCurrentPosition(null);
if(target != null)
actor.setCurrentPosition(target);
// Remove one ticket for the used transport type
if(transport != null)
actor.addTickets(transport, -1);
}
// If the message is MessageUpdate, also update the numbers of tickets
// and captured phantoms
if(message instanceof MessageUpdate) {
MessageUpdate msgUpdate = (MessageUpdate)message;
for(Actor phantom : msgUpdate.getCapturedPhantoms())
phantom.capture();
for(ActorTickets tickets : msgUpdate.getActorTickets()) {
Actor actor = tickets.getActor();
for(Entry<TransportType, Integer> ttype : tickets.getTickets().entrySet()) {
actor.setNumberOfTickets(ttype.getKey(), ttype.getValue());
}
}
}
}
/**
* Serializes the graph to the same format, as the one, from which the graph
* is loaded.
*
* @return serialized version of the graph.
*/
public Properties[] serialize() {
Properties[] properties = new Properties[getNodes().size() + getEdges().size() + getActors().size() + getTransportTypes().size() + 1];
int pos = 0;
Properties game = new Properties();
game.setProperty(PROPERTY_GAME, Integer.toString(getGameLength()));
StringBuilder reveals_str = new StringBuilder();
for(Integer reveal : phantomReveals) {
if(reveals_str.length() > 0)
reveals_str.append(' ');
reveals_str.append(reveal.intValue());
}
game.setProperty(PROPERTY_REVEALS, reveals_str.toString());
properties[pos++] = game;
for(TransportType transport : transportTypes.values()) {
properties[pos++] = transport.serialize();
}
for(Node nd : getNodes().values()) {
properties[pos++] = nd.serialize();
}
for(Edge e : getEdges()) {
properties[pos++] = e.serialize();
}
for(Actor a : getActors()) {
properties[pos++] = a.serialize();
}
return properties;
}
/**
* Creates a new empty game graph.
*/
public Graph() {
this.init();
}
/**
* Creates a graph using the given specification.
* @param lines specifications of the objects in the game graph.
* @throws GraphFormatException if there is a problem when parsing the graph.
*/
public Graph(String[] lines) throws GraphFormatException {
this.init();
this.loadGraph(lines);
}
/**
* Creates a graph from the given specification.
* @param records specification of the objects in the game graph.
* @throws GraphFormatException if there is a problem when parsing the graph.
*/
public Graph(Properties[] records) throws GraphFormatException {
this.init();
this.loadGraph(records);
}
/**
* Creates a new graph from a specification in a file.
*
* @param graphFile the file, from which the graph is loaded.
* @throws IOException if there is a problem with parsing the graph.
* @throws ParserException if the format of the graph is not valid.
*/
public Graph(File graphFile) throws IOException, ParserException {
this.init();
Properties[] records = Parser.parse(graphFile);
loadGraph(records);
}
protected void init() {
this.actors = new ArrayList<Actor>();
this.actorsReadOnly = Collections.unmodifiableList(this.actors);
this.edges = new ArrayList<Edge>();
this.edgesReadOnly = Collections.unmodifiableList(this.edges);
this.nodes = new HashMap<String, Node>();
this.nodesReadOnly = Collections.unmodifiableMap(this.nodes);
this.transportTypes = new HashMap<String, TransportType>();
this.transportTypesReadOnly = Collections.unmodifiableMap(this.transportTypes);
this.phantomReveals = new HashSet<Integer>();
this.phantomRevealsReadonly = Collections.unmodifiableSet(this.phantomReveals);
try {
this.transportTypes.put(PROPERTY_UNIVERSAL_TICKET,
new TransportType(PROPERTY_UNIVERSAL_TICKET, TransportType.USER_ANY));
}
catch(GraphFormatException e) {
// This should never happen
throw new IllegalStateException("Could not create the 'universal' transport type");
}
}
}