Package org.gbcpainter.game.levels

Source Code of org.gbcpainter.game.levels.SimpleLevelImpl

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.setColoredthis.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.setDirectionthis.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.startNewPartthis.playerDirection );
            playerPath.addPoint( playerPosition );

            if ( player.modifiesColor() ) {
              utilPushAnimation( entitiesColoringTrace,
                                 playerFooting,
                                 playerPath,
                                 player.isColoring(),
                                 playerPath.getNewPart() );
            }
          } else if ( changedDirection ) {
            playerPath.startNewPartthis.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 +
           '}';
  }
}
TOP

Related Classes of org.gbcpainter.game.levels.SimpleLevelImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.