private IEnumerator resetRound()
    {
        if (PerceptorEvaluator != null && PerceptorEvaluator.isActiveAndEnabled)
        {
            yield return(new WaitUntil(() => PerceptorEvaluator.READY));
        }
        // Reset all cards from the discard back into the deck
        foreach (CardController cc in DiscardPile.GetComponentsInChildren <CardController>())
        {
            cc.MoveTo(Deck.transform);
        }
        // Reset all players
        foreach (PlayerController p in Players)
        {
            p.Reset();
        }
        // Reset the discard pile counters (the deck is reset when Shuffle() is called during STAGE_INITIAL)
        DiscardPile.Reset();
        TurnHistory.Clear();
        // Display play statistics, if any
        if (PosterioriPerceptor.StatisticOfPlay != null)
        {
            AIUtil.DisplayMatrix("Statistics of play", PosterioriPerceptor.StatisticOfPlay);
        }
        // Wait untill all cards are back in the deck before reseting the overall game state
        yield return(new WaitUntil(() => (AllCardsDown)));

        CurrentStage = STAGE_INITIAL;
    }
    bool findClosestWalkablePointToDestination(HexGrid grid, Vector3 destination, Vector3 startLoc, List <PathNode> pathNodes, float destinationRadius, out Vector3 foundPoint, out PathNode foundPathNode)
    {
        bool foundAny = false;

        foundPoint    = Vector3.zero;
        foundPathNode = null;

        float closestDistanceToDestination = float.MaxValue;
        float closestDistanceToStart       = float.MaxValue; // used for tie-breaking between nodes of equal distance to destination

        for (int nodeIndex = 0; nodeIndex < pathNodes.Count; ++nodeIndex)
        {
            PathNode node       = pathNodes[nodeIndex];
            float    distToDest = AIUtil.Get2DDistanceBetweenVector3s(node.Position, destination);
            if (distToDest <= destinationRadius)
            {
                float distToStart = AIUtil.Get2DDistanceBetweenVector3s(node.Position, startLoc);
                if ((distToDest < closestDistanceToDestination) ||
                    ((distToDest == closestDistanceToDestination) &&
                     (distToStart < closestDistanceToStart)))
                {
                    foundAny      = true;
                    foundPathNode = node;
                    foundPoint    = node.Position;
                    closestDistanceToDestination = distToDest;
                    closestDistanceToStart       = distToStart;
                }
            }
        }

        return(foundAny);
    }
Example #3
0
        public static void Postfix(AIUtil __instance, ref bool __result, AbstractActor attacker, ICombatant target, Vector3 position, List <AbstractActor> allies)
        {
            //LowVisibility.Logger.Debug("AIUtil:UnitHasVisibilityToTargetFromPosition:post - entered.");
            bool alliesHaveVis = false;

            for (int i = 0; i < allies.Count; i++)
            {
                //if (allies[i].VisibilityCache.VisibilityToTarget(target).VisibilityLevel == VisibilityLevel.LOSFull) {
                if (allies[i].VisibilityCache.VisibilityToTarget(target).VisibilityLevel == VisibilityLevel.Blip0Minimum)
                {
                    alliesHaveVis = true;
                }
            }
            if (alliesHaveVis)
            {
                VisibilityLevel visibilityToTargetWithPositionsAndRotations =
                    attacker.Combat.LOS.GetVisibilityToTargetWithPositionsAndRotations(attacker, position, target);
                //__result = visibilityToTargetWithPositionsAndRotations >= VisibilityLevel.LOSFull;
                __result = visibilityToTargetWithPositionsAndRotations >= VisibilityLevel.Blip0Minimum;
            }
            else
            {
                __result = true;
            }
            //LowVisibility.Logger.Debug($"AIUtil:UnitHasVisibilityToTargetFromPosition:post - result is:{__result}");
        }
Example #4
0
        public float EvaluateUnit(AbstractActor unit)
        {
            var hostiles = AIUtil.HostilesToUnit(unit);

            if (hostiles.Count == 0)
            {
                return(float.MinValue);
            }

            var minDistance = float.MaxValue;

            foreach (var hostile in hostiles)
            {
                if (hostile.IsDead)
                {
                    continue;
                }

                var distance = Vector3.Distance(unit.CurrentPosition, hostile.CurrentPosition);
                minDistance = Mathf.Min(minDistance, distance);
            }

            // ReSharper disable once CompareOfFloatsByEqualityOperator
            if (minDistance == float.MaxValue)
            {
                return(float.MinValue);
            }

            return(minDistance);
        }
Example #5
0
    // On a knock-out, reduce the dimension of the joint distribution array and renormalize it
    public void KnockOutFilter(int SittingOrder, int DiscardedValue)
    {
        Debug.Assert(SittingOrder != MySittingOrder);
        Debug.Assert(DiscardedValue >= CardController.VALUE_GUARD && DiscardedValue <= CardController.VALUE_PRINCESS);
        int CardIndex       = DiscardedValue - 1;
        int HiddenHandIndex = GetHiddenHandIndex(SittingOrder);

        // Account for the discared card
        AccountForCard(DiscardedValue);
        // Merge down the current matrix to one dimension lower, taking the slice that corresponds to the knocked-out player's actual hand
        if (CurrentOpponentCount == 3)
        {
            TwoOpponentsHandsDistribution = ThreeOpponentsHandsDistribution.GetSlice(HiddenHandIndex, CardIndex);
            TwoOpponentsHandsDistribution.Renormalize();
            // Also, we move out the knocked-out player's sitting order to the last place of the HiddenHands array
            AIUtil.ShiftToLast(ref HiddenHands, HiddenHandIndex);
        }
        else if (CurrentOpponentCount == 2)
        {
            SingleOpponentHandDistribution = TwoOpponentsHandsDistribution.GetSlice(HiddenHandIndex, CardIndex);
            SingleOpponentHandDistribution.Renormalize();
            AIUtil.ShiftToLast(ref HiddenHands, HiddenHandIndex);
        }
        // Update player situation
        CurrentOpponentCount            -= 1;
        PlayerIsKnockedOut[SittingOrder] = true;
    }
Example #6
0
    protected override IEnumerator PickAMove()
    {
        // Wait until the perceptor has finished working
        yield return(new WaitUntil(() => MyPerceptor.READY));

        // Get available moves
        List <MoveData> availableMoves = myHand.GetLegalMoves(Game, this);

        availableMoves.AddRange(justDrawn.GetLegalMoves(Game, this));
        // Calculate utilities of every move
        MoveData.DualUtility[] moveUtility    = new MoveData.DualUtility[availableMoves.Count];
        MoveData.DualUtility   highestUtility = MoveData.DualUtility.MinValue;
        for (int i = 0; i < availableMoves.Count; i++)
        {
            moveUtility[i] = GetMoveUtility(availableMoves[i]);
            if (moveUtility[i].CompareTo(highestUtility) > 0)
            {
                highestUtility = moveUtility[i];
            }
        }
        // Calculate cutoff for random selection
        float cutoff = Mathf.Max(0, RELATIVE_CUTOFF_POINT * highestUtility.Utility);

        // Return a weighted random of the resulting moves
        myNextMove = availableMoves[AIUtil.GetWeightedRandom(moveUtility, highestUtility.Rank, cutoff)];
    }
 static void Postfix(AIUtil __instance, AbstractActor unit, AttackType attackType, List <Weapon> weaponList,
                     ICombatant target, Vector3 attackPosition, Vector3 targetPosition, bool useRevengeBonus,
                     AbstractActor unitForBVContext, float __result)
 {
     Mod.AILog.Info?.Write($"=== Expected damage of: {__result} for attacker {unit.DistinctId()} using attackType: " +
                           $"{attackType} versus target: {target.DistinctId()}");
 }
Example #8
0
        private float GetExpectedDamageForAllWeaponsVsTarget(AbstractActor attackingUnit, ICombatant targetUnit, bool targetIsEvasive)
        {
            if (!AIUtil.UnitHasVisibilityToTargetFromCurrentPosition(attackingUnit, targetUnit))
            {
                // Our team can't see this hostile.
                return(0.0f);
            }

            float damage = 0;

            for (int weaponIndex = 0; weaponIndex < attackingUnit.Weapons.Count; ++weaponIndex)
            {
                Weapon weapon = attackingUnit.Weapons[weaponIndex];
                if (weapon.CanFire && weapon.WillFireAtTarget(targetUnit))
                {
                    int   numShots = weapon.ShotsWhenFired;
                    float toHit    = weapon.GetToHitFromPosition(targetUnit, 1, attackingUnit.CurrentPosition, targetUnit.CurrentPosition, true, targetIsEvasive);                  // TODO (DAVE) : 1 = attacking a single target. Once AI can multi-target, this should reflect the number of targets

                    float damagePerShot     = weapon.DamagePerShotFromPosition(MeleeAttackType.NotSet, attackingUnit.CurrentPosition, targetUnit);
                    float heatDamagePerShot = 1 + (weapon.HeatDamagePerShot);                     // Factoring in heatDamagePerShot. +1, since most weapons deal 0 heat Dmg
                    damage += numShots * toHit * damagePerShot * heatDamagePerShot;
                }
            }

            return(damage);
        }
Example #9
0
        // Duplication of HBS code, avoiding prefix=true for now.
        public static void Postfix(ref BehaviorTreeResults __result, string ___name, BehaviorTree ___tree, AbstractActor ___unit)
        {
            Mod.Log.Info?.Write("CJMCN:T - entered");

            Mech mech = ___unit as Mech;

            if (mech != null && mech.WorkingJumpjets > 0)
            {
                string stayInsideRegionGUID = RegionUtil.GetStayInsideRegionGUID(___unit);

                float acceptableHeat = AIUtil.GetAcceptableHeatLevelForMech(mech);
                float currentHeat    = (float)mech.CurrentHeat;
                Mod.Log.Info?.Write($"CJMCN:T - === actor:{CombatantUtils.Label(mech)} has currentHeat:{currentHeat} and acceptableHeat:{acceptableHeat}");

                List <PathNode> sampledPathNodes = ___unit.JumpPathing.GetSampledPathNodes();
                Mod.Log.Info?.Write($"CJMCN:T - calculating {sampledPathNodes.Count} nodes");
                for (int i = 0; i < sampledPathNodes.Count; i++)
                {
                    Vector3 candidatePos      = sampledPathNodes[i].Position;
                    float   distanceBetween2D = AIUtil.Get2DDistanceBetweenVector3s(candidatePos, ___unit.CurrentPosition);
                    float   distanceBetween3D = Vector3.Distance(candidatePos, ___unit.CurrentPosition);
                    Mod.Log.Info?.Write($"CJMCN:T - calculated distances 2D:'{distanceBetween2D}' 3D:'{distanceBetween3D} ");
                    if (distanceBetween2D >= 1f)
                    {
                        float magnitude = (candidatePos - ___unit.CurrentPosition).magnitude;
                        float jumpHeat  = (float)mech.CalcJumpHeat(magnitude);
                        Mod.Log.Info?.Write($"CJMCN:T - calculated jumpHeat:'{jumpHeat}' from magnitude:'{magnitude}. ");

                        Mod.Log.Info?.Write($"CJMCN:T - comparing heat: [jumpHeat:'{jumpHeat}' + currentHeat:'{currentHeat}'] <= acceptableHeat:'{acceptableHeat}. ");
                        if (jumpHeat + (float)mech.CurrentHeat <= acceptableHeat)
                        {
                            if (stayInsideRegionGUID != null)
                            {
                                MapTerrainDataCell cellAt = ___unit.Combat.MapMetaData.GetCellAt(candidatePos);
                                if (cellAt != null)
                                {
                                    MapEncounterLayerDataCell mapEncounterLayerDataCell = cellAt.MapEncounterLayerDataCell;
                                    if (mapEncounterLayerDataCell != null &&
                                        mapEncounterLayerDataCell.regionGuidList != null &&
                                        !mapEncounterLayerDataCell.regionGuidList.Contains(stayInsideRegionGUID))
                                    {
                                        // Skip this loop iteration if
                                        Mod.Log.Info?.Write($"CJMCN:T - candidate outside of constraint region, ignoring.");
                                        goto CANDIDATE_OUTSIDE_REGION;
                                    }
                                }
                            }

                            Mod.Log.Info?.Write($"CJMCN:T - adding candidate position:{candidatePos}");
                            ___tree.movementCandidateLocations.Add(new MoveDestination(sampledPathNodes[i], MoveType.Jumping));
                        }
                    }

                    CANDIDATE_OUTSIDE_REGION :;
                }
            }

            // Should already be set by prefix method
            //__result = BehaviorTreeResults(BehaviorNodeState.Success);
        }
Example #10
0
    /// <summary>
    /// Computes a partial hand distribution of the specified player,
    /// for an assumed world state and the observation of which card they played.
    /// </summary>
    /// <param name="PlayedCardIndex">The index of the card that was played by the opponent.</param>
    /// <param name="HandIndex">The index of the card that the other player is assumed to have in their hand.</param>
    /// <param name="OutDistribution">A reference to a probability distribution that containts return values.</param>
    protected void UpdatePartialHandDistribution(int PlayedCardIndex, int PlayerHand, ref Distribution1D OutDistribution)
    {
        // Don't continue if priori probability is zero
        float PrioriProbability = SingleOpponentHandDistribution[PlayerHand];

        if (PrioriProbability <= 0)
        {
            return;
        }
        // Make a copy of the unaccounted card counters and update it with the "virtually" accounted-for cards
        Array.Copy(CountUnaccountedForCards, 1, virtualRemainingCards, 0, CARD_VECTOR_LENGTH);
        if (--virtualRemainingCards[PlayerHand] < 0)
        {
            return; // If a counter goes below zero, this is an impossible case, so return without an update
        }
        // Prepare computation
        int remainingDeckSize = AIUtil.SumUpArray(virtualRemainingCards);

        Debug.Assert(remainingDeckSize > 0);
        // Now loop through each deck card and see if the card that was played could have been played from hand or from deck
        for (int dc = 0; dc < CARD_VECTOR_LENGTH; dc++)
        {
            if (virtualRemainingCards[dc] > 0 && (PlayedCardIndex == PlayerHand || PlayedCardIndex == dc))
            {
                // Compute which card the player has left in their hand, given PlayedCardIndex
                int otherCard = (PlayedCardIndex == PlayerHand) ? dc : PlayerHand;
                // Calculate the joint probability of such play
                OutDistribution[otherCard] += PrioriProbability * virtualRemainingCards[dc] / remainingDeckSize *
                                              PosterioriPerceptor.LikelihoodOfPlay[PlayedCardIndex + 1, otherCard + 1];
            }
        }
    }
Example #11
0
        //public static List<AttackEvaluation> EvaluateAttacks(AbstractActor attacker, ICombatant target,
        //    List<List<CondensedWeapon>>[] weaponSetListByAttack, Vector3 attackPosition, Vector3 targetPosition,
        //    bool targetIsEvasive) {

        //    ConcurrentBag<AttackEvaluation> allResults = new ConcurrentBag<AttackEvaluation>();

        //    // List 0 is ranged weapons, 1 is melee+support, 2 is DFA+support
        //    for (int i = 0; i < 3; i++) {

        //        List<List<CondensedWeapon>> weaponSetsByAttackType = weaponSetListByAttack[i];
        //        string attackLabel = "ranged attack";
        //        if (i == 1) { attackLabel = "melee attacks"; }
        //        if (i == 2) { attackLabel = "DFA attacks"; }
        //        Mod.Log.Debug?.Write($"Evaluating {weaponSetsByAttackType.Count} {attackLabel}");

        //        if (weaponSetsByAttackType != null) {

        //            //ConcurrentQueue<List<CondensedWeapon>> workQueue = new ConcurrentQueue<List<CondensedWeapon>>();
        //            //for (int j = 0; j < weaponSetsByAttackType.Count; j++) {
        //            //    workQueue.Enqueue(weaponSetsByAttackType[j]);
        //            //}

        //            //void evaluateWeaponSet() {
        //            //    Mod.Log.Debug?.Write($" New action started.");
        //            //    SpinWait spin = new SpinWait();
        //            //    while (true) {

        //            //        if (workQueue.TryDequeue(out List<Weapon> weaponSet)) {
        //            //            AttackEvaluator.AttackEvaluation attackEvaluation = new AttackEvaluator.AttackEvaluation();
        //            //            attackEvaluation.WeaponList = weaponSet;
        //            //            attackEvaluation.AttackType = (AIUtil.AttackType)i;
        //            //            attackEvaluation.HeatGenerated = (float)AIUtil.HeatForAttack(weaponSet);

        //            //            if (unit is Mech mech) {
        //            //                attackEvaluation.HeatGenerated += (float)mech.TempHeat;
        //            //                attackEvaluation.HeatGenerated -= (float)mech.AdjustedHeatsinkCapacity;
        //            //            }

        //            //            attackEvaluation.ExpectedDamage = AIUtil.ExpectedDamageForAttack(unit, attackEvaluation.AttackType, weaponSet, target, attackPosition, targetPosition, true, unit);
        //            //            attackEvaluation.lowestHitChance = AIUtil.LowestHitChance(weaponSet, target, attackPosition, targetPosition, targetIsEvasive);
        //            //            allResults.Add(attackEvaluation);
        //            //            Mod.Log.Debug?.Write($"Processed a weaponSet, {workQueue.Count} remaining");
        //            //        } else {
        //            //            Mod.Log.Debug?.Write($"Failed to dequeue, {workQueue.Count} remaining");
        //            //            if (workQueue.Count == 0) { break; } else { spin.SpinOnce(); }
        //            //        }
        //            //    }
        //            //    Mod.Log.Debug?.Write($" New action ending.");
        //            //};
        //            //Parallel.Invoke(evaluateWeaponSet, evaluateWeaponSet, evaluateWeaponSet);

        //            for (int j = 0; j < weaponSetsByAttackType.Count; j++) {
        //                List<CondensedWeapon> weaponList = weaponSetsByAttackType[j];
        //                Mod.Log.Debug?.Write($"Evaluating {weaponList?.Count} weapons for a {attackLabel}");
        //                AttackEvaluator.AttackEvaluation attackEvaluation = new AttackEvaluator.AttackEvaluation();
        //                attackEvaluation.AttackType = (AIUtil.AttackType)i;
        //                attackEvaluation.HeatGenerated = (float)AIHelper.HeatForAttack(weaponList);

        //                if (attacker is Mech mech) {
        //                    attackEvaluation.HeatGenerated += (float)mech.TempHeat;
        //                    attackEvaluation.HeatGenerated -= (float)mech.AdjustedHeatsinkCapacity;
        //                }

        //                attackEvaluation.ExpectedDamage = AIHelper.ExpectedDamageForAttack(attacker, attackEvaluation.AttackType,
        //                    weaponList, target, attackPosition, targetPosition, true, attacker);
        //                attackEvaluation.lowestHitChance = AIHelper.LowestHitChance(weaponList, target, attackPosition, targetPosition, targetIsEvasive);

        //                // Expand the list to all weaponDefs, not our condensed ones
        //                Mod.Log.Debug?.Write($"Expanding weapon list for AttackEvaluation");
        //                List<Weapon> aeWeaponList = new List<Weapon>();
        //                foreach (CondensedWeapon cWeapon in weaponList) {
        //                    List<Weapon> cWeapons = cWeapon.condensedWeapons;
        //                    if (cWeapon.ammoAndMode != null) {
        //                        foreach (Weapon wep in cWeapons) {
        //                            Mod.Log.Debug?.Write($" -- Setting ammoMode to: {cWeapon.ammoAndMode.ammoId}_{cWeapon.ammoAndMode.modeId} for weapon: {wep.UIName}");
        //                            CleverGirlHelper.ApplyAmmoMode(wep, cWeapon.ammoAndMode);
        //                        }
        //                    }

        //                    aeWeaponList.AddRange(cWeapon.condensedWeapons);
        //                }
        //                Mod.Log.Debug?.Write($"List size {weaponList?.Count} was expanded to: {aeWeaponList?.Count}");
        //                attackEvaluation.WeaponList = aeWeaponList;
        //                allResults.Add(attackEvaluation);
        //            }
        //        }
        //    }

        //    List<AttackEvaluator.AttackEvaluation> sortedResults = new List<AttackEvaluator.AttackEvaluation>();
        //    sortedResults.AddRange(allResults);
        //    sortedResults.Sort((AttackEvaluator.AttackEvaluation a, AttackEvaluator.AttackEvaluation b) => a.ExpectedDamage.CompareTo(b.ExpectedDamage));
        //    sortedResults.Reverse();

        //    return sortedResults;
        //}


        public static bool MeleeDamageOutweighsRisk(Mech attacker, ICombatant target)
        {
            float attackerMeleeDam = AIUtil.ExpectedDamageForMeleeAttackUsingUnitsBVs(attacker, target, attacker.CurrentPosition, target.CurrentPosition, false, attacker);

            if (attackerMeleeDam <= 0f)
            {
                Mod.Log.Debug?.Write("Attacker has no expected damage, melee is too risky.");
                return(false);
            }

            Mech targetMech = target as Mech;

            if (targetMech == null)
            {
                Mod.Log.Debug?.Write("Target has no expected damage, melee is safe.");
                return(true);
            }

            // Use the target mech's position, because if we melee the attacker they can probably get to us
            float targetMeleeDam      = AIUtil.ExpectedDamageForMeleeAttackUsingUnitsBVs(targetMech, attacker, targetMech.CurrentPosition, targetMech.CurrentPosition, false, attacker);
            float meleeDamageRatio    = attackerMeleeDam / targetMeleeDam;
            float meleeDamageRatioCap = AIHelper.GetBehaviorVariableValue(attacker.BehaviorTree, BehaviorVariableName.Float_MeleeDamageRatioCap).FloatVal;

            Mod.Log.Debug?.Write($" meleeDamageRatio: {meleeDamageRatio} = target: {targetMeleeDam} / attacker: {attackerMeleeDam} vs. cap: {meleeDamageRatioCap}");

            return(meleeDamageRatio > meleeDamageRatioCap);
        }
        public void moveAll()
        {
            foreach (Player player in playerList)
            {
                player.Move();
            }

            if (projectiles.Count != 0)
            {
                foreach (Projectile proj in projectiles)
                {
                    if (proj.Active)
                    {
                        proj.Move();
                    }
                    else
                    {
                        removeProjList.Add(proj);
                    }
                }
            }

            if (enemyList.Count != 0)
            {
                foreach (Enemy enemy in enemyList)
                {
                    if (!enemy.TargetPlayer.Equals(""))
                    {
                        enemy.Rotation = AIUtil.getAnglePointingAt(enemy.Position, getPlayer(enemy.TargetPlayer).Position);
                        enemy.Accelerate();
                        enemy.Move();
                    }
                }
            }
        }
Example #13
0
 public static void Postfix(AIUtil __instance, ref bool __result, AbstractActor attacker, ICombatant target)
 {
     //LowVisibility.Logger.Debug("AIUtil:UnitHasVisibilityToTargetFromCurrentPosition:post - entered.");
     //__result = attacker.VisibilityToTargetUnit(target) == VisibilityLevel.LOSFull;
     __result = attacker.VisibilityToTargetUnit(target) >= VisibilityLevel.Blip0Minimum;
     //LowVisibility.Logger.Debug($"AIUtil:UnitHasVisibilityToTargetFromCurrentPosition:post - result is:{__result}");
 }
Example #14
0
 public override void FixedUpdate()
 {
     if (Patrol)
     {
         AIUtil.Patrol(true, this);
     }
     base.FixedUpdate();
 }
Example #15
0
        private static Vector3?GetEstimatedPath(AbstractActor actor, out List <Vector3> path)
        {
            PathNode closestNode     = null;
            var      closestNodeDist = float.MaxValue;
            var      destRadius      = GetMinWeaponRange(actor);

            path = null;

            var enemies = actor.Combat.GetAllEnemiesOf(actor);

            if (enemies.Count == 0)
            {
                Main.HBSLog?.LogWarning("FilterToAdjacentSlowestMoveNode: no enemies");
            }

            foreach (var enemy in enemies)
            {
                if (enemy.IsDead)
                {
                    continue;
                }

                var clippedEnemyPos = RegionUtil.MaybeClipMovementDestinationToStayInsideRegion(actor, enemy.CurrentPosition);

                var curPath = DynamicLongRangePathfinder.GetPathToDestination(clippedEnemyPos,
                                                                              actor.MovementCaps.MaxWalkDistance, actor, false, destRadius);
                if (curPath == null || curPath.Count == 0)
                {
                    Main.HBSLog?.LogWarning("FilterToAdjacentSlowestMoveNode: didn't get a path to unit");
                    continue;
                }

                var nodeOnCurPath = AIUtil.GetPrunedClosestValidPathNode(actor, actor.Pathing.CurrentGrid,
                                                                         actor.Combat, actor.MovementCaps.MaxWalkDistance, curPath);
                if (nodeOnCurPath == null)
                {
                    Main.HBSLog?.LogWarning("FilterToAdjacentSlowestMoveNode: didn't get a node on the current path");
                    continue;
                }

                var distanceFromEnemy = Vector3.Distance(nodeOnCurPath.Position, clippedEnemyPos);
                if (distanceFromEnemy >= closestNodeDist)
                {
                    continue;
                }

                path            = curPath;
                closestNode     = nodeOnCurPath;
                closestNodeDist = distanceFromEnemy;
            }

            return(closestNode?.Position);
        }
Example #16
0
        public static BTResult Swim(Animal agent, Vector2 direction, bool wandering, AnimalState state, bool surface, int tries = 10)
        {
            var targetPos = AIUtil.FindTargetSwimPosition(agent.Position, 5.0f, 20.0f, direction, 90, 360, tries, surface).XYZi;

            if (targetPos == agent.Position)
            {
                return(BTResult.Failure("target position is current position."));
            }

            // Avoid fish swimming too close to coast line
            // TODO: Avoid building routes near coast, cache available points far away from coast
            if (!agent.Species.CanSwimNearCoast)
            {
                var waterHeight = World.World.GetWaterHeight(targetPos.XZ);
                var isNearCoast = ((WorldPosition3i)targetPos).SpiralOutXZIter(3).Any(groundPos => World.World.GetBlock((WrappedWorldPosition3i)groundPos.X_Z(waterHeight)).Is <Solid>());
                if (isNearCoast)
                {
                    return(BTResult.Failure("target position is too close to coast line"));
                }
            }

            var targetBlock = World.World.GetBlock(targetPos);

            // If an animal can't float on water surface - move it a block below highest water block TODO: make them move on underwater ground
            // TODO: Remove after pathfinder improvements
            if (!agent.Species.FloatOnSurface && targetBlock is WaterBlock && World.World.GetBlock(targetPos + Vector3i.Up).Is <Empty>())
            {
                targetPos += Vector3i.Down;
            }
            //if (targetBlock.Is<Solid>()) targetPos += Vector3i.Up;
            if (targetBlock.Is <Empty>())
            {
                targetPos += Vector3i.Down;
                // Fail if target position is too shallow
                if (World.World.GetBlock(targetPos).Is <Solid>())
                {
                    return(BTResult.Failure("target position is too thin"));
                }
            }
            // Clamp current position to ground or water, if can't float on water surface - stay below water height TODO: make them move on underwater ground
            var pos = World.World.ClampToWaterHeight(agent.Position.XYZi);

            // TODO: Remove after pathfinder improvements
            if (!agent.Species.FloatOnSurface && pos.y == World.World.GetWaterHeight(agent.Position.XZi))
            {
                pos += Vector3i.Down;
            }

            var route     = Route.Basic(agent.Species.GetTraversalData(wandering), agent.FacingDir, pos + Vector3i.Down, targetPos); //For fish, we need to compensate, since route is built from positions of the ground below
            var routeTime = route.IsValid ? agent.SetRoute(route, state, null) : 0f;

            return(routeTime < float.Epsilon ? BTResult.Failure("route not set") : BTResult.Success($"swimming path"));
        }
    public float[] GetCardProbabilitiesInDeck()
    {
        float[] result = new float[CardController.VALUE_PRINCESS + 1];
        float   sum    = 0;

        for (int i = CardController.VALUE_GUARD; i <= CardController.VALUE_PRINCESS; i++)
        {
            result[i] = GetCardProbabilityInDeck(i);
            sum      += result[i];
        }
        Debug.AssertFormat(sum <= 0 || AIUtil.Approx(sum, 1f) || MyController.Game.Deck.CountCardsLeft < 1, "{0}: Deck card probabilities don't sum up to 1! ({1})", MyController, sum);
        return(result);
    }
Example #18
0
        static bool ValidateMultiAttackOrder(MultiTargetAttackOrderInfo order, AbstractActor unit)
        {
            AIUtil.LogAI("Multiattack validation", unit);
            for (int subAttackIndex = 0; subAttackIndex < order.SubTargetOrders.Count; ++subAttackIndex)
            {
                AttackOrderInfo subOrder = order.SubTargetOrders[subAttackIndex];

                AIUtil.LogAI(string.Format("SubAttack #{0}: target {1} {2}", subAttackIndex, subOrder.TargetUnit.GUID, subOrder.TargetUnit.DisplayName));

                foreach (Weapon w in subOrder.Weapons)
                {
                    AIUtil.LogAI(string.Format("  Weapon {0}", w.Name));
                }
            }

            List <string> targetGUIDs = new List <string>();

            foreach (AttackOrderInfo subOrder in order.SubTargetOrders)
            {
                string thisGUID = subOrder.TargetUnit.GUID;
                if (targetGUIDs.IndexOf(thisGUID) != -1)
                {
                    // found duplicated target GUIDs
                    AIUtil.LogAI("Multiattack error: Duplicated target GUIDs", unit);
                    return(false);
                }

                foreach (Weapon w in subOrder.Weapons)
                {
                    if (!w.CanFire)
                    {
                        AIUtil.LogAI("Multiattack error: weapon that cannot fire", unit);
                        return(false);
                    }

                    ICombatant target = subOrder.TargetUnit;

                    if (!unit.Combat.LOFCache.UnitHasLOFToTargetAtTargetPosition(
                            unit, target, w.MaxRange, unit.CurrentPosition, unit.CurrentRotation,
                            target.CurrentPosition, target.CurrentRotation, w.IndirectFireCapable))
                    {
                        AIUtil.LogAI("Multiattack error: weapon that cannot fire", unit);
                        return(false);
                    }
                }
            }

            AIUtil.LogAI("Multiattack validates OK", unit);
            return(true);
        }
 public static bool Prefix(BehaviorNode __instance, ref BehaviorTreeResults __result, ref BehaviorTree ___tree, ref AbstractActor ___unit)
 {
     try {
         List <AbstractActor> allAlliesOf = ___unit.Combat.GetAllAlliesOf(___unit);
         AuraBubble           sensors     = ___unit.sensorAura();
         for (int index1 = 0; index1 < ___tree.enemyUnits.Count; ++index1)
         {
             ICombatant    enemyUnit     = ___tree.enemyUnits[index1];
             AbstractActor abstractActor = enemyUnit as AbstractActor;
             float         magnitude     = (enemyUnit.CurrentPosition - ___unit.CurrentPosition).magnitude;
             if (AIUtil.UnitHasVisibilityToTargetFromPosition(___unit, enemyUnit, ___unit.CurrentPosition, allAlliesOf))
             {
                 if (___unit.CanEngageTarget(enemyUnit) || ___unit.CanDFATargetFromPosition(enemyUnit, ___unit.CurrentPosition))
                 {
                     __result = new BehaviorTreeResults(BehaviorNodeState.Success);
                     return(false);
                 }
                 if ((double)magnitude <= (double)___unit.MaxWalkDistance)
                 {
                     __result = new BehaviorTreeResults(BehaviorNodeState.Success);
                     return(false);
                 }
                 if (abstractActor != null && abstractActor.IsGhosted)
                 {
                     float num   = Mathf.Lerp(___unit.MaxWalkDistance, ___unit.MaxSprintDistance, ___unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_SignalInWeapRngWhenEnemyGhostedWithinMoveDistance).FloatVal);
                     float range = sensors.collider.radius;
                     if ((double)Vector3.Distance(___unit.CurrentPosition, abstractActor.CurrentPosition) - (double)range >= (double)num)
                     {
                         continue;
                     }
                 }
                 for (int index2 = 0; index2 < ___unit.Weapons.Count; ++index2)
                 {
                     Weapon weapon = ___unit.Weapons[index2];
                     if (weapon.CanFire && (double)weapon.MaxRange >= (double)magnitude)
                     {
                         __result = new BehaviorTreeResults(BehaviorNodeState.Success);
                         return(false);
                     }
                 }
             }
         }
         __result = new BehaviorTreeResults(BehaviorNodeState.Failure);
         return(false);
     } catch (Exception e) {
         Log.Debug?.Write(e.ToString() + "\n");
         __result = new BehaviorTreeResults(BehaviorNodeState.Failure);
         return(false);
     }
 }
    float findDistanceFromGuardLance(Vector3 position, Lance guardLance)
    {
        float bestDistance = float.MaxValue;

        for (int i = 0; i < guardLance.unitGuids.Count; ++i)
        {
            string        unitGUID       = guardLance.unitGuids[i];
            AbstractActor guardUnitActor = unit.Combat.ItemRegistry.GetItemByGUID <AbstractActor>(unitGUID);
            if ((guardUnitActor != null) && (!guardUnitActor.IsDead))
            {
                bestDistance = Mathf.Min(bestDistance, AIUtil.Get2DDistanceBetweenVector3s(guardUnitActor.CurrentPosition, position));
            }
        }
        return(bestDistance);
    }
    RouteGameLogic getRoute()
    {
        BehaviorVariableValue variableValue = tree.GetBehaviorVariableValue(BehaviorVariableName.String_RouteGUID);

        if (variableValue == null)
        {
            // no route
            AIUtil.LogAI("No behavior variable for route GUID found");
            return(null);
        }

        string routeGUID = variableValue.StringVal;

        return(RoutingUtil.FindRouteByGUID(tree, routeGUID));
    }
        private static List <WeaponAttackEval> FilterForHeatBudget(List <WeaponAttackEval> attacks, AttackDetails details)
        {
            if (!(details.Attacker is Mech))
            {
                Mod.Log.Debug?.Write("Attacker is not a mech, returning all weapons from heat filter.");
                return(attacks);
            }

            // Build a state for the atacker defining what level of heat/damage is acceptable.
            Mech  attackerMech = details.Attacker as Mech;
            float currentHeat  = attackerMech == null ? 0f : (float)attackerMech.CurrentHeat;

            // TODO: Improve this / link to CBTBE
            float acceptableHeat = attackerMech == null ? float.MaxValue : AIUtil.GetAcceptableHeatLevelForMech(attackerMech);
            float heatBudget     = acceptableHeat - currentHeat;

            Mod.Log.Debug?.Write($"Allowing up to {heatBudget} additional points of heat.");

            // TODO: Allow for BVars influence on this

            // Now start optimizing. First, sort the list by heatratio
            Mod.Log.Debug?.Write($"Sorting {attacks.Count} weapons by heatRatio");
            attacks.Sort(
                (wae1, wae2) => wae1.DirectDmgPerHeat.CompareTo(wae2.DirectDmgPerHeat)
                );
            Mod.Log.Debug?.Write($" ..Done.");

            // What's the highest damage solution we can get without overheating?
            List <WeaponAttackEval> filteredAttacks = new List <WeaponAttackEval>();

            foreach (WeaponAttackEval wae in attacks)
            {
                Mod.Log.Debug?.Write($"Evaluating weapon {wae?.Weapon?.UIName} with generated heat: {wae?.Weapon?.HeatGenerated} versus budget");
                if (wae.Weapon.HeatGenerated <= heatBudget)
                {
                    Mod.Log.Debug?.Write($"Adding weapon {wae.Weapon.UIName}");
                    filteredAttacks.Add(wae);
                    heatBudget -= wae.Weapon.HeatGenerated;
                }
                else
                {
                    Mod.Log.Debug?.Write($"Skipping weapon {wae.Weapon.UIName}");
                }
            }
            Mod.Log.Debug?.Write("Done budgeting weapons");

            return(filteredAttacks);
        }
Example #23
0
    // Precomputes the static arrays for quicker access
    protected static void PrecomputeStaticArrays()
    {
        // Initialized base deck distribution
        BaseDeckDistribution = new Distribution1D(CARD_VECTOR_LENGTH);
        for (int CardIndex = 0; CardIndex < CARD_VECTOR_LENGTH; CardIndex++)
        {
            BaseDeckDistribution[CardIndex] = ((float)GameController.CARD_COUNT[CardIndex + 1]) / GameController.TOTAL_CARD_COUNT;
        }
        // Initialize base joint hand distribution
        BaseThreeOpponentsHandsDistribution = new Distribution3D(CARD_VECTOR_LENGTH, CARD_VECTOR_LENGTH, CARD_VECTOR_LENGTH);
        // Optimization for the following calculation:
        int[] tempRemainingCards = new int[CARD_VECTOR_LENGTH]; // We can't use virtualRemainingCards in a static method...
        Array.Copy(GameController.CARD_COUNT, 1, tempRemainingCards, 0, CARD_VECTOR_LENGTH);
        float SumOfArray = 0, ScalingFactor = 1f / GameController.TOTAL_CARD_COUNT / (GameController.TOTAL_CARD_COUNT - 1) / (GameController.TOTAL_CARD_COUNT - 2);

        // Calculate base joint hand distribution
        for (int Hand1Index = 0; Hand1Index < CARD_VECTOR_LENGTH; Hand1Index++)
        {
            // Account for this value of the first hand
            float Hand1Prob = ScalingFactor * tempRemainingCards[Hand1Index];
            tempRemainingCards[Hand1Index] -= 1;
            // And loop through all possible second and third hidden hands
            for (int Hand2Index = 0; Hand2Index < CARD_VECTOR_LENGTH; Hand2Index++)
            {
                // Account for this value of the second hand
                float Hand2JointProb = Hand1Prob * tempRemainingCards[Hand2Index];
                tempRemainingCards[Hand2Index] -= 1;
                // And loop through all possible third hidden hands
                for (int Hand3Index = 0; Hand3Index < CARD_VECTOR_LENGTH; Hand3Index++)
                {
                    // If this is an impossible case, just jump to the next iteration
                    if (Hand2JointProb <= 0 || tempRemainingCards[Hand3Index] <= 0)
                    {
                        continue;
                    }
                    // Otherwise, calculate the joint probability of this case and store it
                    float jointProb = Hand2JointProb * tempRemainingCards[Hand3Index];
                    BaseThreeOpponentsHandsDistribution[Hand1Index, Hand2Index, Hand3Index] = jointProb;
                    SumOfArray += jointProb;
                }
                // Reset card counts for the next iteration
                tempRemainingCards[Hand2Index] += 1;
            }
            // Reset card counts for the next iteration
            tempRemainingCards[Hand1Index] += 1;
        }
        Debug.Assert(AIUtil.Approx(SumOfArray, 1f));
    }
 public float[] GetCardProbabilitiesInHand(PlayerController Player)
 {
     float[] result = new float[CardController.VALUE_PRINCESS + 1];
     // If no player is specified (when called for a non-targeted card), just return an empty array
     if (Player != null)
     {
         float sum = 0;
         for (int i = CardController.VALUE_GUARD; i <= CardController.VALUE_PRINCESS; i++)
         {
             result[i] = GetCardProbabilityInHand(Player, i);
             sum      += result[i];
         }
         Debug.AssertFormat(sum <= 0 || AIUtil.Approx(sum, 1f), "{0}: Hand card probabilities of {2} don't sum up to 1! ({1})", MyController, sum, Player);
     }
     return(result);
 }
Example #25
0
        // CLONE OF HBS CODE - LIKELY BRITTLE!
        public static bool ShouldUnitUseInspire(AbstractActor unit)
        {
            float  num    = AIUtil.CalcMaxInspirationDelta(unit, true);
            AITeam aiteam = unit.team as AITeam;

            if (aiteam == null || !unit.CanBeInspired)
            {
                return(false);
            }
            if (num < AIHelper.GetBehaviorVariableValue(unit.BehaviorTree, BehaviorVariableName.Float_MinimumInspirationDamage).FloatVal)
            {
                return(false);
            }
            float num2 = 1f - aiteam.GetInspirationWindow();

            return(num > aiteam.GetInspirationTargetDamage() * num2);
        }
 public static bool Prefix(AITeam __instance, List <AbstractActor> unusedUnits, ref AbstractActor __result)
 {
     try {
         if (!__instance.CanEntireEnemyTeamBeGhosted())
         {
             __result = null;
             return(false);
         }
         AbstractActor result      = (AbstractActor)null;
         float         minDistance = float.MaxValue;
         for (int index1 = 0; index1 < unusedUnits.Count; ++index1)
         {
             AbstractActor        unusedUnit = unusedUnits[index1];
             List <AbstractActor> enemies    = AIUtil.HostilesToUnit(unusedUnit);
             AuraBubble           sensors    = unusedUnit.sensorAura();
             if (sensors == null)
             {
                 continue;
             }
             ;
             for (int index2 = 0; index2 < enemies.Count; ++index2)
             {
                 AbstractActor enemy = enemies[index2];
                 if (enemy.HasECMAbilityInstalled)
                 {
                     float floatVal     = unusedUnit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_SignalInWeapRngWhenEnemyGhostedWithinMoveDistance).FloatVal;
                     float maxMoveDist  = Mathf.Lerp(unusedUnit.MaxWalkDistance, unusedUnit.MaxSprintDistance, floatVal);
                     float range        = sensors.collider.radius;
                     float needMoveDist = Vector3.Distance(unusedUnit.CurrentPosition, enemy.CurrentPosition) - range;
                     if ((double)needMoveDist <= (double)maxMoveDist && (double)needMoveDist < (double)minDistance)
                     {
                         result      = unusedUnit;
                         minDistance = needMoveDist;
                     }
                 }
             }
         }
         __result = result;
         return(false);
     }catch (Exception e) {
         Log.WriteCritical(e.ToString() + "\n");
         __result = null;
         return(false);
     }
 }
Example #27
0
        // If the animal is stuck somewhere it shouldn't be help it escape or die if its very far from walkable terrain
        public static BTResult LandAnimalUnStuckOrDie(Animal agent)
        {
            const int maxUnStuckDistance = 20;
            var       nearestValid       = RouteManager.NearestWalkableXYZ(agent.Position.XYZi, maxUnStuckDistance);

            if (nearestValid == agent.Position.WorldPosition3i)
            {
                agent.NextTick = WorldTime.Seconds + 10f;
                return(BTResult.Failure());
            }

            if (!nearestValid.IsValid)
            {
                // cheating failed? time to die!
                agent.Kill();
                return(BTResult.Success());
            }
            // ignore terrain, path directly to a valid area, but only if noone is around *shy* or if he's really trying
            if (agent.TryGetMemory(Animal.TriesToUnStuckMemory, out int tries))
            {
                tries += 1;
            }
            agent.SetMemory(Animal.TriesToUnStuckMemory, tries);
            if (!NetObjectManager.GetObjectsWithin(agent.Position.XZ, 20).OfType <Player>().Any() || tries >= 3)
            {
                var route = AIUtil.GetRoute(agent.FacingDir, agent.Position, (Vector3)nearestValid, agent.Species.GetTraversalData(true), null, null);
                if (!route.IsValid)
                {
                    agent.NextTick = WorldTime.Seconds + 10;
                    return(BTResult.Failure());
                }

                // Proceed with route with time length more than 0
                var timeToFinishRoute = agent.SetRoute(route, AnimalState.Wander);
                if (timeToFinishRoute > float.Epsilon)
                {
                    agent.RemoveMemory(Animal.TriesToUnStuckMemory);
                    return(BTResult.Success());
                }
            }

            agent.NextTick = WorldTime.Seconds + 10;
            return(BTResult.Failure());
        }
Example #28
0
 // Convenience function for easier deck distribution calculation
 // Note that because it is CUMULATIVE, DeckDistributionBuffer contains intermediary results and may not be cleared!
 protected void CumulativeUpdateDeckDistribution(float Probability, int[] RemainingCards, ref Distribution1D DeckDistributionBuffer)
 {
     Debug.Assert(RemainingCards.Length == CARD_VECTOR_LENGTH);
     Debug.Assert(DeckDistributionBuffer.Length == CARD_VECTOR_LENGTH);
     if (Probability > 0)
     {
         int sum = AIUtil.SumUpArray(RemainingCards);
         if (sum > 0)
         {
             for (int i = 0; i < CARD_VECTOR_LENGTH; i++)
             {
                 if (RemainingCards[i] > 0)
                 {
                     DeckDistributionBuffer[i] += Probability * RemainingCards[i] / sum;
                 }
             }
         }
     }
 }
Example #29
0
    /// <summary>
    /// Computes a partial hand distribution of the specified player,
    /// for an assumed world state and the observation of which card they played.
    /// </summary>
    /// <param name="TurnData">Complete data of the turn being analyzed.</param>
    /// <param name="Hand0">The index of the card the first of the three remaining opponents is asssumed to hold.</param>
    /// <param name="Hand1">The index of the card the second of the three remaining opponents is asssumed to hold.</param>
    /// <param name="Hand2">The index of the card the third of the three remaining opponents is asssumed to hold.</param>
    /// <param name="OutDistribution">A reference to a probability distribution that containts return values.</param>
    protected void UpdatePartialHandDistribution(MoveData TurnData, int Hand0, int Hand1, int Hand2, ref Distribution3D OutDistribution)
    {
        // Don't continue if priori probability is zero
        float PrioriProbability = ThreeOpponentsHandsDistribution[Hand0, Hand1, Hand2];

        if (PrioriProbability <= 0)
        {
            return;
        }
        // Make a copy of the unaccounted card counters and update it with the "virtually" accounted-for cards
        Array.Copy(CountUnaccountedForCards, 1, virtualRemainingCards, 0, CARD_VECTOR_LENGTH);
        if (--virtualRemainingCards[Hand0] < 0 || --virtualRemainingCards[Hand1] < 0 || --virtualRemainingCards[Hand2] < 0)
        {
            return; // If a counter goes below zero, this is an impossible case, so return without an update
        }
        // Parse the turn data
        int PlayerIndex     = GetHiddenHandIndex(TurnData.Player.SittingOrder),
            PlayedCardIndex = TurnData.Card.Value - 1,
            TargetIndex     = (TurnData.Target == null || TurnData.Target.SittingOrder == MySittingOrder) ? -1 : GetHiddenHandIndex(TurnData.Target.SittingOrder);

        // Determine which card in the current data belongs to PlayerIndex
        int[] idx        = new int[] { Hand0, Hand1, Hand2 };
        int   playerHand = idx[PlayerIndex],
              targetHand = TargetIndex != -1 ? idx[TargetIndex] : -1;
        // Prepare computation
        int remainingDeckSize = AIUtil.SumUpArray(virtualRemainingCards);

        Debug.Assert(remainingDeckSize > 0);
        // Now loop through each deck card and see if the card that was played could have been played from hand or from deck
        for (int dc = 0; dc < CARD_VECTOR_LENGTH; dc++)
        {
            if (virtualRemainingCards[dc] > 0 && (PlayedCardIndex == playerHand || PlayedCardIndex == dc))
            {
                // Compute which card the player has left in their hand, given PlayedCardIndex
                int otherCard = (PlayedCardIndex == playerHand) ? dc : playerHand;
                // Prepare the index values for the update
                idx[PlayerIndex] = otherCard;
                // Calculate the joint probability of such play and increment the output array
                OutDistribution[idx[0], idx[1], idx[2]] += PrioriProbability * virtualRemainingCards[dc] / remainingDeckSize
                                                           * GetLikelihoodOfPlay(TurnData, otherCard + 1, targetHand + 1);
            }
        }
    }
Example #30
0
    /// <summary>
    /// Computes a partial hand distribution of the specified player's new hand after being targeted by the Prince,
    /// for an assumed world state and the observation of which card they previously discarded.
    /// </summary>
    /// <param name="PlayerIndex">Index of the player for whom the computation is performed.</param>
    /// <param name="Hand0">The index of the card the first of the three remaining opponents is asssumed to hold.</param>
    /// <param name="Hand1">The index of the card the second of the three remaining opponents is asssumed to hold.</param>
    /// <param name="Hand2">The index of the card the third of the three remaining opponents is asssumed to hold.</param>
    /// <param name="DiscardedIndex">The observed index of the card that the player had discarded.</param>
    /// <param name="OutDistribution">A reference to a probability distribution that containts return values.</param>
    protected void PrinceDiscardAndDrawPartialUpdate(int PlayerIndex, int Hand0, int Hand1, int Hand2, int DiscardedIndex, ref Distribution3D OutDistribution)
    {
        // Don't continue if priori probability is zero
        float PrioriProbability = ThreeOpponentsHandsDistribution[Hand0, Hand1, Hand2];

        if (PrioriProbability <= 0)
        {
            return;
        }
        // Determine which card in the current data belongs to PlayerIndex
        int[] idx        = new int[] { Hand0, Hand1, Hand2 };
        int   playerHand = idx[PlayerIndex];

        if (playerHand != DiscardedIndex)
        {
            return;
        }
        // Make a copy of the unaccounted card counters and update it with the "virtually" accounted-for cards
        Array.Copy(CountUnaccountedForCards, 1, virtualRemainingCards, 0, CARD_VECTOR_LENGTH);
        if (--virtualRemainingCards[Hand0] < 0 || --virtualRemainingCards[Hand1] < 0 || --virtualRemainingCards[Hand2] < 0)
        {
            return; // If a counter goes below zero, this is an impossible case, so return without an update
        }
        // Prepare computation
        int remainingDeckSize = AIUtil.SumUpArray(virtualRemainingCards);

        if (remainingDeckSize < 1)
        {
            return; // The game is over, anyway.
        }
        Debug.Assert(remainingDeckSize > 0);
        // Now loop through each deck card and see if the card that was played could have been played from hand or from deck
        for (int dc = 0; dc < CARD_VECTOR_LENGTH; dc++)
        {
            if (virtualRemainingCards[dc] > 0)
            {
                // Calculate the joint probability of such play and increment the output array
                idx[PlayerIndex] = dc;
                OutDistribution[idx[0], idx[1], idx[2]] += PrioriProbability * virtualRemainingCards[dc] / remainingDeckSize;
            }
        }
    }