/*
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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import cz.matfyz.aai.fantom.game.Graph.Edge;
import cz.matfyz.aai.fantom.game.Graph.Node;
import cz.matfyz.aai.fantom.message.ClientType;
import cz.matfyz.aai.fantom.message.MessageMoveBase.ActorMove;
import cz.matfyz.aai.fantom.server.ProtocolException;
/**
* Represents a single actor (detective, phantom) on the game board.
* Each actor is characterized by its current position, and the transport
* tickets available to it.
*/
public class Actor {
/**
* Records the change in position of this actor.
*/
public class PositionChange {
/**
* The node, where the actor started.
*/
private Node sourceNode;
/**
* The node, to which the actor moved.
*/
private Node targetNode;
/**
* The transport type used for the movement.
*/
private TransportType transportType;
/**
* Returns the actor, for which this change was recorded.
* @return the actor, for which this change was recorded.
*/
public Actor getActor() {
return Actor.this;
}
/**
* Returns the node, from which the actor moved.
* @return the node, from which the actor moved.
*/
public Node getSourceNode() {
return this.sourceNode;
}
/**
* Returns the node, to which the actor moved.
* @return the node, to which the actor moved.
*/
public Node getTragetNode() {
return this.targetNode;
}
/**
* Returns the transport type used for the movement.
* @return the transport type used for the movement.
*/
public TransportType getTransportType() {
return this.transportType;
}
/**
* Initializes a new change in position of the actor.
* @param sourceNode the node, from which the agent moved.
* @param targetNode the node, to which the agent moved.
* @param trasnportType the type of transport used by the agent.
*/
public PositionChange(Node sourceNode, Node targetNode, TransportType transportType) {
this.sourceNode = sourceNode;
this.targetNode = targetNode;
this.transportType = transportType;
}
}
/**
* The current position of the actor on the game board (i. e. the
* node on which the actor stands).
*/
private Graph.Node currentPosition;
/**
* The number of double move tickets available to the actor.
*/
private int doubleMoves;
/**
* The numbers of tickets available to the actor.
*/
private Map<TransportType, Integer> tickets;
private Map<TransportType, Integer> ticketsReadOnly;
/**
* An ID of this actor.
*/
private String id;
/**
* Specifies, if the actor is a phantom..
*/
private boolean isPhantom = false;
/**
* Only applies to the phantom. Set to <code>true</code>, if the phantom
* was captured by the detectives.
*/
private boolean isCaptured = false;
/**
* Checks if the actor can move from <code>source</code> to
* <code>destination</code> in one turn (i.e. by using a single edge,
* or by using a double move).
*
* @param source the node where the actor starts.
* @param destination the node where the actor ends.
* @return <code>true</code> if the actor can move to <code>destination</code>
* from <code>source</code>; otherwise, <code>false</code>.
*/
public boolean canMove(Graph.Node source, Graph.Node destination) {
return canMoveSingleEdge(source, destination)
|| canMoveDoubleMove(source, destination);
}
/**
* Checks if the actor can move from <code>source</code> to
* <code>destination</code> using double move.
*
* @param source the node where the actor starts.
* @param destination the node where the actor ends.
* @return <code>true</code> if the actor can move to <code>destination</code>
* from <code>source</code> using a double move; otherwise,
* <code>false</code>.
*/
public boolean canMoveDoubleMove(Graph.Node source, Graph.Node destination) {
if(getDoubleMoves() == 0)
return false;
if(destination.isOccupied(getClientType()) && !destination.equals(getCurrentPosition()))
return false;
for(Graph.Edge e : source.getOutgoingEdges()) {
if(hasTickets(e.getTransportType(), 1)
&& (!e.getTarget().isOccupied(getClientType())
|| e.getTarget().equals(getCurrentPosition()))) {
for(Graph.Edge e2 : e.getTarget().getOutgoingEdges()) {
if(e2.getTarget() == destination) {
if(hasTicket(e.getTransportType(), e2.getTransportType()))
return true;
}
}
}
}
return false;
}
/**
* Checks if the actor can move from <code>source</code> to
* <code>destination</code> using a single edge.
*
* @param source the node where the actor starts.
* @param destination the node, where the actor ends.
* @return <code>true</code> if the actor can move to <code>destination</code>
* from <code>source</code> using a single edge; otherwise,
* <code>false</code>.
*/
public boolean canMoveSingleEdge(Graph.Node source, Graph.Node destination) {
return canMoveSingleEdge(source, destination, null);
}
/**
* Checks if the actor can move from <code>source</code> to
* <code>destination</code> using a single edge and the given transport type.
*
* @param source the node where the actor starts.
* @param destination the node, where the actor ends.
* @param transport the requested transport type. if null, the transport type is not specified and any can be used
* @return <code>true</code> if the actor can move to <code>destination</code>
* from <code>source</code> using a single edge; otherwise,
* <code>false</code>.
*/
public boolean canMoveSingleEdge(Graph.Node source, Graph.Node destination, TransportType transport) {
if(destination == null)
throw new IllegalArgumentException("destination must not be null");
if(source == null)
throw new IllegalArgumentException("source must not be null");
if(destination.isOccupied(getClientType())
&& !destination.equals(getCurrentPosition()))
return false;
for(Graph.Edge e : source.getOutgoingEdges()) {
if(e.getTarget() == destination
&& hasTicket(e.getTransportType())
&& (transport == null || e.getTransportType().equals(transport)))
return true;
}
return false;
}
/**
* Determines, if the actor can move from its current position to the
* given node in the graph.
* @param destination the node in the graph, for which the check is
* performed.
* @return <code>true</code> if the actor can move to <code>destination</code>;
* otherwise, <code>false</code>.
*/
public boolean canMoveTo(Graph.Node destination) {
if(currentPosition == null)
throw new IllegalStateException("the current position of the actor is not defined");
return canMove(currentPosition, destination);
}
/**
* Determines, if the actor can move from its current position to the
* given node in the graph using the given transport type.
* @param destination the node in the graph, for which the check is
* performed.
* @param transport the desired transport type
* @return <code>true</code> if the actor can move to <code>destination</code> using the transport type;
* otherwise, <code>false</code>.
*/
public boolean canMoveTo(Graph.Node destination, TransportType transport) {
if(currentPosition == null)
throw new IllegalStateException("the current position of the actor is not defined");
return canMoveSingleEdge(currentPosition, destination, transport);
}
/**
* Sets the 'captured' flag for this actor. This method can only be called
* for phantoms.
*
* @throws IllegalStateException if the method is called for a detective.
*/
public void capture() {
if(!isPhantom)
throw new IllegalStateException("Only phantoms can be captures");
isCaptured = true;
}
/**
* Compares the actor object with another actor for equality. Two actor
* objects are equal, if they have the same ID, and the same numbers of tickets.
*/
@Override
public boolean equals(Object otherObject) {
if(!(otherObject instanceof Actor))
return false;
Actor other = (Actor)otherObject;
if (!other.getId().equals(this.getId())
|| other.getDoubleMoves() != this.getDoubleMoves()
|| other.getClientType() != this.getClientType()
|| other.isCaptured() != this.isCaptured())
return false;
if((this.getCurrentPosition() == null && other.getCurrentPosition() != null)
|| (this.getCurrentPosition() != null && !this.getCurrentPosition().equals(other.getCurrentPosition())))
return false;
for(TransportType transport : other.getTickets().keySet()) {
if(other.getNumberOfTickets(transport) != this.getNumberOfTickets(transport))
return false;
}
for(TransportType transport : this.getTickets().keySet()) {
if(other.getNumberOfTickets(transport) != this.getNumberOfTickets(transport))
return false;
}
return true;
}
/**
* Returns the type of client to which the actor belongs.
* @return the type of client to which the actor belongs.
*/
public ClientType getClientType() {
if(isPhantom)
return ClientType.PHANTOM;
else
return ClientType.DETECTIVE;
}
/**
* Returns the current position of the actor.
* @return the current position of the actor.
*/
public Graph.Node getCurrentPosition() {
return currentPosition;
}
/**
* Sets the current position.
* @param position The assigned position of the actor.
*/
public void setCurrentPosition(Graph.Node position) {
this.currentPosition = position;
}
/**
* Returns the ID of the actor.
* @return the ID of the actor.
*/
public String getId() {
if(this.id == null)
throw new IllegalStateException("ID of an actor is not set.");
return this.id;
}
/**
* Returns the number of double move tickets available to the actor.
* @return the number of double move tickets available to the actor.
*/
public int getDoubleMoves() {
return doubleMoves;
}
/**
* Returns true if this actor has a double-move ticket.
* @return true if this actor has a double-move ticket.
*/
public boolean hasDoubleMove() {
return getDoubleMoves() > 0;
}
/**
* Returns the list of single edge moves for this actor at the current
* state of the game. Takes available tickets into account, but does not
* consider positions of other actors (as they may change in the same
* movement message).
*
* @param graph the graph, for which the movements are determined.
* @return the list of legal moves;
*/
public List<ActorMove> getLegalMoves(Graph graph) {
ArrayList<ActorMove> moves = new ArrayList<ActorMove>();
if(isCaptured())
return moves;
if(currentPosition == null) {
for(Node n : graph.getNodes().values())
moves.add(new ActorMove(this, n, null));
}
else {
for(Edge e : currentPosition.getOutgoingEdges()) {
if(hasTicket(e.getTransportType())) {
moves.add(new ActorMove(this, e.getTarget(), e.getTransportType()));
}
}
}
return moves;
}
/**
* Returns true, if this actor is a phantom, and it was already captured
* by the detectives.
* @return true, if this actor is a phantom that was already captured.
*/
public boolean isCaptured() {
return isCaptured;
}
/**
* Returns the number of tickets of the given type available to the
* actor.
* @param transport the transport type, for which the number of
* tickets is returned.
* @return the number of tickets of the given type available to the actor.
*/
public int getNumberOfTickets(TransportType transport) {
if(transport == null)
throw new IllegalArgumentException("transport must not be null");
Integer ticketCount = tickets.get(transport);
if(ticketCount != null)
return ticketCount.intValue();
return 0;
}
/**
* Sets the number of tickets of the given type available to the
* actor.
* @param transport the transport type, for which the number of
* tickets is set.
* @param count the set value
*/
public void setNumberOfTickets(TransportType transport, int count) {
if( (this.isDetective() && !transport.isUsedByDetective())
|| (this.isPhantom() && !transport.isUsedByPhantom()) ) {
throw new IllegalArgumentException("setting tickets which are not allowed");
}
tickets.put(transport, count);
}
/**
* Adds the given number of tickets of the given type.
* @param transport the transport type, for which the number of
* tickets is added.
* @param count the set value
*/
public void addTickets(TransportType transport, int count) {
setNumberOfTickets(transport, count + getNumberOfTickets(transport));
}
/**
* Adds one ticket of the given type.
* @param transport the transport type, for which the number of
* tickets is added.
* @param count the set value
*/
public void addTicket(TransportType transport) {
addTickets(transport, 1);
}
/**
* Returns the number of tickets available to the actor.
* @return the number of tickets available to the actor.
*/
public Map<TransportType, Integer> getTickets() {
return ticketsReadOnly;
}
@Override
public int hashCode() {
return id.hashCode();
}
/**
* Checks if the player can use the specified transport type (i.e.
* has ticket of the given type, or an universal ticket).
*
* @param transport the transport type.
* @return <code>true</code> if the player can use this transport type;
* otherwise, <code>false</code>.
*/
public boolean hasTicket(TransportType transport) {
return hasTickets(transport, 1);
}
/**
* Checks if the player can use the specified transport types (i.e.
* has the tickets for the given transport types, or enough universal
* tickets).
*
* @param transport1 the first type of transport.
* @param transport2 the second type of transport.
* @return <code>true</code> if the player can use these transport types;
* otherwise, <code>false</code>.
*/
public boolean hasTicket(TransportType transport1, TransportType transport2) {
return hasTickets(transport1, 1, transport2, 1);
}
/**
* Checks if the player has the required number of tickets available.
* Uses the universal tickets, if necessary.
*
* @param transport the type of ticket.
* @param count the requested number of tickets.
* @return <code>true</code>, if the requested number of tickets is
* available; otherwise, <code>false</code>.
*/
public boolean hasTickets(TransportType transport, int count) {
return count <= getNumberOfTickets(transport);
}
/**
* Checks if the player has the required number of tickets available.
* Uses the universal tickets, if necessary. This is version for the
* double moves.
*
* @param transport the type of ticket.
* @param count the requested number of tickets.
* @return <code>true</code>, if the requested number of tickets is
* available; otherwise, <code>false</code>.
*/
public boolean hasTickets(TransportType transport1, int count1, TransportType transport2, int count2) {
if(transport1 == transport2)
return hasTickets(transport1, count1 + count2);
else {
return count1 <= getNumberOfTickets(transport1)
&& count2 <= getNumberOfTickets(transport2);
}
}
/**
* Checks if the actor is hidden to other players.
* @return <code>true</code> if the actor is hidden to other players;
* otherwise, <code>false</code>.
*/
public boolean isHidden() {
return isPhantom();
}
/**
* Checks if this actor is a phantom.
* @return <code>true</code> if this actor is a phantom.
*/
public boolean isPhantom() {
return this.isPhantom;
}
/**
* Checks if this actor is a detective.
* @return <code>true</code> if this actor is a detective.
*/
public boolean isDetective() {
return !this.isPhantom;
}
/**
* Loads the information about the edge from <code>properties</code>.
* @param properties The specification of the edge.
* @param transportTypes All available transport types. Mapping from names to types.
* @throws GraphFormatException If there is a problem when processing the properties.
*/
protected void loadProperties(Graph graph, Properties properties, Map<String, TransportType> transportTypes) throws GraphFormatException {
String whoami = null;
for(String property : properties.stringPropertyNames()) {
if(property.equals(Graph.PROPERTY_DETECTIVE) || property.equals(Graph.PROPERTY_PHANTOM)) {
whoami = property;
this.id = properties.getProperty(property);
this.isPhantom = property.equals(Graph.PROPERTY_PHANTOM);
} else if(property.equals(Graph.PROPERTY_DOUBLE_TICKET)) {
this.doubleMoves = Integer.parseInt(properties.getProperty(property));
} else if(property.equals(Graph.PROPERTY_POSITION)) {
this.currentPosition = graph.getNode(properties.getProperty(property));
} else {
TransportType transport = transportTypes.get(property);
if(transport == null) {
throw new GraphFormatException("Unknown transport type: " + property);
}
Integer cnt = Integer.valueOf(properties.getProperty(property));
this.tickets.put(transport, cnt);
}
}
if(whoami == null) {
throw new GraphFormatException("Unknown actor type.");
}
}
/**
* Performs a move of the actor to the given node, using the given transport
* type.
* @param target the target node for the movement.
* @param transport the transport type used for the movement.
* @throws ProtocolException if the actor cannot perform such move.
* @returns information about the movement of the actor.
*/
public PositionChange moveTo(Node target, TransportType transport) throws ProtocolException {
Node origin = getCurrentPosition();
if(!canMoveTo(target, transport))
throw new ProtocolException("Cannot perform the requested move", null);
// Use one ticket for the selected transport type
addTickets(transport, -1);
// Update position of the actor
this.currentPosition = target;
return new PositionChange(origin, target, transport);
}
/**
* Undoes a move, previously performed by the actor.
* @param change information about the movement to be undone.
*/
public void undoMove(PositionChange change) {
if(change == null)
throw new IllegalArgumentException("Change must not be null");
if(change.getActor() != this)
throw new IllegalArgumentException("The change object was created for a different actor");
if(getCurrentPosition() != change.getTragetNode())
throw new IllegalArgumentException("Cannot undo the change, the actor is at a different position");
this.currentPosition = change.getSourceNode();
addTickets(change.getTransportType(), 1);
}
/**
* Serializes the actor to the format, from which it was loaded.
* @return the specification of the actor.
*/
public Properties serialize() {
Properties res = new Properties();
if(getCurrentPosition() != null)
res.setProperty(Graph.PROPERTY_POSITION, getCurrentPosition().getId());
res.setProperty(isPhantom ? Graph.PROPERTY_PHANTOM : Graph.PROPERTY_DETECTIVE, getId());
res.setProperty(Graph.PROPERTY_DOUBLE_TICKET, Integer.toString(getDoubleMoves()));
for(Entry<TransportType, Integer> ticket : tickets.entrySet()) {
res.setProperty(ticket.getKey().getName(), ticket.getValue().toString());
}
return res;
}
/**
* Creates a new actor on the given position in the graph.
*
*/
public Actor() {
this.tickets = new HashMap<TransportType, Integer>();
this.ticketsReadOnly = Collections.unmodifiableMap(this.tickets);
}
}