package org.gbcpainter.game.levels;
import net.jcip.annotations.ThreadSafe;
import org.gbcpainter.env.GameSettings;
import org.gbcpainter.game.model.Monster;
import org.gbcpainter.game.model.Player;
import org.gbcpainter.game.model.grid.Junction;
import org.gbcpainter.game.model.grid.MapGridElement;
import org.gbcpainter.game.model.grid.Pipe;
import org.gbcpainter.game.view.animations.*;
import org.gbcpainter.geom.PERPENDICULAR_DIRECTION;
import org.gbcpainter.geom.Segment;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jgrapht.Graph;
import org.jgrapht.graph.SimpleGraph;
import org.jgrapht.graph.UnmodifiableUndirectedGraph;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* Implementation of the {@link Level} interface.
* <p/>
* The level starts with a given number of lives and a given initial score 0
*
* @author Lorenzo Pellegrini
*/
@ThreadSafe
public class SimpleLevelImpl implements Level, LivesLevel, ScoreLevel {
private static final PERPENDICULAR_DIRECTION INITIAL_DIRECTION = PERPENDICULAR_DIRECTION.DOWN;
private static final int COLORING_SCORE = 10;
private static final int FACE_COLORING_SCORE = 100;
/* Start model data */
@NotNull
private final SimpleGraph<Junction, Pipe> levelGraph;
@NotNull
private final Map<Monster, MapGridElement> monstersPosition;
/**
* The player and its grid position
*/
@NotNull
private final Pair<Player, MapGridElement> painter;
/**
* If the associated boolean is true, the face identified with the key is colored
*/
@NotNull
private final Map<Integer, Boolean> facesColor;
/* End model data */
/**
* Each pipe is associated with the faces' perimeters it belongs to
* <p/>
* By the fact a pipe can be in the perimeter of two faces, a Pair is used to keep the faces.
*/
@NotNull
private final Map<Pipe, Pair<Set<Pipe>, Set<Pipe>>> segmentsPerimeter;
/**
* The Face: Face ID mapping
*/
@NotNull
private final Map<Set<Pipe>, Integer> facesMap;
/**
* Keeps the list of the elements modified (colored or decolored) in the last step
*/
@NotNull
private final Set<MapGridElement> modifiedMapElements = new HashSet<>();
/* Player arrows */
@NotNull
private final Set<PERPENDICULAR_DIRECTION> playerArrows = EnumSet
.noneOf( PERPENDICULAR_DIRECTION.class );
@NotNull
private final Set<PERPENDICULAR_DIRECTION> playerDirectionNew = EnumSet
.noneOf( PERPENDICULAR_DIRECTION.class );
@Nullable
private PERPENDICULAR_DIRECTION playerDirection = INITIAL_DIRECTION;
/* End dynamic data */
/* Start initial level data */
private final int startLives;
@NotNull
private final Point initialPainterPosition;
@NotNull
private final MapGridElement initialPainterFooting;
@NotNull
private final Map<Monster, Point> initialMonstersPosition;
@NotNull
private final Map<Monster, MapGridElement> initialMonstersFooting;
/* End initial level data */
/**
* Keeps only {@link org.gbcpainter.game.view.animations.AnimatedElement} monsters
*/
@NotNull
private final Map<AnimatedElement, PathAnimation> monstersAndAnimations;
private int coloredFaces = 0;
private int actualLives;
private long score;
/**
* Set to true if the player was hit
*/
private boolean playerHitted = false;
/**
* Keeps the status of the level
*/
private LEVEL_STATE state = LEVEL_STATE.BEFORE_START;
/**
* Keeps the time of the last call to {@link #doMove()}
*/
private long actualStepTime = 0;
/**
* If true, this is the first step of the level
* <p/>
* It is set to false at the first run of {@link #doMove()}.
*/
private boolean firstStep = true;
/* End status variables*/
public SimpleLevelImpl( @NotNull Player player,
@NotNull SimpleGraph<Junction, Pipe> graph,
@NotNull Map<Set<Pipe>, Integer> facesMap,
int startLives,
int startScore,
@NotNull Set<Monster> levelMonsters ) {
/* Store 'easy' data and create Collections */
this.levelGraph = graph;
this.facesMap = facesMap;
this.startLives = startLives;
this.actualLives = startLives;
this.score = startScore;
this.monstersPosition = new HashMap<>( levelMonsters.size() );
this.initialMonstersFooting = new HashMap<>( levelMonsters.size() );
this.initialMonstersPosition = new HashMap<>( levelMonsters.size() );
this.initialPainterPosition = player.getPosition();
this.monstersAndAnimations = new HashMap<>();
/* Assign the grid element to the player */
final MapGridElement footingExistenceCheck = findElement( this.levelGraph,
player.getPosition() );
if ( footingExistenceCheck == null ) {
/* Error in level definition file... */
throw new IllegalArgumentException( "Painter is not on the game grid " + player
.getPosition() );
}
this.initialPainterFooting = footingExistenceCheck;
this.painter = new Pair<>( player, this.initialPainterFooting );
player.setOwner( this );
/* Color initial position */
this.initialPainterFooting.setColored( this.initialPainterPosition, true );
for (Monster mob : levelMonsters) {
/* First, set the owner of the monster */
mob.setOwner( this );
/*
Search the element whose the monster is over and
assign it to the monster in the footing map
*/
final MapGridElement mobFooting = findElement( levelGraph, mob.getPosition() );
if ( mobFooting == null ) {
/* Error in level definition file... */
throw new IllegalArgumentException( "Monster is not on the game grid" );
}
this.monstersPosition.put( mob, mobFooting );
this.initialMonstersFooting.put( mob, mobFooting );
this.initialMonstersPosition.put( mob, mob.getPosition() );
if ( mob instanceof AnimatedElement ) {
/*
Add to the animations map -> makes map larger with all the keys already added.
This speeds up first launch
*/
this.monstersAndAnimations.put( ( (AnimatedElement) mob ), null );
}
}
this.segmentsPerimeter = new HashMap<>( facesMap.values().size() );
this.facesColor = new HashMap<>( facesMap.values().size() );
/* A pipe can belong to the perimeter of 1 or 2 faces */
for (Map.Entry<Set<Pipe>, Integer> faceEntry : facesMap.entrySet()) {
final Set<Pipe> face = faceEntry.getKey();
final Integer faceID = faceEntry.getValue();
/* Initialize this face as non colored */
this.facesColor.put( faceID, false );
/*
A pipe can belong to the perimeter of 1 or 2 faces so
a pair of Set<Pipe>, each one defining a perimeter, will be used
*/
for (Pipe perimeterPart : face) {
/* First, check if a face for this pipe was already assigned */
Pair<Set<Pipe>, Set<Pipe>> facesHolder = this.segmentsPerimeter.get(
perimeterPart );
if ( facesHolder == null ) {
/* If it is the first pipe's face, create a new Pair and push it in the map */
facesHolder = new Pair<>( face, null );
this.segmentsPerimeter.put( perimeterPart, facesHolder );
} else {
/*
If it is the second pipe's face, add the Set<Pipe> defining the face to
the existing Pair in the Map
*/
facesHolder.setSecond( face );
}
}
}
}
/**
* Utility methods that searches for an element in the graph in both vertexes and edges given a
* point of that element
*
* @param graph The graph to search in
* @param position The element to search for
*
* @return The found element or null if none of the graph's elements contains the point
*/
@Nullable
private static MapGridElement findElement( @NotNull Graph<Junction, Pipe> graph,
@NotNull Point position ) {
for (Junction junction : graph.vertexSet()) {
if ( junction.getPosition().equals( position ) ) {
return junction;
}
}
for (Pipe pipe : graph.edgeSet()) {
if ( ! pipe.isVoidPipe() && pipe.contains( position ) ) {
return pipe;
}
}
return null;
}
/**
* Finds the element attached to a given a map element and given the direction to follow
*
* @param graph The graph of the map
* @param element The element to start from
* @param direction The direction to look at
*
* @return The element in that direction
*
* @throws NoSuchElementException If an element couldn't be founs in that direction of if the
* starting element is not in the map
*/
@NotNull
private static MapGridElement findElementRelativeTo( final @NotNull
Graph<Junction, Pipe> graph,
final @NotNull MapGridElement element,
final @NotNull
PERPENDICULAR_DIRECTION direction )
throws NoSuchElementException {
if ( element instanceof Junction ) {
/* JUNCTION */
final Point elementPosition = ( (Junction) element ).getPosition();
final Set<Pipe> connectedEdges = graph.edgesOf( (Junction) element );
assert ! connectedEdges.isEmpty();
for (final Pipe edge : connectedEdges) {
Segment geomEdge = edge.getSegment();
Point otherPoint = geomEdge.getA();
if ( otherPoint.equals( elementPosition ) ) {
otherPoint = geomEdge.getB();
}
boolean isCandidate = false;
switch ( direction ) {
case LEFT:
if ( otherPoint.x < elementPosition.x ) {
isCandidate = true;
}
break;
case RIGHT:
if ( otherPoint.x > elementPosition.x ) {
isCandidate = true;
}
break;
case UP:
if ( otherPoint.y < elementPosition.y ) {
isCandidate = true;
}
break;
case DOWN:
if ( otherPoint.y > elementPosition.y ) {
isCandidate = true;
}
break;
default:
throw new AssertionError( "Invalid enum value " + direction );
}
if ( isCandidate ) {
if ( edge.isVoidPipe() ) {
final Junction otherJunction = graph.getEdgeSource( edge );
return otherJunction.getPosition()
.equals( ( (Junction) element ).getPosition() ) ? graph
.getEdgeTarget(
edge ) : otherJunction;
} else {
return edge;
}
}
}
throw new NoSuchElementException( "Can't find a valid element in that direction" );
} else if ( element instanceof Pipe ) {
/* PIPE */
final Junction firstElement = graph.getEdgeSource( (Pipe) element );
final Junction secondElement = graph.getEdgeTarget( (Pipe) element );
final Point firstPosition = firstElement.getPosition();
final Point secondPosition = secondElement.getPosition();
switch ( direction ) {
case LEFT:
return ( firstPosition.x < secondPosition.x ) ? firstElement : secondElement;
case RIGHT:
return ( firstPosition.x > secondPosition.x ) ? firstElement : secondElement;
case UP:
return ( firstPosition.y < secondPosition.y ) ? firstElement : secondElement;
case DOWN:
return ( firstPosition.y > secondPosition.y ) ? firstElement : secondElement;
default:
throw new AssertionError( "Invalid enum value " + direction );
}
} else {
throw new IllegalArgumentException( "Element is not a valid Junction or Pipe" );
}
}
/**
* Utility method that pushes a path animation to a map given the grid element it must be
* assigned to, the coloring flag and the affected segment.
*
* @param animationsStructure The data structure to which the data will be pushed
* @param gridElement The grid element affected by the animation
* @param animation The animation
* @param coloring The coloring flag, true if it is a coloring animation, false if
* decoloring
* @param segment The part of the grid element affected by the animation
*/
private static void utilPushAnimation( @NotNull Map<MapGridElement,
Map<PathAnimation,
Pair<Boolean, Collection<Segment>>
>
> animationsStructure,
@NotNull MapGridElement gridElement,
@NotNull PathAnimation animation,
boolean coloring,
@NotNull Segment segment ) {
Map<PathAnimation, Pair<Boolean, Collection<Segment>>> animationPairMap = animationsStructure
.get( gridElement );
if ( animationPairMap == null ) {
animationPairMap = new HashMap<>( 1 );
animationsStructure.put( gridElement, animationPairMap );
}
Pair<Boolean, Collection<Segment>> segmentsAndColoringFlagPair = animationPairMap
.get( animation );
if ( segmentsAndColoringFlagPair == null ) {
final Collection<Segment> newCollection = new ArrayList<>( 1 );
segmentsAndColoringFlagPair = new Pair<>( coloring, newCollection );
animationPairMap.put( animation, segmentsAndColoringFlagPair );
}
final Collection<Segment> segments = segmentsAndColoringFlagPair.getSecond();
assert segments != null;
segments.add( segment );
}
@Override
public synchronized int getActualLives() {
return this.actualLives;
}
@Override
public int getInitialLives() {
return this.startLives;
}
@Override
public synchronized void setLives( final int howMany ) {
this.actualLives = howMany;
}
protected synchronized void setActualStepTime( final long actualStepTime ) {
this.actualStepTime = actualStepTime;
}
@NotNull
@Override
public synchronized Set<PERPENDICULAR_DIRECTION> getAvailablePlayerDirections()
throws IllegalArgumentException {
MapGridElement element = this.painter.getSecond();
assert element != null;
return element.getAvailableDirections();
}
@NotNull
@Override
public synchronized Point getPlayerPosition() {
Player player = this.painter.getFirst();
assert player != null;
return player.getPosition();
}
/**
* Ritorna il giocatore.
*
* @return Il giocatore del livello.
*/
@NotNull
@Override
public Player getPlayer() {
Player player = painter.getFirst();
assert player != null;
return player;
}
@Override
public synchronized boolean isAlive() {
return this.state != LEVEL_STATE.LEVEL_LOST;
}
@Override
public synchronized boolean isWin() {
return this.state == LEVEL_STATE.LEVEL_WIN;
}
@Override
public synchronized boolean isEnded() {
return ( this.getActualLives() <= 0 ) || this.isWin();
}
@NotNull
@Override
public synchronized Set<PERPENDICULAR_DIRECTION> getPlayerMovements() {
return EnumSet.copyOf( this.playerArrows );
}
@Override
public synchronized void setPlayerMovements(
@NotNull final Set<PERPENDICULAR_DIRECTION> directions ) {
this.playerDirectionNew.clear();
this.playerDirectionNew.addAll( directions );
}
@Override
public synchronized void doMove() throws Exception {
if ( ! this.isAlive() ) {
return;
}
/* Set global state variables */
this.state = LEVEL_STATE.RUNNING;
this.playerHitted = false;
this.setActualStepTime( System.currentTimeMillis() );
/* End setting global state variables */
/* Method "global" variables */
int previousColoredFacesNumber = this.coloredFaces;
boolean deadMan = false;
final long millisCallTime = System.currentTimeMillis();
final Map<MapGridElement,
Map<PathAnimation,
Pair<Boolean, Collection<Segment>>
>
> entitiesColoringTrace = new HashMap<>();
/* End global variables */
/* Player variables initialization */
this.playerArrows.clear();
this.playerArrows.addAll( this.playerDirectionNew );
final Set<PERPENDICULAR_DIRECTION> actualDirection = this.playerArrows;
final Player player = this.painter.getFirst();
assert player != null;
final boolean anyKeyPressed = ! actualDirection.isEmpty();
final boolean oppositePressed = ( actualDirection
.contains( PERPENDICULAR_DIRECTION.DOWN ) && actualDirection
.contains( PERPENDICULAR_DIRECTION.UP ) )
|| ( actualDirection
.contains( PERPENDICULAR_DIRECTION.RIGHT ) && actualDirection
.contains( PERPENDICULAR_DIRECTION.LEFT ) );
final boolean isMoving = anyKeyPressed && ! oppositePressed;
@Nullable
PathAnimation playerPath = null;
int remainingPlayerSpeed = isMoving ? player.getSpeed() : 0;
player.endStep();
/* End player variables */
/* Start monsters variables initialization */
final Map<Monster, Integer> roundRobin = new HashMap<>();
final Set<Monster> levelMonsters = this.getMonsters();
final Set<Monster> stillMonsters = new HashSet<>();
for (Monster mob : levelMonsters) {
if ( mob instanceof AnimatedElement ) {
this.monstersAndAnimations.put( ( (AnimatedElement) mob ), null );
}
final int mobSpeed = mob.getSpeed();
if ( mobSpeed > 0 ) {
roundRobin.put( mob, mobSpeed );
} else {
stillMonsters.add( mob );
}
mob.endStep();
}
/* End monsters variables and initialization */
/* Start changes reset from previous move */
this.resetModifiedElements();
for (Pipe pipe : this.getMap().edgeSet()) {
pipe.clearAnimations();
}
for (Junction junction : this.getMap().vertexSet()) {
junction.clearAnimations();
}
/* End changes reset from previous move */
/*
This is the main loop
Every entity is moved ny a number of places equal to its speed
Monsters aren't moved in a burst!
A round robin algorithm is used instead.
Monsters and player speed was previously saved. This will be used as a counter.
Every monster and the player moves by one place each loop and the counter associated is lowered by one.
The loop ends when every entity has a 0 counter
*/
while ( ( ! roundRobin.isEmpty() ) || remainingPlayerSpeed > 0 ) {
if ( remainingPlayerSpeed > 0 ) {
final Point playerPosition = player.getPosition();
/* Muovo il giocatore */
remainingPlayerSpeed--;
int elem = 0;
PERPENDICULAR_DIRECTION[] choices = new PERPENDICULAR_DIRECTION[2];
Set<PERPENDICULAR_DIRECTION> availableDirections = this.getAvailablePlayerDirections();
if ( actualDirection.contains( PERPENDICULAR_DIRECTION.DOWN ) && availableDirections
.contains( PERPENDICULAR_DIRECTION.DOWN ) ) {
choices[elem] = PERPENDICULAR_DIRECTION.DOWN;
elem++;
}
if ( actualDirection.contains( PERPENDICULAR_DIRECTION.UP ) && availableDirections
.contains( PERPENDICULAR_DIRECTION.UP ) ) {
choices[elem] = PERPENDICULAR_DIRECTION.UP;
elem++;
}
if ( actualDirection.contains( PERPENDICULAR_DIRECTION.LEFT ) && availableDirections
.contains( PERPENDICULAR_DIRECTION.LEFT ) ) {
choices[elem] = PERPENDICULAR_DIRECTION.LEFT;
elem++;
}
if ( actualDirection
.contains( PERPENDICULAR_DIRECTION.RIGHT ) && availableDirections
.contains( PERPENDICULAR_DIRECTION.RIGHT ) ) {
choices[elem] = PERPENDICULAR_DIRECTION.RIGHT;
elem++;
}
boolean changedDirection = false;
assert elem != 0;
if ( elem == 1 ) {
if ( this.playerDirection != choices[0] ) {
/* Direction change */
this.playerDirection = choices[0];
changedDirection = true;
}
} else {
/* The arrow that differs from the actual direction as an higher priority */
if ( choices[0] == this.playerDirection ) {
changedDirection = true;
this.playerDirection = choices[1];
} else if ( choices[1] == this.playerDirection ) {
changedDirection = true;
this.playerDirection = choices[0];
} else {
/*
User is pressing two valid arrow in the same time and was not
following none of these before
*/
this.playerDirection = null;
}
}
if ( this.playerDirection != null ) {
//Player moves
if ( changedDirection ) {
player.setDirection( this.playerDirection );
}
MapGridElement playerFooting = this.painter.getSecond();
assert playerFooting != null;
//Apply the coloring animations to the grid element
if ( playerPath == null ) {
//If the path wasn't created yet, create it
playerPath = new PathAnimationImpl();
playerPath.startNewPart( this.playerDirection );
playerPath.addPoint( playerPosition );
if ( player.modifiesColor() ) {
utilPushAnimation( entitiesColoringTrace,
playerFooting,
playerPath,
player.isColoring(),
playerPath.getNewPart() );
}
} else if ( changedDirection ) {
playerPath.startNewPart( this.playerDirection );
playerPath.addPoint( playerPosition );
if ( player.modifiesColor() ) {
utilPushAnimation( entitiesColoringTrace,
playerFooting,
playerPath,
player.isColoring(),
playerPath.getNewPart() );
}
}
switch ( this.playerDirection ) {
case LEFT:
playerPosition.x--;
break;
case RIGHT:
playerPosition.x++;
break;
case UP:
playerPosition.y--;
break;
case DOWN:
playerPosition.y++;
break;
}
player.setPosition( playerPosition );
playerPath.addPoint( playerPosition );
if ( ! playerFooting.contains( playerPosition ) ) {
//The player changed grid element, the animations must be applied to this element too
playerFooting = findElementRelativeTo( this.levelGraph, playerFooting,
this.playerDirection );
this.painter.setSecond( playerFooting );
if ( player.modifiesColor() ) {
utilPushAnimation( entitiesColoringTrace,
playerFooting,
playerPath,
player.isColoring(),
playerPath.getNewPart() );
}
}
if ( player.modifiesColor() ) {
//Colors this part of the grid
if ( playerFooting.isColoredAt( playerPosition ) != player.isColoring() ) {
if ( player.isColoring() ) {
this.addScore( COLORING_SCORE );
} else {
//Decoloration by the player is allowed, only for completeness
this.addScore( - COLORING_SCORE );
}
}
this.utilColorGridElementAndUpdateChanges( playerFooting,
playerPosition,
player.isColoring() );
}
for (Monster stillMonster : stillMonsters) {
//Moving monster will be checked for collision in the code below
if ( stillMonster.getPosition().equals( playerPosition ) ) {
deadMan = true;
}
}
} else {
remainingPlayerSpeed = 0;
}
}
final Point actualPlayerPosition = player.getPosition();
//Monsters are moved
Iterator<Map.Entry<Monster, Integer>> monstersIterator = roundRobin.entrySet()
.iterator();
while ( monstersIterator.hasNext() ) {
final Map.Entry<Monster, Integer> monsterWithSpeed = monstersIterator.next();
final int remainingMonsterSpeed = monsterWithSpeed.getValue();
final Monster mob = monsterWithSpeed.getKey();
if ( remainingMonsterSpeed > 0 ) {
monsterWithSpeed.setValue( remainingMonsterSpeed - 1 );
final Point monsterPosition = mob.getPosition();
if ( actualPlayerPosition.equals( monsterPosition ) ) {
deadMan = true;
}
final PathAnimation mobAnimation;
@Nullable
PERPENDICULAR_DIRECTION monsterDirection;
if ( this.firstStep ) {
monsterDirection = null;
} else {
monsterDirection = mob.getActualDirection();
}
MapGridElement monsterFooting = this.monstersPosition.get( mob );
assert monsterFooting != null;
if ( monsterFooting instanceof Junction || monsterDirection == null ) {
//Monster is in a junction, must change direction
monsterDirection = mob.getNewDirection( monsterDirection, false );
}
if ( mob instanceof AnimatedElement ) {
//This is an animated monster, an animations must be applied
PathAnimation animation = this.monstersAndAnimations.get( mob );
if ( animation == null ) {
//If the animations wasn't created yet, create it
animation = new PathAnimationImpl();
animation.startNewPart( monsterDirection );
animation.addPoint( monsterPosition );
this.monstersAndAnimations.put( ( (AnimatedElement) mob ), animation );
if ( mob.modifiesColor() ) {
utilPushAnimation( entitiesColoringTrace,
monsterFooting,
animation,
mob.isColoring(),
animation.getNewPart() );
}
} else if ( monsterDirection != animation.getNewPartDirection() ) {
animation.startNewPart( monsterDirection );
animation.addPoint( monsterPosition );
if ( mob.modifiesColor() ) {
utilPushAnimation( entitiesColoringTrace,
monsterFooting,
animation,
mob.isColoring(),
animation.getNewPart() );
}
}
mobAnimation = animation;
} else {
mobAnimation = null;
}
switch ( monsterDirection ) {
case LEFT:
monsterPosition.x--;
break;
case RIGHT:
monsterPosition.x++;
break;
case UP:
monsterPosition.y--;
break;
case DOWN:
monsterPosition.y++;
break;
}
mob.setPosition( monsterPosition );
if ( mobAnimation != null ) {
mobAnimation.addPoint( monsterPosition );
}
if ( ! monsterFooting.contains( monsterPosition ) ) {
//The monster changed grid element, the animations must be applied to this element too
monsterFooting = findElementRelativeTo( this.levelGraph,
monsterFooting,
monsterDirection );
this.monstersPosition.put( mob, monsterFooting );
if ( mob.modifiesColor() && mobAnimation != null ) {
utilPushAnimation( entitiesColoringTrace,
monsterFooting,
mobAnimation,
mob.isColoring(),
mobAnimation.getNewPart() );
}
}
if ( mob.modifiesColor() ) {
//(De)Colors this part of the grid
final boolean isMobColoring = mob.isColoring();
final boolean wasColoredHere = monsterFooting.isColoredAt( monsterPosition );
if ( wasColoredHere != isMobColoring ) {
if ( wasColoredHere ) {
this.addScore( - COLORING_SCORE );
} else {
this.addScore( COLORING_SCORE );
}
}
this.utilColorGridElementAndUpdateChanges( monsterFooting, monsterPosition,
isMobColoring );
}
if ( actualPlayerPosition.equals( monsterPosition ) ) {
deadMan = true;
}
} else {
//Monster remaining speed is 0, remove it from the moving monsters
stillMonsters.add( mob );
monstersIterator.remove();
}
}
}
//Computes the end time of all the animations
final long scheduledAnimationEndTime = millisCallTime + GameSettings.getInstance()
.getValue( GameSettings.INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT );
if ( playerPath != null && player instanceof AnimatedElement ) {
//Applies the previously computed path animations to the Player entity as an entity animations
playerPath.startAnimation( millisCallTime, scheduledAnimationEndTime );
( (AnimatedElement) player )
.applyAnimation( new PathBasedMovementAnimation( playerPath ) );
}
for (Map.Entry<AnimatedElement, PathAnimation> animatedEntry : this.monstersAndAnimations
.entrySet()) {
//Applies the previously computed path animations to the Monster entity as an entity animations
final PathAnimation entryAnimation = animatedEntry.getValue();
if ( entryAnimation != null ) {
entryAnimation.startAnimation( millisCallTime, scheduledAnimationEndTime );
animatedEntry.getKey()
.applyAnimation( new PathBasedMovementAnimation( entryAnimation ) );
}
}
for (Map.Entry<MapGridElement, Map<PathAnimation, Pair<Boolean, Collection<Segment>>>> mapGridElementEntry : entitiesColoringTrace
.entrySet()) {
//The key of the outer map keeps the grid element the animations refer to
final MapGridElement mapElement = mapGridElementEntry.getKey();
if ( mapElement instanceof Pipe ) {
final Collection<PipeColoringAnimation> resultElementAnimations = new LinkedList<>();
for (Map.Entry<PathAnimation, Pair<Boolean, Collection<Segment>>> pathAnimationEntry : mapGridElementEntry
.getValue().entrySet()) {
/*
The inner map keeps the mapping path animations: <coloring/decoloring flag; list of segments>.
The list of segments usually keeps only one segment because entities speeds are in the range 0-3 so
the same entity can't enter and, exit and then enter another time the same grid element is a single step
*/
final PathAnimation animation = pathAnimationEntry.getKey();
final boolean isColoring = pathAnimationEntry.getValue().getFirst();
for (Segment segment : pathAnimationEntry.getValue().getSecond()) {
resultElementAnimations.add( new PathBasedPipeColoringAnimation( animation,
segment,
( (Pipe) mapElement )
.getSegment(),
isColoring ) );
}
}
( (Pipe) mapElement ).applyAnimations( resultElementAnimations );
} else /*if ( mapElement instanceof Junction )*/ {
final Collection<JunctionColoringAnimation> resultElementAnimations = new LinkedList<>();
for (Map.Entry<PathAnimation, Pair<Boolean, Collection<Segment>>> pathAnimationEntry : mapGridElementEntry
.getValue().entrySet()) {
final PathAnimation animation = pathAnimationEntry.getKey();
final boolean isColoring = pathAnimationEntry.getValue().getFirst();
//Segment list is not needed when creating the JunctionColoringAnimation
resultElementAnimations.add( new PathBasedJunctionColoringAnimation( animation,
( (Junction) mapElement )
.getPosition(),
isColoring ) );
}
( (Junction) mapElement ).applyAnimations( resultElementAnimations );
}
}
if ( ! this.playerHitted ) {
if ( deadMan ) {
this.hitPlayer();
} else if ( this.checkWinCondition(this.modifiedMapElements) ) {
this.state = LEVEL_STATE.LEVEL_WIN;
}
}
if ( this.coloredFaces > previousColoredFacesNumber ) {
final int coloredFacesThisStep = this.coloredFaces - previousColoredFacesNumber;
this.addScore( FACE_COLORING_SCORE * ( 2 * coloredFacesThisStep - 1 ) );
}
for (Monster mob : levelMonsters) {
mob.startStep();
}
player.startStep();
this.firstStep = false;
}
@Override
public synchronized void restart() {
this.resetPlayerPositionAndDirection();
this.resetMonstersPositionAndState();
this.state = LEVEL_STATE.BEFORE_START;
this.firstStep = true;
}
@Override
public synchronized void reset() {
this.restart();
this.setLives( this.getInitialLives() );
this.resetPipeColor();
this.setScore( 0 );
}
@NotNull
@Override
public UnmodifiableUndirectedGraph<Junction, Pipe> getMap() {
return new UnmodifiableUndirectedGraph<>( this.levelGraph );
}
@NotNull
@Override
public Map<Set<Pipe>, Integer> getFacesMap() {
return Collections.unmodifiableMap( this.facesMap );
}
@Override
public synchronized boolean isFaceColored( final Integer face ) {
Boolean color = this.facesColor.get( face );
if ( color == null ) {
throw new IllegalArgumentException();
}
return color;
}
@Override
public synchronized boolean isElementModified( @NotNull final MapGridElement element )
throws NoSuchElementException {
return this.modifiedMapElements.contains(element);
}
@Override
public int getMonstersNumber() {
return this.getMonsters().size();
}
@Override
public synchronized long getStepTime() {
return this.actualStepTime;
}
@Override
public long getNextStepTime() {
return this.getNextStepTime( 1 );
}
@Override
public synchronized long getNextStepTime( final int steps ) {
return this.actualStepTime + ( GameSettings.getInstance()
.getValue( GameSettings.INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT ) * steps );
}
@NotNull
@Override
public Set<Monster> getMonsters() {
return Collections.unmodifiableSet( this.monstersPosition.keySet() );
}
@Override
public synchronized void hitPlayer() {
if ( ! this.playerHitted ) {
this.playerHitted = true;
this.setLives( this.getActualLives() - 1 );
this.state = LEVEL_STATE.LEVEL_LOST;
}
}
@NotNull
@Override
@SuppressWarnings ( "unchecked" )
public <T extends Monster> Set<T> getMonstersByType( @NotNull final Class<T> type ) {
Set<T> typeMonsters = new HashSet<>( 0 );
for (Monster mob : this.getMonsters()) {
if ( type.isAssignableFrom( mob.getClass() ) ) {
typeMonsters.add( (T) mob );
}
}
return typeMonsters;
}
@NotNull
@Override
public synchronized Set<PERPENDICULAR_DIRECTION> getAvailableDirections(
@NotNull final Monster monster ) throws IllegalArgumentException {
MapGridElement exists = this.monstersPosition.get( monster );
if ( exists == null ) {
throw new IllegalArgumentException( "Monster not in this level" );
}
return exists.getAvailableDirections();
}
@Override
public synchronized long getScore() {
return this.score;
}
@Override
public synchronized void setScore( final long newScore ) {
this.score = newScore;
}
@Override
public synchronized void addScore( final long addedScore ) {
this.setScore( this.getScore() + addedScore );
}
private boolean checkWinCondition( @NotNull Set<MapGridElement> modifiedElements ) {
for (MapGridElement modElement : modifiedElements) {
if ( ! modElement.isColored() ) {
continue;
}
Collection<Pipe> connectedPipes = new LinkedList<>( );
if(modElement instanceof Junction) {
connectedPipes.addAll( this.levelGraph.edgesOf( (Junction) modElement ) );
} else if(modElement instanceof Pipe) {
connectedPipes.add( (Pipe) modElement );
} else {
throw new IllegalArgumentException( "Only pipes and junctions are supported" );
}
for (Pipe pipe : connectedPipes) {
final Pair<Set<Pipe>, Set<Pipe>> faces = this.segmentsPerimeter.get( pipe );
if ( faces == null ) {
continue;
}
final List<Set<Pipe>> nonColoredFaces = new ArrayList<>( 2 );
if ( faces.getFirst() != null ) {
nonColoredFaces.add( faces.getFirst() );
}
if ( faces.getSecond() != null ) {
nonColoredFaces.add( faces.getSecond() );
}
Collection<Set<Pipe>> coloredFaces = this.getColoredPerimeters( pipe, nonColoredFaces );
for (Set<Pipe> doneFace : coloredFaces) {
this.stripColoredFace( doneFace );
}
}
}
return this.segmentsPerimeter.isEmpty();
}
@NotNull
private Collection<Set<Pipe>> getColoredPerimeters( @NotNull Pipe pipe,
@NotNull
Collection<Set<Pipe>> perimeters ) {
final List<Set<Pipe>> result = new LinkedList<>();
if ( ! pipe.isColored() ) {
return result;
}
for (Set<Pipe> actualFace : perimeters) {
boolean coloredFace = true;
for (Pipe perimeterPipe : actualFace) {
if ( ! perimeterPipe.isColored() ) {
coloredFace = false;
break;
}
Junction vertex = this.levelGraph.getEdgeSource( perimeterPipe );
if ( ! vertex.isColored() ) {
coloredFace = false;
break;
}
vertex = this.levelGraph.getEdgeTarget( perimeterPipe );
if ( ! vertex.isColored() ) {
coloredFace = false;
break;
}
}
if ( coloredFace ) {
result.add( actualFace );
}
}
return result;
}
private void stripColoredFace( @NotNull Set<Pipe> face ) {
final Integer faceID = this.facesMap.get( face );
if ( faceID == null ) {
throw new IllegalArgumentException();
}
boolean wasColored = this.facesColor.put( faceID, true );
if ( ! wasColored ) {
this.coloredFaces++;
}
for (Pipe pipe : face) {
final Pair<Set<Pipe>, Set<Pipe>> otherFaces = this.segmentsPerimeter.get( pipe );
assert otherFaces != null;
Set<Pipe> firstFace = otherFaces.getFirst();
Set<Pipe> secondFace = otherFaces.getSecond();
if ( firstFace != null && firstFace.equals( face ) ) {
otherFaces.setFirst( null );
firstFace = null;
}
if ( secondFace != null && secondFace.equals( face ) ) {
otherFaces.setSecond( null );
secondFace = null;
}
if ( firstFace == null && secondFace == null ) {
this.segmentsPerimeter.remove( pipe );
}
}
}
private void resetPlayerPositionAndDirection() {
final Player player = this.getPlayer();
player.setPosition( this.initialPainterPosition );
player.setDirection( INITIAL_DIRECTION );
this.painter.setSecond( this.initialPainterFooting );
}
private void resetMonstersPositionAndState() {
for (Monster mob : this.getMonsters()) {
mob.resetState();
}
for (Map.Entry<Monster, Point> monsterPointEntry : this.initialMonstersPosition
.entrySet()) {
monsterPointEntry.getKey().setPosition( monsterPointEntry.getValue() );
}
for (Map.Entry<Monster, MapGridElement> monsterMapGridElementEntry : this.initialMonstersFooting
.entrySet()) {
this.monstersPosition.put( monsterMapGridElementEntry.getKey(),
monsterMapGridElementEntry.getValue() );
}
}
private void resetPipeColor() {
for (Pipe pipe : this.getMap().edgeSet()) {
pipe.reset();
}
this.coloredFaces = 0;
}
private void resetModifiedElements() {
this.playerHitted = false;
this.modifiedMapElements.clear();
}
private void utilColorGridElementAndUpdateChanges( @NotNull MapGridElement element,
@NotNull Point where,
boolean coloring ) {
element.setColored( where, coloring );
this.setIsElementModified( element, true );
}
private void setIsElementModified( @NotNull final MapGridElement element,
final boolean modified ) throws NoSuchElementException {
if ( modified ) {
this.modifiedMapElements.add( element );
} else {
this.modifiedMapElements.remove( element );
}
}
private static class Pair<T, V> {
@Nullable
private T first;
@Nullable
private V second;
public Pair( @Nullable final T first, @Nullable final V second ) {
this.first = first;
this.second = second;
}
@Nullable
public T getFirst() {
return this.first;
}
public void setFirst( @Nullable final T first ) {
this.first = first;
}
@Nullable
public V getSecond() {
return this.second;
}
public void setSecond( @Nullable final V second ) {
this.second = second;
}
@Override
@NonNls
public String toString() {
return "Pair{" +
"first=" + this.first +
", second=" + this.second +
'}';
}
}
@Override
@NonNls
public synchronized String toString() {
return "SimpleLevelImpl{" +
"playerDirection=" + this.playerDirection +
", monstersPosition=" + this.monstersPosition +
", actualLives=" + this.actualLives +
", segmentsPerimeter=" + this.segmentsPerimeter +
", facesMap=" + this.facesMap +
", facesColor=" + this.facesColor +
", modifiedMapElements=" + this.modifiedMapElements +
", painter=" + this.painter +
", playerArrows=" + this.playerArrows +
", playerDirectionNew=" + this.playerDirectionNew +
", startLives=" + this.startLives +
", initialPainterPosition=" + this.initialPainterPosition +
", initialPainterFooting=" + this.initialPainterFooting +
", initialMonstersPosition=" + this.initialMonstersPosition +
", initialMonstersFooting=" + this.initialMonstersFooting +
", levelGraph=" + this.levelGraph +
", monstersAndAnimations=" + this.monstersAndAnimations +
", score=" + this.score +
", playerHitted=" + this.playerHitted +
", state=" + this.state +
", actualStepTime=" + this.actualStepTime +
", firstStep=" + this.firstStep +
'}';
}
}