init
This commit is contained in:
225
Assets/TcgEngine/Scripts/AI/AIHeuristic.cs
Normal file
225
Assets/TcgEngine/Scripts/AI/AIHeuristic.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
Assets/TcgEngine/Scripts/AI/AIHeuristic.cs.meta
Normal file
11
Assets/TcgEngine/Scripts/AI/AIHeuristic.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a435ad5e46007f5449dc8d627fbe175f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
825
Assets/TcgEngine/Scripts/AI/AILogic.cs
Normal file
825
Assets/TcgEngine/Scripts/AI/AILogic.cs
Normal 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; } }
|
||||
}
|
||||
}
|
||||
11
Assets/TcgEngine/Scripts/AI/AILogic.cs.meta
Normal file
11
Assets/TcgEngine/Scripts/AI/AILogic.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ab13916fa774b3419a43439fc50ec1c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/TcgEngine/Scripts/AI/AIPlayer.cs
Normal file
48
Assets/TcgEngine/Scripts/AI/AIPlayer.cs
Normal 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
|
||||
}
|
||||
}
|
||||
11
Assets/TcgEngine/Scripts/AI/AIPlayer.cs.meta
Normal file
11
Assets/TcgEngine/Scripts/AI/AIPlayer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0354b727a964c654787c7597d4091569
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
287
Assets/TcgEngine/Scripts/AI/AIPlayerMM.cs
Normal file
287
Assets/TcgEngine/Scripts/AI/AIPlayerMM.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/TcgEngine/Scripts/AI/AIPlayerMM.cs.meta
Normal file
11
Assets/TcgEngine/Scripts/AI/AIPlayerMM.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee38d0542562f9543b095360388a2867
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
340
Assets/TcgEngine/Scripts/AI/AIPlayerRandom.cs
Normal file
340
Assets/TcgEngine/Scripts/AI/AIPlayerRandom.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/TcgEngine/Scripts/AI/AIPlayerRandom.cs.meta
Normal file
11
Assets/TcgEngine/Scripts/AI/AIPlayerRandom.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f327588bb7c02141baa6fbeb0081e84
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user