This commit is contained in:
yaoyanwei
2025-08-04 16:45:48 +08:00
parent 565aa16389
commit 2f2a601227
2296 changed files with 522745 additions and 93 deletions

View File

@@ -0,0 +1,225 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine.AI
{
/// <summary>
/// Values and calculations for various values of the AI decision-making, adjusting these can improve your AI
/// Heuristic: Represent the score of a board state, high score favor AI, low score favor the opponent
/// Action Score: Represent the score of an individual action, to proritize actions if too many in a single node
/// Action Sort Order: Value to determine the order actions should be executed in a single turn to avoid searching same things in different order, executed in ascending order
/// </summary>
public class AIHeuristic
{
//---------- Heuristic PARAMS -------------
public int board_card_value = 20; //Score of having cards on board
public int secret_card_value = 10; //Score of having cards in secret zone
public int hand_card_value = 5; //Score of having cards in hand
public int kill_value = 5; //Score of killing a card
public int player_hp_value = 4; //Score per player hp
public int card_attack_value = 3; //Score per board card attack
public int card_hp_value = 2; //Score per board card hp
public int card_status_value = 15; //Score per status on card (multiplied by hvalue of StatusData)
//-----------
private int ai_player_id; //ID of this AI, usually the human is 0 and AI is 1
private int ai_level; //ai level (level 10 is the best, level 1 is the worst)
private int heuristic_modifier; //Randomize heuristic for lower level ai
private System.Random random_gen;
public AIHeuristic(int player_id, int level)
{
ai_player_id = player_id;
ai_level = level;
heuristic_modifier = GetHeuristicModifier();
random_gen = new System.Random();
}
public int CalculateHeuristic(Game data, NodeState node)
{
Player aiplayer = data.GetPlayer(ai_player_id);
Player oplayer = data.GetOpponentPlayer(ai_player_id);
return CalculateHeuristic(data, node, aiplayer, oplayer);
}
//Calculate full heuristic
//Should return a value between -10000 and 10000 (unless its a win)
public int CalculateHeuristic(Game data, NodeState node, Player aiplayer, Player oplayer)
{
int score = 0;
//Victories
if (aiplayer.IsDead())
score += -100000 + node.tdepth * 1000; //Add node depth to seek surviving longest
if (oplayer.IsDead())
score += 100000 - node.tdepth * 1000; //Reduce node depth to seek fastest win
//Board state
score += aiplayer.cards_board.Count * board_card_value;
score += aiplayer.cards_equip.Count * board_card_value;
score += aiplayer.cards_secret.Count * secret_card_value;
score += aiplayer.cards_hand.Count * hand_card_value;
score += aiplayer.kill_count * kill_value;
score += aiplayer.hp * player_hp_value;
score -= oplayer.cards_board.Count * board_card_value;
score -= oplayer.cards_equip.Count * board_card_value;
score -= oplayer.cards_secret.Count * secret_card_value;
score -= oplayer.cards_hand.Count * hand_card_value;
score -= oplayer.kill_count * kill_value;
score -= oplayer.hp * player_hp_value;
foreach (Card card in aiplayer.cards_board)
{
score += card.GetAttack() * card_attack_value;
score += card.GetHP() * card_hp_value;
foreach (CardStatus status in card.status)
score += status.StatusData.hvalue * card_status_value;
foreach (CardStatus status in card.ongoing_status)
score += status.StatusData.hvalue * card_status_value;
}
foreach (Card card in oplayer.cards_board)
{
score -= card.GetAttack() * card_attack_value;
score -= card.GetHP() * card_hp_value;
foreach (CardStatus status in card.status)
score -= status.StatusData.hvalue * card_status_value;
foreach (CardStatus status in card.ongoing_status)
score -= status.StatusData.hvalue * card_status_value;
}
if (heuristic_modifier > 0)
score += random_gen.Next(-heuristic_modifier, heuristic_modifier);
return score;
}
//This calculates the score of an individual action, instead of the board state
//When too many actions are possible in a single node, only the ones with best action score will be evaluated
//Make sure to return a positive value
public int CalculateActionScore(Game data, AIAction order)
{
if (order.type == GameAction.EndTurn)
return 0; //Other orders are better
if (order.type == GameAction.CancelSelect)
return 0; //Other orders are better
if (order.type == GameAction.CastAbility)
{
return 200;
}
if (order.type == GameAction.Attack)
{
Card card = data.GetCard(order.card_uid);
Card target = data.GetCard(order.target_uid);
int ascore = card.GetAttack() >= target.GetHP() ? 300 : 100; //Are you killing the card?
int oscore = target.GetAttack() >= card.GetHP() ? -200 : 0; //Are you getting killed?
return ascore + oscore + target.GetAttack() * 5; //Always better to get rid of high-attack cards
}
if (order.type == GameAction.AttackPlayer)
{
Card card = data.GetCard(order.card_uid);
Player player = data.GetPlayer(order.target_player_id);
int ascore = card.GetAttack() >= player.hp ? 500 : 200; //Are you killing the player?
return ascore + (card.GetAttack() * 10) - player.hp; //Always better to inflict more damage
}
if (order.type == GameAction.PlayCard)
{
Player player = data.GetPlayer(ai_player_id);
Card card = data.GetCard(order.card_uid);
if (card.CardData.IsBoardCard())
return 200 + (card.GetMana() * 5) - (30 * player.cards_board.Count); //High cost cards are better to play, better to play when not a lot of cards in play
else if (card.CardData.IsEquipment())
return 200 + (card.GetMana() * 5) - (30 * player.cards_equip.Count);
else
return 200 + (card.GetMana() * 5);
}
if (order.type == GameAction.Move)
{
return 100;
}
return 100; //Other actions are better than End/Cancel
}
//Within the same turn, actions can only be executed in sorting order, make sure it returns positive value higher than 0 or it wont be sorted
//This prevents calculating all possibilities of A->B->C B->C->A C->A->B etc..
//If two AIActions with same sorting value, or if sorting value is 0, ai will test all ordering variations (slower)
//This would not be necessary in a game with only 1 action per turn (such as chess) but is useful for AI that can perform multiple actions in 1 turn
//Ordering could be improved, pretty much random now
public int CalculateActionSort(Game data, AIAction order)
{
if (order.type == GameAction.EndTurn)
return 0; //End turn can always be performed, 0 means any order
if (data.selector != SelectorType.None)
return 0; //Selector actions not affected by sorting
Card card = data.GetCard(order.card_uid);
Card target = order.target_uid != null ? data.GetCard(order.target_uid) : null;
bool is_spell = card != null && !card.CardData.IsBoardCard();
int type_sort = 0;
if (order.type == GameAction.PlayCard && is_spell)
type_sort = 1; //Play Spells first
if (order.type == GameAction.CastAbility)
type_sort = 2; //Card Abilities second
if (order.type == GameAction.Move)
type_sort = 3; //Move third
if (order.type == GameAction.Attack)
type_sort = 4; //Attacks fourth
if (order.type == GameAction.AttackPlayer)
type_sort = 5; //Player attacks fifth
if (order.type == GameAction.PlayCard && !is_spell)
type_sort = 7; //Play Characters last
int card_sort = card != null ? (card.Hash % 100) : 0;
int target_sort = target != null ? (target.Hash % 100) : 0;
int sort = type_sort * 10000 + card_sort * 100 + target_sort + 1;
return sort;
}
//Lower level AI add a random number to their heuristic
private int GetHeuristicModifier()
{
if (ai_level >= 10)
return 0;
if (ai_level == 9)
return 5;
if (ai_level == 8)
return 10;
if (ai_level == 7)
return 20;
if (ai_level == 6)
return 30;
if (ai_level == 5)
return 40;
if (ai_level == 4)
return 50;
if (ai_level == 3)
return 75;
if (ai_level == 2)
return 100;
if (ai_level <= 1)
return 200;
return 0;
}
//Check if this node represent one of the players winning
public bool IsWin(NodeState node)
{
return node.hvalue > 50000 || node.hvalue < -50000;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a435ad5e46007f5449dc8d627fbe175f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,825 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Profiling;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// Minimax algorithm for AI.
/// </summary>
public class AILogic
{
//-------- AI Logic Params ------------------
public int ai_depth = 3; //How many turns in advance does it check, higher number takes exponentially longer
public int ai_depth_wide = 1; //For these first few turns, will consider more options, slow!
public int actions_per_turn = 2; //AI wont predict more than this number of sequential actions per turn, if more than that will EndTurn (Do A, then do B, then do C, then end turn)
public int actions_per_turn_wide = 3; //Same but in wide depth
public int nodes_per_action = 4; //For a turn action (1st, 2nd, or 3rd...), cannot evaluate more than this number of child nodes, if more, will only process the AIActions with with best score
public int nodes_per_action_wide = 7; //Same but in wide depth
//Example: for the first turn, AI will predict 3 sequential actions (I play a card, then attack with this one, then play a spell),
//for each of those actions, it will look at 7 possibilities, if more will cut based on score, keeping the actions with highest score
//At depth 2 and 3 it will only try to perform 2 actions but for each one will evaluate 4 possibilities. Depth 2 is the opponent's turn and depth 3 is the AI's next turn.
//For the nodes that are evaluated, will go down to depth 3 and calculate heuristic at the max depth, and then propagate the heuristic up in the node tree.
//AI will choose the move that has a path leading to the best heuristic.
//-----
public int ai_player_id; //AI player_id (usually its 1)
public int ai_level; //AI level
private GameLogic game_logic; //Game logic used to calculate moves
private Game original_data; //Original game data when start calculating possibilities
private AIHeuristic heuristic;
private Thread ai_thread;
private NodeState first_node = null;
private NodeState best_move = null;
private bool running = false;
private int nb_calculated = 0;
private int reached_depth = 0;
private System.Random random_gen;
private Pool<NodeState> node_pool = new Pool<NodeState>();
private Pool<Game> data_pool = new Pool<Game>();
private Pool<AIAction> action_pool = new Pool<AIAction>();
private Pool<List<AIAction>> list_pool = new Pool<List<AIAction>>();
private ListSwap<Card> card_array = new ListSwap<Card>();
private ListSwap<Slot> slot_array = new ListSwap<Slot>();
public static AILogic Create(int player_id, int level)
{
AILogic job = new AILogic();
job.ai_player_id = player_id;
job.ai_level = level;
job.heuristic = new AIHeuristic(player_id, level);
job.game_logic = new GameLogic(true); //Skip all delays for the AI calculations
return job;
}
public void RunAI(Game data)
{
if (running)
return;
original_data = Game.CloneNew(data); //Clone game data to keep original data unaffected
game_logic.ClearResolve(); //Clear temp memory
game_logic.SetData(original_data); //Assign data to game logic
random_gen = new System.Random(); //Reset random seed
first_node = null;
reached_depth = 0;
nb_calculated = 0;
running = true;
//Uncomment these lines to run on separate thread (and comment Execute()), better for production so it doesn't freeze the UI while calculating the AI
ai_thread = new Thread(Execute);
ai_thread.Start();
//Uncomment this line to run on main thread (and comment the thread one), better for debuging since you will be able to use breakpoints, profiler and Debug.Log
//Execute();
}
public void Stop()
{
running = false;
if (ai_thread != null && ai_thread.IsAlive)
ai_thread.Abort();
}
private void Execute()
{
//Create first node
first_node = CreateNode(null, null, ai_player_id, 0, 0);
first_node.hvalue = heuristic.CalculateHeuristic(original_data, first_node);
first_node.alpha = int.MinValue;
first_node.beta = int.MaxValue;
Profiler.BeginSample("AI");
System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
//Calculate first node
CalculateNode(original_data, first_node);
Debug.Log("AI: Time " + watch.ElapsedMilliseconds + "ms Depth " + reached_depth + " Nodes " + nb_calculated);
Profiler.EndSample();
//Save best move
best_move = first_node.best_child;
running = false;
}
//Add list of all possible orders and search in all of them
private void CalculateNode(Game data, NodeState node)
{
Profiler.BeginSample("Add Actions");
Player player = data.GetPlayer(data.current_player);
List<AIAction> action_list = list_pool.Create();
int max_actions = node.tdepth < ai_depth_wide ? actions_per_turn_wide : actions_per_turn;
if (node.taction < max_actions)
{
if (data.selector == SelectorType.None)
{
//Play card
for (int c = 0; c < player.cards_hand.Count; c++)
{
Card card = player.cards_hand[c];
AddActions(action_list, data, node, GameAction.PlayCard, card);
}
//Action on board
for (int c = 0; c < player.cards_board.Count; c++)
{
Card card = player.cards_board[c];
AddActions(action_list, data, node, GameAction.Attack, card);
AddActions(action_list, data, node, GameAction.AttackPlayer, card);
AddActions(action_list, data, node, GameAction.CastAbility, card);
//AddActions(action_list, data, node, GameAction.Move, card); //Uncomment to consider move actions
}
if (player.hero != null)
AddActions(action_list, data, node, GameAction.CastAbility, player.hero);
}
else
{
AddSelectActions(action_list, data, node);
}
}
//End Turn (dont add action if ai can still attack player, or ai hasnt spent any mana)
bool is_full_mana = HasAction(action_list, GameAction.PlayCard) && player.mana >= player.mana_max;
bool can_attack_player = HasAction(action_list, GameAction.AttackPlayer);
bool can_end = !can_attack_player && !is_full_mana && data.selector == SelectorType.None;
if (action_list.Count == 0 || can_end)
{
AIAction actiont = CreateAction(GameAction.EndTurn);
action_list.Add(actiont);
}
//Remove actions with low score
FilterActions(data, node, action_list);
Profiler.EndSample();
//Execute valid action and search child node
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
if (action.valid && node.alpha < node.beta)
{
CalculateChildNode(data, node, action);
}
}
action_list.Clear();
list_pool.Dispose(action_list);
}
//Mark valid/invalid on each action, if too many actions, will keep only the ones with best score
private void FilterActions(Game data, NodeState node, List<AIAction> action_list)
{
int count_valid = 0;
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
action.sort = heuristic.CalculateActionSort(data, action);
action.valid = action.sort <= 0 || action.sort >= node.sort_min;
if (action.valid)
count_valid++;
}
int max_actions = node.tdepth < ai_depth_wide ? nodes_per_action_wide : nodes_per_action;
int max_actions_skip = max_actions + 2; //No need to calculate all scores if its just to remove 1-2 actions
if (count_valid <= max_actions_skip)
return; //No filtering needed
//Calculate scores
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
if (action.valid)
{
action.score = heuristic.CalculateActionScore(data, action);
}
}
//Sort, and invalidate actions with low score
action_list.Sort((AIAction a, AIAction b) => { return b.score.CompareTo(a.score); });
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
action.valid = action.valid && o < max_actions;
}
}
//Create a child node for parent, and calculate it
private void CalculateChildNode(Game data, NodeState parent, AIAction action)
{
if (action.type == GameAction.None)
return;
int player_id = data.current_player;
//Clone data so we can update it in a new node
Profiler.BeginSample("Clone Data");
Game ndata = data_pool.Create();
Game.Clone(data, ndata); //Clone
game_logic.ClearResolve();
game_logic.SetData(ndata);
Profiler.EndSample();
//Execute move and update data
Profiler.BeginSample("Execute AIAction");
DoAIAction(ndata, action, player_id);
Profiler.EndSample();
//Update depth
bool new_turn = action.type == GameAction.EndTurn;
int next_tdepth = parent.tdepth;
int next_taction = parent.taction + 1;
if (new_turn)
{
next_tdepth = parent.tdepth + 1;
next_taction = 0;
}
//Create node
Profiler.BeginSample("Create Node");
NodeState child_node = CreateNode(parent, action, player_id, next_tdepth, next_taction);
parent.childs.Add(child_node);
Profiler.EndSample();
//Set minimum sort for next AIActions, if new turn, reset to 0
child_node.sort_min = new_turn ? 0 : Mathf.Max(action.sort, child_node.sort_min);
//If win or reached max depth, stop searching deeper
if (!ndata.HasEnded() && child_node.tdepth < ai_depth)
{
//Calculate child
CalculateNode(ndata, child_node);
}
else
{
//End of tree, calculate full Heuristic
child_node.hvalue = heuristic.CalculateHeuristic(ndata, child_node);
}
//Update parents hvalue, alpha, beta, and best child
if (player_id == ai_player_id)
{
//AI player
if (parent.best_child == null || child_node.hvalue > parent.hvalue)
{
parent.best_child = child_node;
parent.hvalue = child_node.hvalue;
parent.alpha = Mathf.Max(parent.alpha, parent.hvalue);
}
}
else
{
//Opponent player
if (parent.best_child == null || child_node.hvalue < parent.hvalue)
{
parent.best_child = child_node;
parent.hvalue = child_node.hvalue;
parent.beta = Mathf.Min(parent.beta, parent.hvalue);
}
}
//Just for debug, keep track of node/depth count
nb_calculated++;
if (child_node.tdepth > reached_depth)
reached_depth = child_node.tdepth;
//We are done with this game data, dispose it.
//Dont dispose NodeState here (node_pool) since we want to retrieve the full tree path later
data_pool.Dispose(ndata);
}
private NodeState CreateNode(NodeState parent, AIAction action, int player_id, int turn_depth, int turn_action)
{
NodeState nnode = node_pool.Create();
nnode.current_player = player_id;
nnode.tdepth = turn_depth;
nnode.taction = turn_action;
nnode.parent = parent;
nnode.last_action = action;
nnode.alpha = parent != null ? parent.alpha : int.MinValue;
nnode.beta = parent != null ? parent.beta : int.MaxValue;
nnode.hvalue = 0;
nnode.sort_min = 0;
return nnode;
}
//Add all possible moves for card to list of actions
private void AddActions(List<AIAction> actions, Game data, NodeState node, ushort type, Card card)
{
Player player = data.GetPlayer(data.current_player);
if (data.selector != SelectorType.None)
return;
if (card.HasStatus(StatusType.Paralysed))
return;
if (type == GameAction.PlayCard)
{
if (card.CardData.IsBoardCard())
{
//Doesn't matter where the card is played
Slot slot = player.GetRandomEmptySlot(random_gen, slot_array.Get());
if (data.CanPlayCard(card, slot))
{
AIAction action = CreateAction(type, card);
action.slot = slot;
actions.Add(action);
}
}
else if (card.CardData.IsEquipment())
{
Player tplayer = data.GetPlayer(card.player_id);
for (int c = 0; c < tplayer.cards_board.Count; c++)
{
Card tcard = tplayer.cards_board[c];
if (data.CanPlayCard(card, tcard.slot))
{
AIAction action = CreateAction(type, card);
action.slot = tcard.slot;
action.target_player_id = tplayer.player_id;
actions.Add(action);
}
}
}
else if (card.CardData.IsRequireTargetSpell())
{
for (int p = 0; p < data.players.Length; p++)
{
Player tplayer = data.players[p];
Slot tslot = new Slot(tplayer.player_id);
if (data.CanPlayCard(card, tslot))
{
AIAction action = CreateAction(type, card);
action.slot = tslot;
action.target_player_id = tplayer.player_id;
actions.Add(action);
}
}
foreach (Slot slot in Slot.GetAll())
{
if (data.CanPlayCard(card, slot))
{
Card slot_card = data.GetSlotCard(slot);
AIAction action = CreateAction(type, card);
action.slot = slot;
action.target_uid = slot_card != null ? slot_card.uid : null;
actions.Add(action);
}
}
}
else if (data.CanPlayCard(card, Slot.None))
{
AIAction action = CreateAction(type, card);
actions.Add(action);
}
}
if (type == GameAction.Attack)
{
if (card.CanAttack())
{
for (int p = 0; p < data.players.Length; p++)
{
if (p != player.player_id)
{
Player oplayer = data.players[p];
for (int tc = 0; tc < oplayer.cards_board.Count; tc++)
{
Card target = oplayer.cards_board[tc];
if (data.CanAttackTarget(card, target))
{
AIAction action = CreateAction(type, card);
action.target_uid = target.uid;
actions.Add(action);
}
}
}
}
}
}
if (type == GameAction.AttackPlayer)
{
if (card.CanAttack())
{
for (int p = 0; p < data.players.Length; p++)
{
if (p != player.player_id)
{
Player oplayer = data.players[p];
if (data.CanAttackTarget(card, oplayer))
{
AIAction action = CreateAction(type, card);
action.target_player_id = oplayer.player_id;
actions.Add(action);
}
}
}
}
}
if (type == GameAction.CastAbility)
{
List<AbilityData> abilities = card.GetAbilities();
for (int a = 0; a < abilities.Count; a++)
{
AbilityData ability = abilities[a];
if (ability.trigger == AbilityTrigger.Activate && data.CanCastAbility(card, ability) && ability.HasValidSelectTarget(data, card))
{
AIAction action = CreateAction(type, card);
action.ability_id = ability.id;
actions.Add(action);
}
}
}
if (type == GameAction.Move)
{
foreach (Slot slot in Slot.GetAll(player.player_id))
{
if (data.CanMoveCard(card, slot))
{
AIAction action = CreateAction(type, card);
action.slot = slot;
actions.Add(action);
}
}
}
}
//Add all possible moves for a selection
private void AddSelectActions(List<AIAction> actions, Game data, NodeState node)
{
if (data.selector == SelectorType.None)
return;
Player player = data.GetPlayer(data.selector_player_id);
Card caster = data.GetCard(data.selector_caster_uid);
AbilityData ability = AbilityData.Get(data.selector_ability_id);
if (player == null || caster == null)
return;
if (data.selector == SelectorType.SelectTarget && ability != null)
{
for (int p = 0; p < data.players.Length; p++)
{
Player tplayer = data.players[p];
if (ability.CanTarget(data, caster, tplayer))
{
AIAction action = CreateAction(GameAction.SelectPlayer, caster);
action.target_player_id = tplayer.player_id;
actions.Add(action);
}
}
foreach (Slot slot in Slot.GetAll())
{
Card tcard = data.GetSlotCard(slot);
if (tcard != null && ability.CanTarget(data, caster, tcard))
{
AIAction action = CreateAction(GameAction.SelectCard, caster);
action.target_uid = tcard.uid;
actions.Add(action);
}
else if (tcard == null && ability.CanTarget(data, caster, slot))
{
AIAction action = CreateAction(GameAction.SelectSlot, caster);
action.slot = slot;
actions.Add(action);
}
}
}
if (data.selector == SelectorType.SelectorCard && ability != null)
{
for (int p = 0; p < data.players.Length; p++)
{
List<Card> cards = ability.GetCardTargets(data, caster, card_array);
foreach (Card tcard in cards)
{
AIAction action = CreateAction(GameAction.SelectCard, caster);
action.target_uid = tcard.uid;
actions.Add(action);
}
}
}
if (data.selector == SelectorType.SelectorChoice && ability != null)
{
for (int i = 0; i < ability.chain_abilities.Length; i++)
{
AbilityData choice = ability.chain_abilities[i];
if (choice != null && data.CanSelectAbility(caster, choice))
{
AIAction action = CreateAction(GameAction.SelectChoice, caster);
action.value = i;
actions.Add(action);
}
}
}
if (data.selector == SelectorType.SelectorCost)
{
for (int i = 1; i <= player.mana; i++)
{
AIAction action = CreateAction(GameAction.SelectCost, caster);
action.value = i;
actions.Add(action);
}
}
//Add option to cancel, if no valid options
if (actions.Count == 0)
{
AIAction caction = CreateAction(GameAction.CancelSelect, caster);
actions.Add(caction);
}
}
private AIAction CreateAction(ushort type)
{
AIAction action = action_pool.Create();
action.Clear();
action.type = type;
action.valid = true;
return action;
}
private AIAction CreateAction(ushort type, Card card)
{
AIAction action = action_pool.Create();
action.Clear();
action.type = type;
action.card_uid = card.uid;
action.valid = true;
return action;
}
//Simulate AI action
private void DoAIAction(Game data, AIAction action, int player_id)
{
Player player = data.GetPlayer(player_id);
if (action.type == GameAction.PlayCard)
{
Card card = player.GetHandCard(action.card_uid);
game_logic.PlayCard(card, action.slot);
}
if (action.type == GameAction.Move)
{
Card card = player.GetBoardCard(action.card_uid);
game_logic.MoveCard(card, action.slot);
}
if (action.type == GameAction.Attack)
{
Card card = player.GetBoardCard(action.card_uid);
Card target = data.GetBoardCard(action.target_uid);
game_logic.AttackTarget(card, target);
}
if (action.type == GameAction.AttackPlayer)
{
Card card = player.GetBoardCard(action.card_uid);
Player tplayer = data.GetPlayer(action.target_player_id);
game_logic.AttackPlayer(card, tplayer);
}
if (action.type == GameAction.CastAbility)
{
Card card = player.GetCard(action.card_uid);
AbilityData ability = AbilityData.Get(action.ability_id);
game_logic.CastAbility(card, ability);
}
if (action.type == GameAction.SelectCard)
{
Card target = data.GetCard(action.target_uid);
game_logic.SelectCard(target);
}
if (action.type == GameAction.SelectPlayer)
{
Player target = data.GetPlayer(action.target_player_id);
game_logic.SelectPlayer(target);
}
if (action.type == GameAction.SelectSlot)
{
game_logic.SelectSlot(action.slot);
}
if (action.type == GameAction.SelectChoice)
{
game_logic.SelectChoice(action.value);
}
if (action.type == GameAction.CancelSelect)
{
game_logic.CancelSelection();
}
if (action.type == GameAction.EndTurn)
{
game_logic.EndTurn();
}
}
private bool HasAction(List<AIAction> list, ushort type)
{
for (int i = 0; i < list.Count; i++)
{
if (list[i].type == type)
return true;
}
return false;
}
//----Return values----
public bool IsRunning()
{
return running;
}
public string GetNodePath()
{
return GetNodePath(first_node);
}
public string GetNodePath(NodeState node)
{
string path = "Prediction: HValue: " + node.hvalue + "\n";
NodeState current = node;
AIAction move;
while (current != null)
{
move = current.last_action;
if (move != null)
path += "Player " + current.current_player + ": " + move.GetText(original_data) + "\n";
current = current.best_child;
}
return path;
}
public void ClearMemory()
{
original_data = null;
first_node = null;
best_move = null;
foreach (NodeState node in node_pool.GetAllActive())
node.Clear();
foreach (AIAction order in action_pool.GetAllActive())
order.Clear();
data_pool.DisposeAll();
node_pool.DisposeAll();
action_pool.DisposeAll();
list_pool.DisposeAll();
System.GC.Collect(); //Free memory from AI
}
public int GetNbNodesCalculated()
{
return nb_calculated;
}
public int GetDepthReached()
{
return reached_depth;
}
public NodeState GetBest()
{
return best_move;
}
public NodeState GetFirst()
{
return first_node;
}
public AIAction GetBestAction()
{
return best_move != null ? best_move.last_action : null;
}
public bool IsBestFound()
{
return best_move != null;
}
}
public class NodeState
{
public int tdepth; //Depth in number of turns
public int taction; //How many orders in current turn
public int sort_min; //Sorting minimum value, orders below this value will be ignored to avoid calculate both path A -> B and path B -> A
public int hvalue; //Heuristic value, this AI tries to maximize it, opponent tries to minimize it
public int alpha; //Highest heuristic reached by the AI player, used for optimization and ignore some tree branch
public int beta; //Lowest heuristic reached by the opponent player, used for optimization and ignore some tree branch
public AIAction last_action = null;
public int current_player;
public NodeState parent;
public NodeState best_child = null;
public List<NodeState> childs = new List<NodeState>();
public NodeState() { }
public NodeState(NodeState parent, int player_id, int turn_depth, int turn_action, int turn_sort)
{
this.parent = parent;
this.current_player = player_id;
this.tdepth = turn_depth;
this.taction = turn_action;
this.sort_min = turn_sort;
}
public void Clear()
{
last_action = null;
best_child = null;
parent = null;
childs.Clear();
}
}
public class AIAction
{
public ushort type;
public string card_uid;
public string target_uid;
public int target_player_id;
public string ability_id;
public Slot slot;
public int value;
public int score; //Score to determine which orders get cut and ignored
public int sort; //Orders must be executed in sort order
public bool valid; //If false, this order will be ignored
public AIAction() { }
public AIAction(ushort t) { type = t; }
public string GetText(Game data)
{
string txt = GameAction.GetString(type);
Card card = data.GetCard(card_uid);
Card target = data.GetCard(target_uid);
if (card != null)
txt += " card " + card.card_id;
if (target != null)
txt += " target " + target.card_id;
if (slot != Slot.None)
txt += " slot " + slot.x + "-" + slot.p;
if (ability_id != null)
txt += " ability " + ability_id;
if (value > 0)
txt += " value " + value;
return txt;
}
public void Clear()
{
type = 0;
valid = false;
card_uid = null;
target_uid = null;
ability_id = null;
target_player_id = -1;
slot = Slot.None;
value = -1;
score = 0;
sort = 0;
}
public static AIAction None { get { AIAction a = new AIAction(); a.type = 0; return a; } }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ab13916fa774b3419a43439fc50ec1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// AI player base class, other AI inherit from this
/// </summary>
public abstract class AIPlayer
{
public int player_id;
public int ai_level;
protected GameLogic gameplay;
public virtual void Update()
{
//Script called by game server to update AI
//Override this to let the AI play
}
public bool CanPlay()
{
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
bool can_play = game_data.IsPlayerTurn(player) || game_data.IsPlayerMulliganTurn(player);
return can_play && !gameplay.IsResolving();
}
public static AIPlayer Create(AIType type, GameLogic gameplay, int id, int level = 0)
{
if (type == AIType.Random)
return new AIPlayerRandom(gameplay, id, level);
if (type == AIType.MiniMax)
return new AIPlayerMM(gameplay, id, level);
return null;
}
}
public enum AIType
{
Random = 0, //Dumb AI that just do random moves, useful for testing cards without getting destroyed
MiniMax = 10, //Stronger AI using Minimax algo with alpha-beta pruning
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0354b727a964c654787c7597d4091569
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,287 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// AI player using the MinMax AI algorithm
/// </summary>
public class AIPlayerMM : AIPlayer
{
private AILogic ai_logic;
private bool is_playing = false;
public AIPlayerMM(GameLogic gameplay, int id, int level)
{
this.gameplay = gameplay;
player_id = id;
ai_level = Mathf.Clamp(level, 1, 10);
ai_logic = AILogic.Create(id, ai_level);
}
public override void Update()
{
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (!is_playing && game_data.IsPlayerTurn(player))
{
is_playing = true;
TimeTool.StartCoroutine(AiTurn());
}
if (!is_playing && game_data.IsPlayerMulliganTurn(player))
{
SkipMulligan();
}
if (!game_data.IsPlayerTurn(player) && ai_logic.IsRunning())
Stop();
}
private IEnumerator AiTurn()
{
yield return new WaitForSeconds(1f);
Game game_data = gameplay.GetGameData();
ai_logic.RunAI(game_data);
while (ai_logic.IsRunning())
{
yield return new WaitForSeconds(0.1f);
}
AIAction best = ai_logic.GetBestAction();
if (best != null)
{
Debug.Log("Execute AI Action: " + best.GetText(game_data) + "\n" + ai_logic.GetNodePath());
//foreach (NodeState node in ai_logic.GetFirst().childs)
// Debug.Log(ai_logic.GetNodePath(node));
ExecuteAction(best);
}
ai_logic.ClearMemory();
yield return new WaitForSeconds(0.5f);
is_playing = false;
}
private void Stop()
{
ai_logic.Stop();
is_playing = false;
}
//----------
private void ExecuteAction(AIAction action)
{
if (!CanPlay())
return;
if (action.type == GameAction.PlayCard)
{
PlayCard(action.card_uid, action.slot);
}
if (action.type == GameAction.Attack)
{
AttackCard(action.card_uid, action.target_uid);
}
if (action.type == GameAction.AttackPlayer)
{
AttackPlayer(action.card_uid, action.target_player_id);
}
if (action.type == GameAction.Move)
{
MoveCard(action.card_uid, action.slot);
}
if (action.type == GameAction.CastAbility)
{
CastAbility(action.card_uid, action.ability_id);
}
if (action.type == GameAction.SelectCard)
{
SelectCard(action.target_uid);
}
if (action.type == GameAction.SelectPlayer)
{
SelectPlayer(action.target_player_id);
}
if (action.type == GameAction.SelectSlot)
{
SelectSlot(action.slot);
}
if (action.type == GameAction.SelectChoice)
{
SelectChoice(action.value);
}
if (action.type == GameAction.SelectCost)
{
SelectCost(action.value);
}
if (action.type == GameAction.SelectMulligan)
{
SkipMulligan();
}
if (action.type == GameAction.CancelSelect)
{
CancelSelect();
}
if (action.type == GameAction.EndTurn)
{
EndTurn();
}
if (action.type == GameAction.Resign)
{
Resign();
}
}
private void PlayCard(string card_uid, Slot slot)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(card_uid);
if (card != null)
{
gameplay.PlayCard(card, slot);
}
}
private void MoveCard(string card_uid, Slot slot)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(card_uid);
if (card != null)
{
gameplay.MoveCard(card, slot);
}
}
private void AttackCard(string attacker_uid, string target_uid)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(attacker_uid);
Card target = game_data.GetCard(target_uid);
if (card != null && target != null)
{
gameplay.AttackTarget(card, target);
}
}
private void AttackPlayer(string attacker_uid, int target_player_id)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(attacker_uid);
if (card != null)
{
Player oplayer = game_data.GetPlayer(target_player_id);
gameplay.AttackPlayer(card, oplayer);
}
}
private void CastAbility(string caster_uid, string ability_id)
{
Game game_data = gameplay.GetGameData();
Card caster = game_data.GetCard(caster_uid);
AbilityData iability = AbilityData.Get(ability_id);
if (caster != null && iability != null)
{
gameplay.CastAbility(caster, iability);
}
}
private void SelectCard(string target_uid)
{
Game game_data = gameplay.GetGameData();
Card target = game_data.GetCard(target_uid);
if (target != null)
{
gameplay.SelectCard(target);
}
}
private void SelectPlayer(int tplayer_id)
{
Game game_data = gameplay.GetGameData();
Player target = game_data.GetPlayer(tplayer_id);
if (target != null)
{
gameplay.SelectPlayer(target);
}
}
private void SelectSlot(Slot slot)
{
if (slot != Slot.None)
{
gameplay.SelectSlot(slot);
}
}
private void SelectChoice(int choice)
{
gameplay.SelectChoice(choice);
}
private void SelectCost(int cost)
{
gameplay.SelectCost(cost);
}
private void CancelSelect()
{
if (CanPlay())
{
gameplay.CancelSelection();
}
}
private void SkipMulligan()
{
string[] cards = new string[0]; //Don't mulligan
SelectMulligan(cards);
}
private void SelectMulligan(string[] cards)
{
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
gameplay.Mulligan(player, cards);
}
private void EndTurn()
{
if (CanPlay())
{
gameplay.EndTurn();
}
}
private void Resign()
{
int other = player_id == 0 ? 1 : 0;
gameplay.EndGame(other);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee38d0542562f9543b095360388a2867
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,340 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Client;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// AI player making completely random decisions, really bad AI but useful for testing
/// </summary>
public class AIPlayerRandom : AIPlayer
{
private bool is_playing = false;
private bool is_selecting = false;
private System.Random rand = new System.Random();
public AIPlayerRandom(GameLogic gameplay, int id, int level)
{
this.gameplay = gameplay;
player_id = id;
}
public override void Update()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (game_data.IsPlayerTurn(player) && !gameplay.IsResolving())
{
if(!is_playing && game_data.selector == SelectorType.None && game_data.current_player == player_id)
{
is_playing = true;
TimeTool.StartCoroutine(AiTurn());
}
if (!is_selecting && game_data.selector != SelectorType.None && game_data.selector_player_id == player_id)
{
if (game_data.selector == SelectorType.SelectTarget)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectTarget());
}
if (game_data.selector == SelectorType.SelectorCard)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectCard());
}
if (game_data.selector == SelectorType.SelectorChoice)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectChoice());
}
if (game_data.selector == SelectorType.SelectorCost)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectCost());
}
}
}
if (!is_selecting && game_data.IsPlayerMulliganTurn(player))
{
is_selecting = true;
TimeTool.StartCoroutine(AiSelectMulligan());
}
}
private IEnumerator AiTurn()
{
yield return new WaitForSeconds(1f);
PlayCard();
yield return new WaitForSeconds(0.5f);
PlayCard();
yield return new WaitForSeconds(0.5f);
PlayCard();
yield return new WaitForSeconds(0.5f);
Attack();
yield return new WaitForSeconds(0.5f);
Attack();
yield return new WaitForSeconds(0.5f);
AttackPlayer();
yield return new WaitForSeconds(0.5f);
EndTurn();
is_playing = false;
}
private IEnumerator AiSelectCard()
{
yield return new WaitForSeconds(0.5f);
SelectCard();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectTarget()
{
yield return new WaitForSeconds(0.5f);
SelectTarget();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectChoice()
{
yield return new WaitForSeconds(0.5f);
SelectChoice();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectCost()
{
yield return new WaitForSeconds(0.5f);
SelectCost();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectMulligan()
{
yield return new WaitForSeconds(0.5f);
SelectMulligan();
yield return new WaitForSeconds(0.5f);
is_selecting = false;
}
//----------
public void PlayCard()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (player.cards_hand.Count > 0 && game_data.IsPlayerActionTurn(player))
{
Card random = player.GetRandomCard(player.cards_hand, rand);
Slot slot = player.GetRandomEmptySlot(rand);
if (random != null && random.CardData.IsRequireTargetSpell())
slot = game_data.GetRandomSlot(rand); //Spell can target any slot, not just your side
if(random != null && random.CardData.IsEquipment())
slot = player.GetRandomOccupiedSlot(rand);
if (random != null)
gameplay.PlayCard(random, slot);
}
}
public void Attack()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (player.cards_board.Count > 0 && game_data.IsPlayerActionTurn(player))
{
Card random = player.GetRandomCard(player.cards_board, rand);
Card rtarget = game_data.GetRandomBoardCard(rand);
if (random != null && rtarget != null)
gameplay.AttackTarget(random, rtarget);
}
}
public void AttackPlayer()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
Player oplayer = game_data.GetRandomPlayer(rand);
if (player.cards_board.Count > 0 && game_data.IsPlayerActionTurn(player))
{
Card random = player.GetRandomCard(player.cards_board, rand);
if (random != null && oplayer != null && oplayer != player)
gameplay.AttackPlayer(random, oplayer);
}
}
public void SelectCard()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
AbilityData ability = AbilityData.Get(game_data.selector_ability_id);
Card caster = game_data.GetCard(game_data.selector_caster_uid);
if (player != null && ability != null && caster != null)
{
List<Card> card_list = ability.GetCardTargets(game_data, caster);
if (card_list.Count > 0)
{
Card card = card_list[rand.Next(0, card_list.Count)];
gameplay.SelectCard(card);
}
}
}
public void SelectTarget()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.selector != SelectorType.None)
{
int target_player = player_id;
AbilityData ability = AbilityData.Get(game_data.selector_ability_id);
if (ability != null && ability.target == AbilityTarget.SelectTarget)
target_player = (player_id == 0 ? 1 : 0);
Player tplayer = game_data.GetPlayer(target_player);
if (tplayer.cards_board.Count > 0)
{
Card random = tplayer.GetRandomCard(tplayer.cards_board, rand);
if (random != null)
gameplay.SelectCard(random);
}
}
}
public void SelectChoice()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.selector != SelectorType.None)
{
AbilityData ability = AbilityData.Get(game_data.selector_ability_id);
if (ability != null && ability.chain_abilities.Length > 0)
{
int choice = rand.Next(0, ability.chain_abilities.Length);
gameplay.SelectChoice(choice);
}
}
}
public void SelectCost()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.selector != SelectorType.None)
{
Player player = game_data.GetPlayer(player_id);
Card card = game_data.GetCard(game_data.selector_caster_uid);
if (player != null && card != null)
{
int max = Mathf.Clamp(player.mana, 0, 9);
int choice = rand.Next(0, max + 1);
gameplay.SelectCost(choice);
}
}
}
public void CancelSelect()
{
if (CanPlay())
{
gameplay.CancelSelection();
}
}
public void SelectMulligan()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.phase == GamePhase.Mulligan)
{
Player player = game_data.GetPlayer(player_id);
string[] cards = new string[0]; //Don't mulligan
gameplay.Mulligan(player, cards);
}
}
public void EndTurn()
{
if (CanPlay())
{
gameplay.EndTurn();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f327588bb7c02141baa6fbeb0081e84
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: