EnemyFleet.groovy

package net.ebdon.trk21;

import static net.ebdon.trk21.GameSpace.*;
/**
 * @file
 * @author      Terry Ebdon
 * @date        January 2019
 * @copyright
 * 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.
 */
@groovy.util.logging.Log4j2
final class EnemyFleet {
    int numKlingonBatCrTotal  = 0; ///< k0% in TREK.BAS
    int numKlingonBatCrRemain = 0; ///< k9% in TREK.BAS
    int numKlingonBatCrInQuad = 0; ///< K3% in TREK.BAS

    final int maxKlingonBCinQuad = 9;
    final int maxPossibleKlingonShips = 64 * maxKlingonBCinQuad;

    /**
    Energy level assigned to each Klingon ship. This is reset every time
    the Enterprise enters a quadrant that contains Klingons. This is `S9%`
    in TREK.BAS.
    **/
    static final int maxKlingonEnergy = 200;

    def klingons    = new int[maxKlingonBCinQuad + 1][4]; ///< k%[] in TREK.BAS
    def scrapHeap   = []
    final softProbs = [
      0,
      0.0001,
      0.01,
      0.03,
      0.08,
      0.28,
      1.28,
      3.28,
      6.28,
      13.28 ]; ///< r[0..9] in TREK.BAS

    boolean isValid() {
      numKlingonBatCrTotal >= 0 &&
      numKlingonBatCrRemain >= 0 &&
      numKlingonBatCrInQuad >= 0 &&
      numKlingonBatCrInQuad <= numKlingonBatCrRemain &&
      numKlingonBatCrRemain <= numKlingonBatCrTotal &&
      numKlingonBatCrTotal <= maxPossibleKlingonShips &&
      scrapHeap.size() <= maxKlingonBCinQuad
    }

    boolean getDefeated() {
      assert isValid()
      numKlingonBatCrRemain == 0
    }

    void setNumKlingonBatCrRemain( final int newNumKbcRemain ) {
      assert newNumKbcRemain >= 0 && newNumKbcRemain <= numKlingonBatCrTotal
      numKlingonBatCrRemain = newNumKbcRemain
    }

    void setNumKlingonBatCrTotal( newNumKbcTotal ) {
      assert newNumKbcTotal >= 0 && newNumKbcTotal <= maxPossibleKlingonShips
      numKlingonBatCrTotal = newNumKbcTotal
    }

    void setNumKlingonBatCrInQuad( final int newNumKbcIq ) {
      assert newNumKbcIq >= 0 && newNumKbcIq <=9
      assert newNumKbcIq <= numKlingonBatCrRemain

      log.trace( "numKlingonBatCrInQuad changed from $numKlingonBatCrInQuad to $newNumKbcIq" )
      numKlingonBatCrInQuad = newNumKbcIq

      if ( numKlingonBatCrInQuad == 0 ) {
          resetQuadrant()
      }
    }

    void resetQuadrant() { ///> Forget all Klingon ship data for the quadrant.
      log.trace "Removing all battle cruisers & scrap from quadrant."
      1.upto( maxKlingonBCinQuad ) { shipNum ->
        if ( shipExists( shipNum ) ) {
          launchIntoStar shipNum
        }
      }
      scrapHeap.clear()
    }

    boolean isShipAt( key ) {
      klingons.find {
        key.first() == it[1] && key.last() == it[2]
      }
    }

    void positionInSector( final klingonShipNo, final klingonPosition ) {
      assert klingonShipNo >= 0 && klingonShipNo <= numKlingonBatCrInQuad
      assert sectorIsInsideQuadrant( klingonPosition )

      /// @pre Target sector must be empty
      assert null == klingons.find {
        it[1] == klingonPosition[0] && it[2] == klingonPosition[1]
      }

      log.debug "Klingon $klingonShipNo is at sector " +
        GameSpace.logFmtCoords( *klingonPosition )
      /// @todo replace array with new EnemyShip class.
      klingons[klingonShipNo][0] = klingonShipNo
      klingons[klingonShipNo][1] = klingonPosition[0]
      klingons[klingonShipNo][2] = klingonPosition[1]
      klingons[klingonShipNo][3] = maxKlingonEnergy
    }

    String toString() {
      "Enemy Bat C total, $numKlingonBatCrTotal, " +
      "remain: $numKlingonBatCrRemain, " +
      "in quad: $numKlingonBatCrInQuad\n" +
      "Bat Cru: ${klingons[1..maxKlingonBCinQuad]}"
    }

    boolean canAttack() {
      numKlingonBatCrInQuad > 0
    }

    /// Distance to target calculated via Pythagorous
    float distanceToTarget( final int shipNo, final Coords2d targetSectorCoords ) {
      final float distance = distanceBetween(
        klingons[ shipNo ][1..2], targetSectorCoords
      )
      log.info(
        sprintf(
          'Ship %d in %d - %d is %1.3f sectors from target %d - %d',
          shipNo, *(klingons[shipNo][2..1]),distance,
          targetSectorCoords.col, targetSectorCoords.row
        )
      )
      distance
    }

    /// @todo Move energyHittingTarget() into a new Galaxy or GamePhysics class?
    static float energyHittingTarget(
        final float energyReleased,
        final float distanceToTarget ) {

      assert energyReleased > 0 && energyReleased <= maxKlingonEnergy
      assert distanceToTarget > 0 &&
             distanceToTarget <= maxSectorDistance

      /// @todo: Same bug as was in PhaserControl - it's possible to
      /// hit the target with more energy than was fired at it.
      def rnd = new Random().nextFloat()
      ( ( energyReleased / distanceToTarget ) * ( 2 + rnd ) ) + 1
    }

    void attack( final Coords2d targetSectorCoords, reportAttack ) {
      assert sectorIsInsideQuadrant( targetSectorCoords)
      assert canAttack()
      assert reportAttack != null
      assert klingons.count { it[1] && it[2] } == numKlingonBatCrInQuad

      log.info "Fleet is beginning an attack with $numKlingonBatCrInQuad ships." // 1740 IF K3%>0 THEN GOSUB 2370
      log.info "Target is in sector $targetSectorCoords"
      1.upto( maxKlingonBCinQuad ) { int shipNo ->
        final int attackerEnergy = klingons[ shipNo ][3]
        if ( attackerEnergy > 0 ) {
          log.info "Ship $shipNo is attacking. It's energy level is: $attackerEnergy"

          final float distance = distanceToTarget( shipNo, targetSectorCoords )
          final int hitWithEnergy = energyHittingTarget( attackerEnergy, distance )

          reportAttack(
            hitWithEnergy,
            "Hit from Klingon at sector " +
              GameSpace.logFmtCoords( *(klingons[shipNo][1..2]) ) // Line 2410
          )
        } else {
          log.trace "Ship $shipNo is dead or never existed."
        }
      }
    }

    def hitOnShip( final int shipNum, final int hitAmount ) {
      log.info 'Fleet ship {} hit by {} units of Federation phasers',
        shipNum, hitAmount

      assert energy( shipNum ) > 0
      final int nrg = energy( shipNum ) - hitAmount
      klingons[ shipNum ][3] = [0,nrg].max()
      if (!shipExists(shipNum)) {
        scrapShip(shipNum)
      }
    }

    void scrapShip( final shipNum ) {
      scrapHeap << shipNum
      log.info "Ship $shipNum destroyed."
      log.info "There are ${scrapHeap.size()} scrapped ships in this quadrant."
      assert scrapHeap.size() <= maxKlingonBCinQuad
    }

    void regroup() {
      log.info "${scrapHeap.size()} dead ships will be launched into a star."
      while ( scrapHeap.size() ) {
        removeShip scrapHeap.pop()
      }
    }

    private void launchIntoStar(final shipNum) {
      log.debug "Launching ship $shipNum into a star."
      // 1.upto(3) {
        klingons[shipNum] = [shipNum,0,0,0]
      // }
    }
    private void removeShip(final shipNum) {
      launchIntoStar shipNum
      --numKlingonBatCrInQuad
      --numKlingonBatCrRemain
    }

    int energy( final int shipNum ) {
      assert shipNum >=1 && shipNum <= 9
      klingons[ shipNum ][3]
    }

    boolean shipExists(final int shipNum) {
      klingons[ shipNum ][3] > 0
    }
}