public void SelectAbility(int index) { var currentMob = _game.CurrentMob; if (currentMob == null) { return; } var mobInfo = _game.MobManager.MobInfos[currentMob.Value]; if (index >= mobInfo.Abilities.Count) { Utils.Log(LogSeverity.Info, nameof(GameBoardController), "Trying to select an ability index higher than the number of abilities."); return; } var ability = mobInfo.Abilities[index]; if (SelectedAbilityIndex.HasValue) { if (SelectedAbilityIndex.Value == index) { SelectedAbilityIndex = null; } else if (GameInvariants.IsAbilityUsableNoTarget(_game, currentMob.Value, ability)) { SelectedAbilityIndex = index; } } else if (GameInvariants.IsAbilityUsableNoTarget(_game, currentMob.Value, ability)) { SelectedAbilityIndex = index; } }
/// <summary> /// Generates possible direct ability use actions. /// </summary> public static bool GenerateDirectAbilityUse(GameInstance state, CachedMob mob, List <UctAction> result) { bool foundAbilityUse = false; var mobInfo = mob.MobInfo; var mobId = mob.MobId; foreach (var abilityId in mobInfo.Abilities) { if (!GameInvariants.IsAbilityUsableNoTarget(state, mobId, abilityId)) { continue; } foreach (var targetId in state.MobManager.Mobs) { if (GameInvariants.IsAbilityUsable(state, mob, state.CachedMob(targetId), abilityId)) { foundAbilityUse = true; var action = UctAction.AbilityUseAction(abilityId, mobId, targetId); GameInvariants.AssertValidAction(state, action); result.Add(action); } } } return(foundAbilityUse); }
public void Render(Entity entity, SpriteBatch batch, AssetManager assetManager) { var effect = assetManager.LoadEffect(AssetManager.ShaderAbility); batch.Begin(effect: effect, samplerState: Camera2D.SamplerState); var mob = _mobFunc(); if (mob != null) { var abilityId = mob.MobInfo.Abilities[_abilityIndex]; var isActive = _gameBoardController.SelectedAbilityIndex == _abilityIndex; if (GameInvariants.IsAbilityUsableNoTarget(_game, mob.MobId, abilityId)) { isActive = true; } var ability = _game.MobManager.Abilities[abilityId]; batch.Draw(assetManager[AssetManager.SpellBg], entity.RenderPosition); if (entity.AABB.Contains(InputManager.Instance.MousePosition)) { batch.Draw(assetManager[AssetManager.SpellHighlight], entity.RenderPosition); } } else { Debug.WriteLine("ERROR - Rendering abilities, but no mob is currently active."); batch.Draw(assetManager[AssetManager.NoTexture], entity.RenderPosition); } batch.End(); }
/// <summary> /// Generates a list of possible actions, truncated for the purposes of MCTS. /// </summary> public static List <UctAction> PossibleActions(GameInstance game, UctNode parent, bool allowMove, bool allowEndTurn) { var result = new List <UctAction>(10); var currentMob = game.CurrentMob; if (currentMob.HasValue) { var mob = game.CachedMob(currentMob.Value); GameInvariants.AssertMobPlayable(game, mob); bool foundAbilityUse = GenerateDirectAbilityUse(game, mob, result); // We disable movement if there is a possibility to cast abilities. if (allowMove && (Constants.AlwaysAttackMove || !foundAbilityUse)) { GenerateAttackMoveActions(game, game.CachedMob(mob.MobId), result); } if (allowMove) { if (parent == null || parent.Action.Type != UctActionType.DefensiveMove) { GenerateDefensiveMoveActions(game, mob, result); } } } else { Utils.Log(LogSeverity.Warning, nameof(UctNode), "Final state reached while trying to compute possible actions."); throw new InvalidOperationException(); } if (allowEndTurn) { // We would skip end turn if there are not enough actions. if (!Constants.EndTurnAsLastResort || result.Count <= 1) { result.Add(UctAction.EndTurnAction()); } } GameInvariants.AssertValidActions(game, result); return(result); }
private void UnselectAbilityIfNeeded() { var mobId = _game.CurrentMob; if (mobId == null || !SelectedAbilityIndex.HasValue) { return; } var mobInfo = _game.MobManager.MobInfos[mobId.Value]; var selectedAbility = mobInfo.Abilities[SelectedAbilityIndex.Value]; if (!GameInvariants.IsAbilityUsableNoTarget(_game, mobId.Value, selectedAbility)) { SelectedAbilityIndex = null; } }
/// <summary> /// Generates a number of defensive move actions based on a heatmap. /// </summary> public static void GenerateDefensiveMoveActions(GameInstance state, CachedMob mob, List <UctAction> result) { var heatmap = Heatmap.BuildHeatmap(state, null, false); var coords = new List <AxialCoord>(); var mobInstance = mob.MobInstance; var mobId = mob.MobId; foreach (var coord in heatmap.Map.AllCoords) { if (heatmap.Map[coord] != heatmap.MinValue) { continue; } if (state.Map[coord] == HexType.Wall) { continue; } if (state.State.AtCoord(coord, true).HasValue) { continue; } bool canMoveTo = state.Pathfinder.Distance(mobInstance.Coord, coord) <= mobInstance.Ap; if (!canMoveTo) { continue; } coords.Add(coord); } coords.Shuffle(); int maximumMoveActions = Math.Max(0, 3 - result.Count); for (int i = 0; i < Math.Min(coords.Count, maximumMoveActions); i++) { var action = UctAction.DefensiveMoveAction(mobId, coords[i]); GameInvariants.AssertValidAction(state, action); result.Add(action); } }
/// <summary> /// Calculates a move towards an enemy, or ends a turn if no such move is possible. /// </summary> private static UctAction PickMoveTowardsEnemyAction(GameInstance game, CachedMob mob, CachedMob possibleTarget) { foreach (var targetId in game.MobManager.Mobs) { var target = game.CachedMob(targetId); if (!GameInvariants.IsTargetableNoSource(game, mob, target)) { continue; } var action = FastMoveTowardsEnemy(game, mob, target); if (action.Type == UctActionType.Move) { return(action); } } return(UctAction.EndTurnAction()); }
/// <summary> /// Runs a benchmark comparing two different AIs against each other on a fixed map. /// </summary> public static void CompareAi() { var dna = new DNA(2, 2); dna.Randomize(); var map = Map.Load(@"data/map.json"); var game = GameSetup.GenerateFromDna(dna, dna, map); game.PrepareEverything(); GameInvariants.AssertMobsNotStandingOnEachother(game); IMobController c1, c2; switch (Constants.MctsBenchType) { case 0: c1 = new MctsController(game, Constants.MctsBenchIterations); c2 = new AiRandomController(game); break; case 1: c1 = new MctsController(game, Constants.MctsBenchIterations); c2 = new AiRuleBasedController(game); break; case 2: c1 = new AiRuleBasedController(game); c2 = new AiRandomController(game); break; default: throw new ArgumentException($"Invalid value of {Constants.MctsBenchType} for --MctsBenchType"); } var iterationStopwatch = new Stopwatch(); int c1Wins = 0; int c2Wins = 0; for (int i = 0; i < 1000; i++) { dna.Randomize(); iterationStopwatch.Restart(); GameSetup.OverrideGameDna(game, dna, dna); var r1 = GameEvaluator.Playout(game, c1, c2); GameSetup.OverrideGameDna(game, dna, dna); var r2 = GameEvaluator.Playout(game, c2, c1); iterationStopwatch.Stop(); Console.WriteLine(Accounting.GetStats()); c1Wins += r1.RedWins + r2.BlueWins; c2Wins += r1.BlueWins + r2.RedWins; Console.WriteLine($"{i.ToString("0000")} STATS: M2: {c1Wins}, M5: {c2Wins}, winrate: {((double)c1Wins/(c1Wins+c2Wins)).ToString("0.000")}"); } }
/// <summary> /// Generates possible attack move actions. /// </summary> public static void GenerateAttackMoveActions(GameInstance state, CachedMob mob, List <UctAction> result) { var mobInfo = mob.MobInfo; var mobInstance = mob.MobInstance; foreach (var enemyId in state.MobManager.Mobs) { var target = state.CachedMob(enemyId); if (!GameInvariants.IsTargetableNoSource(state, mob, target)) { continue; } AxialCoord myCoord = mobInstance.Coord; AxialCoord?closestCoord = null; int? distance = null; int? chosenAbilityId = null; foreach (var coord in state.Map.EmptyCoords) { if (!state.Map.IsVisible(coord, target.MobInstance.Coord)) { continue; } var possibleMoveAction = GameInvariants.CanMoveTo(state, mob, coord); if (possibleMoveAction.Type == UctActionType.Null) { continue; } Debug.Assert(possibleMoveAction.Type == UctActionType.Move); foreach (var abilityId in mobInfo.Abilities) { if (!GameInvariants.IsAbilityUsableFrom(state, mob, coord, target, abilityId)) { continue; } int myDistance = state.Pathfinder.Distance(myCoord, coord); if (!closestCoord.HasValue) { chosenAbilityId = abilityId; closestCoord = coord; distance = myDistance; } else if (distance.Value > myDistance) { chosenAbilityId = abilityId; closestCoord = coord; distance = myDistance; } } } if (closestCoord.HasValue) { if (Constants.AttackMoveEnabled) { var action = UctAction.AttackMoveAction(mob.MobId, closestCoord.Value, chosenAbilityId.Value, target.MobId); var after = ActionEvaluator.F(state, action.ToPureMove()); GameInvariants.AssertValidAbilityUseAction(after, action.ToPureAbilityUse()); GameInvariants.AssertValidAction(state, action); result.Add(action); } else { var action = UctAction.MoveAction(mob.MobId, closestCoord.Value); GameInvariants.AssertValidAction(state, action); result.Add(action); } } } }
/// <summary> /// Calculates an action according to a simple default policy. Used mainly /// in MCTS playouts. /// </summary> public static UctAction DefaultPolicyAction(GameInstance state) { var mobId = state.CurrentMob; if (mobId == null) { throw new InvalidOperationException("Requesting mob action when there is no current mob."); } Debug.Assert(state.State.MobInstances[mobId.Value].Hp > 0, "Current mob is dead"); var mob = state.CachedMob(mobId.Value); if (mob.MobInstance.Ap == 0) { return(UctAction.EndTurnAction()); } var abilityIds = new List <int>(); foreach (var possibleAbilityId in mob.MobInfo.Abilities) { if (GameInvariants.IsAbilityUsableNoTarget(state, mobId.Value, possibleAbilityId)) { abilityIds.Add(possibleAbilityId); } } int moveTargetId = MobInstance.InvalidId; var actions = new List <UctAction>(); foreach (var possibleTargetId in state.MobManager.Mobs) { var possibleTarget = state.CachedMob(possibleTargetId); moveTargetId = possibleTargetId; if (!GameInvariants.IsTargetable(state, mob, possibleTarget)) { continue; } if (abilityIds.Count == 0) { continue; } foreach (var abilityId in abilityIds) { if (GameInvariants.IsAbilityUsableApRangeCheck(state, mob, possibleTarget, abilityId)) { actions.Add(UctAction.AbilityUseAction(abilityId, mob.MobId, possibleTargetId)); } } } if (actions.Count > 0) { return(MaxAbilityRatio(state, actions)); } if (moveTargetId != MobInstance.InvalidId) { return(PickMoveTowardsEnemyAction(state, state.CachedMob(mobId.Value), state.CachedMob(moveTargetId))); } else { Utils.Log(LogSeverity.Error, nameof(ActionGenerator), "No targets, game should be over"); throw new InvalidOperationException("No targets, game should be over."); } }