FederationShip.groovy

package net.ebdon.trk21;

import groovy.transform.TypeChecked;

import static GameSpace.*;
import static Quadrant.*;
/**
 * @file
 * @author      Terry Ebdon
 * @date        January 2019
 * @copyright   Terry Ebdon, 2019
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/// @todo consider using an enum class for #allowedConditions
@groovy.util.logging.Log4j2
final class FederationShip {
    final int energyAtStart         = 3000;               ///< E0% in TREK.BAS
    final int lowEnergyThreshold    = energyAtStart / 10; ///< Triggers condition yellow
    int energyNow                   = energyAtStart;      ///< E% in TREK.BAS
    String condition                = "GREEN";            ///< C$ in TREK.BAS
    final int maxTorpedoes          = 10;
    int numTorpedoes                = maxTorpedoes;
    int energyUsedByLastMove        = 0 // N%
    Position position               = new Position();

    final allowedConditions = [
      'GREEN',
      'YELLOW',
      'RED',
      'DOCKED'
    ];

    private void useEnergyForMove( final int energyUsedByLastMove ) {
      energyNow -= energyUsedByLastMove
      log.info "Ship's movement used $energyUsedByLastMove units of energy."
      logFuelReduction()
    }

    private void energyReducedByEnemyAttack( final int damagelevel ) {
      energyNow -= damagelevel
      log.info "Hit with $damagelevel units of energy."
      logFuelReduction()
    }

    void energyReducedByPhaserUse( final int phaserEnergyUsed ) {
      energyNow -= phaserEnergyUsed
      log.info "Firing phasers used $phaserEnergyUsed units of energy."
      logFuelReduction()
    }

    private void logFuelReduction() {
      log.info "Energy reserves reduced to $energyNow"
      if ( deadInSpace() ) {
        log.info "The ship is dead in space."
      }
    }

    boolean isValid() {
      energyValid && armouryValid && conditionValid && position.valid
    }

    private boolean isEnergyValid() {
      energyNow >= 0 && energyNow <= energyAtStart
    }

    private boolean isArmouryValid() {
      numTorpedoes >= 0 && numTorpedoes <= maxTorpedoes
    }

    private boolean isConditionValid() {
      allowedConditions.contains( condition )
    }

    String toString() {
        "energy: $energyNow, condition: $condition, " +
        "torpedoes: $numTorpedoes, " +
        "energyUsedByLastMove: $energyUsedByLastMove, " +
        "energyAtStart: $energyAtStart, $position"
    }

    /// There's a design flaw here. The ship doesn't have enough information to
    /// move, and it has no access to the current sector or the galaxy.
    ///
    /// It doesn't know:
    /// - It's position in the current quadrant.
    /// - The position of the current quadrant in the galaxy.
    /// - The position of objects it might collide with.
    ///
    /// @note Should this be handled in the FederationShip class?
    ///
    /// @arg The desired course as a ShipVector.
    ///
    void move( final ShipVector sv ) {
      assert( sv.valid ) /// @pre The provided Shipvector is valid.
      log.info( "Ship moving: $sv" )

      energyUsedByLastMove = sv.warpFactor * 8
          // 1 warp factor:
          //  - uses 1 unit of energy
          //  - moves the ship by 1 quadrant.

      useEnergyForMove energyUsedByLastMove
    }

    boolean deadInSpace() {
      final boolean dis = energyNow <= 0
      if ( dis ) log.info "Ship is dead in space.\n$this"
      dis
    }

    void setCondition( final newCond ) {
        assert allowedConditions.contains( newCond )
        final def oldCond = this.condition
        this.condition = newCond
        log.trace( "Conditon changed from $oldCond to ${this.condition}" )
    }

  /// Attempt to dock if adjacent to a @ref StarBase.
  /// @todo attemptDocking() hard-codes 'DOCKED' condition
  /// @pre the current sector is inside the quadrant
  /// @note It's not possible to dock with a @ref StarBase
  ///       in an adjacent quadrant.
  def attemptDocking( final quadrant ) {
    final String logCheckForStarBase = 'Checking for star base in {}, value is {}'
    final String logNowDocked = 'Now docked in sector {} to star base in sector {}'
    final String logDockCheck = 'Checking if I can dock from sector {}'
    final String logAtEdge = 'Ship at board edge, sector {} is outside quadrant.'
    log.debug 'attemptDocking'

    position.sector.with {
      assert quadrant[ row, col ] != Thing.base  // Ship can't be in same sector as a star base.
      assert quadrant.contains( [row, col] )
      assert quadrant[ row, col ] != Thing.base  // Ship can't be in same sector as a star base.
      log.debug logDockCheck, logFmtCoords(row,col) /// @bug fixme!
      for ( int i = row - 1; i <= row + 1 && condition != "DOCKED"; i++) { // 1530
        for ( int j = col - 1; j <= col + 1 && condition != "DOCKED"; j++) { // 1530
          if ( quadrant.contains(i,j) ) {
            log.trace logCheckForStarBase, logFmtCoords(i,j), quadrant[i,j]
            if ( quadrant[i,j] == Thing.base ) {
              condition       = 'DOCKED'
              numTorpedoes    = maxTorpedoes
              energyNow       = energyAtStart
              log.info logNowDocked,
                logFmtCoords(row,col), logFmtCoords(i,j)
            }
          } else {
            log.debug logAtEdge, logFmtCoords(i,j)
          }
        }
      }
    }
  }

  private void battleStations( final numEnemyShipsHere ) {
    log.info "There are $numEnemyShipsHere enemy craft in quadrant " +
      position.quadrant
    log.info "Condition RED: there are $numEnemyShipsHere enemy craft here!"
    condition = "RED"
  }

  /// @todo Remove commented out code
  def shortRangeScan( final Galaxy galaxy ) {
    // logException {

      log.debug "shortRangeScan() called for quadrant " + position.quadrant
      final int minGalaxyDimension = 4                // Galaxy must be at least this "long".
      final int minGalaxyArea = minGalaxyDimension**2 // Galaxy is square.

      assert galaxy.size() >= minGalaxyArea
      assert galaxy.insideGalaxy( position.quadrant )

      final int numEnemyShipsHere = galaxy[ position.quadrant ] / 100
      if ( numEnemyShipsHere > 0 ) {
        battleStations( numEnemyShipsHere )
        // battleStations( numEnemyShipsHere, entQuadX, entQuadY )
      } else {
        setNonBattleCondition( galaxy )
      }
      // attemptDocking()
    // }
  }

  /// @todo Refactor - Extract method setNonBattleCondition()
  private void setNonBattleCondition( final galaxy ) {

    if ( energyNow > lowEnergyThreshold ) {
      log.debug "$energyNow is above threshold of $lowEnergyThreshold"
      condition = "GREEN"
      scanAdjacentQuadrants( galaxy )
    } else {
      log.debug "$energyNow is below threshold of $lowEnergyThreshold"
      condition = "YELLOW"
      log.info "Low on energy: $this"
    }
  }

  /// @todo Refactor - Extract method scanAdjacentQuadrants()
  /// @todo Remove commented out lines.
  private void scanAdjacentQuadrants( final galaxy ) {

    (position.quadrant.row - 1).upto( position.quadrant.row + 1) { i ->
      (position.quadrant.col - 1).upto( position.quadrant.col + 1) { j ->
        if ( insideGalaxy( i, j ) ) {
          final int quadrantStatus = galaxy[i,j]
          if ( quadrantStatus > 99 ) {
            condition = "YELLOW"
            log.info "adjacent quadrant [$i,$j] = $quadrantStatus has enemy ships!"
          } else {
            log.debug "adjacent quadrant [$i,$j] = $quadrantStatus is clear."
          }
        } else {
          log.debug "adjacent quadrant [$i,$j] is outside the galaxy."
        }
      }
    }
  }

  boolean insideGalaxy( x, y ) { /// @todo insideGalaxy() should be in a new Galaxy class.
    [1..8].flatten().containsAll( [x,y] ) /// @todo insideGalaxy() uses hardcoded galaxy size
  }

  /// @deprecated Use Quadrant.contains()
  private boolean inQuadrant(x,y) { /// @todo inQuadrant() should be in a new Galaxy class.
    insideGalaxy(x,y)
  }

  @TypeChecked
  boolean isProtectedByStarBase() {
    'DOCKED' == condition
  }

  void hitFromEnemy( damagelevel ) {
    assert !isProtectedByStarBase()
    assert 'RED' == condition
    energyReducedByEnemyAttack damagelevel
  }
}