Witcher 3 Gwent card game AI class

Disclaimer: the project is a thought experiment/school project, assumed to be under the Fair Use definition
Mean disclaimer: it is recommended to stop reading further if “how it looks” is more important than “how it works”
Facetious disclaimer: the project is based on the code from the original Witcher 3 game that I shamelessly stolen(read: reverse engineered from AS3 and translated into UE4/C++)
Technical disclaimer: the project is a C++ project and relies heavily on reflection, in particular UFunction*/UFUNCTION(). Uses UE4 4.15

Unlike other attempts to re-create the original Witcher 3 Gwent card game, which relied on human players, this project attempts to create a fully functional AI that can be used either against another AI or against a human player.

Notable things to mention without breaking the attention span with a wall of text are:

The function “decideWhichCardToPlay” which returns UCardTransaction*, and its evaluation relies on the following switch statements


TACTIC_SPY_DUMMY_BEST_THEN_PASS
TACTIC_MINIMIZE_LOSS
TACTIC_MINIMIZE_WIN
TACTIC_MAXIMIZE_WIN
TACTIC_AVERAGE_WIN
TACTIC_MINIMAL_WIN
TACTIC_JUST_WAIT
TACTIC_WAIT_DUMMY
TACTIC_SPY

Also there are 3 finite state machines.

The game flow controller


stateMachine->AddState("Initializing", nullptr, state_update_Initializing, state_leave_Initializing);
stateMachine->AddState("Tutorials", state_begin_Tutorials, state_update_Tutorials, nullptr);
stateMachine->AddState("SpawnLeaders", state_begin_SpawnLeaders, state_update_SpawnLeaders, nullptr);
stateMachine->AddState("CoinToss", state_begin_CoinToss, state_update_CoinToss, nullptr);
stateMachine->AddState("Mulligan", state_begin_Mulligan, state_update_Mulligan, nullptr);
stateMachine->AddState("RoundStart", state_begin_RoundStart, state_update_RoundStart, nullptr);
stateMachine->AddState("PlayerTurn", state_begin_PlayerTurn, state_update_PlayerTurn, state_leave_PlayerTurn);
stateMachine->AddState("ChangingPlayer", state_begin_ChangingPlayer, state_update_ChangingPlayer, nullptr);
stateMachine->AddState("ShowingRoundResult", state_begin_ShowingRoundResult, state_update_ShowingRoundResult, nullptr);
stateMachine->AddState("ClearingBoard", state_begin_ClearingBoard, state_update_ClearingBoard, state_leave_ClearingBoard);
stateMachine->AddState("ShowingFinalResult", state_begin_ShowingFinalResult, state_update_ShowingFinalResult, nullptr);
stateMachine->AddState("Reset", state_begin_reset, nullptr, nullptr);

the human player controller


_stateMachine->AddState("Idle", state_begin_Idle, on_state_about_to_update, state_end_Idle);
_stateMachine->AddState("ChoosingCard", state_begin_ChoosingCard, state_update_ChoosingCard, state_end_ChoosingCard);
_stateMachine->AddState("ChoosingHandler", state_begin_ChoosingHandler, state_update_ChoosingHandler, nullptr);
_stateMachine->AddState("ChoosingTargetCard", state_begin_ChoosingTargetCard, state_update_ChoosingTargetCard, nullptr);
_stateMachine->AddState("WaitConfirmation", state_begin_WaitConfirmation, state_update_WaitConfirmation, nullptr);
_stateMachine->AddState("ApplyingCard", state_begin_ApplyingCard, state_update_ApplyingCard, nullptr);

and the AI player controller


_stateMachine->AddState("Idle", state_begin_Idle, nullptr, state_end_Idle);
_stateMachine->AddState("ChoosingMove", state_begin_ChoosingMove, state_update_ChoosingMove, nullptr);
_stateMachine->AddState("SendingCardToTransaction", state_begin_SendingCard, state_update_SendingCard, nullptr);
_stateMachine->AddState("DelayBetweenActions", state_begin_DelayAction, state_update_DelayAction, nullptr);
_stateMachine->AddState("ApplyingCard", state_begin_ApplyingCard, state_update_ApplyingCard, nullptr);

all the “state_” are actually UFunction*

if you would like to learn more (card instance, card leader, card template, card transaction, card manager etc.), please let me know. Thanks!

FiniteStateMachine class has a timer that checks UpdateStates()


void UFiniteStateMachine::UpdateStates()
{
	if (nextState != currentStateName && disallowStateChangeFunc)
	{
		return;
	}
	if (nextState != currentStateName && stateList.Contains(nextState))
	{
#ifdef DEBUG
		LogWarning("GFX - [FSM] Switching from: " + currentStateName + ", to:" + nextState);

#endif // DEBUG

		if (stateList.Contains(currentStateName) && stateList[currentStateName].leaveStateCallback)
		{
			void* locals = nullptr;

			ownerRef->ProcessEvent(stateList[currentStateName].leaveStateCallback, locals);
		}
		prevStateName = currentStateName;
		currentStateName = nextState;
		if (stateList.Contains(nextState) && stateList[nextState].enterStateCallback)
		{
			void* locals = nullptr;

			ownerRef->ProcessEvent(stateList[currentStateName].enterStateCallback, locals);
		}
	}
	if (currentStateName == "")
	{
		return;
	}
	if (stateList[currentStateName].updateStateCallback)
	{
		void* locals = nullptr;

		ownerRef->ProcessEvent(stateList[currentStateName].updateStateCallback, locals);
	}
}

The AddState class


	FFSMState fsm;
	fsm.stateTag = stateTag;
	fsm.enterStateCallback = enterStateCallback;
	fsm.updateStateCallback = updateStateCallback;
	fsm.leaveStateCallback = leaveStateCallback;

	stateList.Add(stateTag, fsm);


	if (currentStateName == "" && nextState == "")
	{
		nextState = stateTag;
	}

which uses a simple struct


USTRUCT()
struct FFSMState
{
	GENERATED_BODY()

	UPROPERTY()
		FString stateTag = "";
	UPROPERTY()
		UFunction* enterStateCallback = nullptr;
	UPROPERTY()
		UFunction* updateStateCallback = nullptr;
	UPROPERTY()
		UFunction* leaveStateCallback = nullptr;

	FORCEINLINE bool operator== (const FFSMState& Other)
	{
		return stateTag == Other.stateTag;
	}
};

finally the ChangeState class


void UFiniteStateMachine::ChangeState(FString stateTag)
{
	bool bHasNextState = stateList.Contains(stateTag);

	if (bHasNextState)
	{
		nextState = stateTag;
	}
#ifdef DEBUG
	else
	{
		LogWarning("GFX - [WARNING] Tried to change to an unknown state: " + stateTag);
	}
#endif // DEBUG
}

here is the class that sucks all the information from XML and converts it into card templates. This applies for both cards and leader cards ( Kings and such).


FString sCards = FPaths::GameDevelopersDir() + "Source/XML/" + "def_gwint_cards_final.xml";

const FXmlFile fileCards(sCards);

const FXmlNode* redXmlNode = fileCards.GetRootNode();

const TArray<FXmlNode*> cardDefinitions = redXmlNode->GetChildrenNodes();

for (int32 i = 0; i < cardDefinitions.Num(); i++)
{
	UCardTemplate* _template = NewObject<UCardTemplate>();

	FXmlNode* _node = cardDefinitions*;
	const TArray<FXmlNode*> children = _node->GetChildrenNodes();
	const TArray<FXmlAttribute> attributes = _node->GetAttributes();

	FXmlNode* _typeNode = children[0]; //type flags
	const TArray<FXmlNode*> typeFlags = _typeNode->GetChildrenNodes();

	for (int32 t = 0; t < typeFlags.Num(); t++)
	{
		FXmlNode* _typeFlag = typeFlags[t];
		const TArray<FXmlAttribute> typeAttributes = _typeFlag->GetAttributes();

		int32 nType = ConvertTypeStringToInt(typeAttributes[0].GetValue());

		_template->nTypeArray |= nType;
	}

	FXmlNode* _effectNode = children[1]; //effect flags
	const TArray<FXmlNode*> effectFlags = _effectNode->GetChildrenNodes();

	for (int32 e = 0; e < effectFlags.Num(); e++)
	{
		FXmlNode* _effectFlag = effectFlags[e];
		const TArray<FXmlAttribute> effectAttributes = _effectFlag->GetAttributes();

		int32 nEffect = ConvertEffectStringToInt(effectAttributes[0].GetValue());

		_template->effectFlags.Add(nEffect);
	}

	if (children.Num() == 3) //summon flags
	{
		FXmlNode* _summonNode = children[2];
		const TArray<FXmlNode*> summonFlags = _summonNode->GetChildrenNodes();

		for (int32 e = 0; e < summonFlags.Num(); e++)
		{
			FXmlNode* _summonFlag = summonFlags[e];
			const TArray<FXmlAttribute> summonAttributes = _summonFlag->GetAttributes();

			int32 nsummon = FCString::Atoi(*summonAttributes[0].GetValue());

			_template->summonFlags.Add(nsummon);
		}
	}

	_template->nIndex = FCString::Atoi(*_node->GetAttribute("index"));
	_template->nPower = FCString::Atoi(*_node->GetAttribute("power"));
	_template->nFactionIdx = ConvertFactionStringToInt(_node->GetAttribute("faction_index"));

	_template->sImageLoc = _node->GetAttribute("picture");
	_template->sTitle = _node->GetAttribute("title");
	_template->sDescription = _node->GetAttribute("description");

	cardManager->_cardTemplates.Add(_template->nIndex, _template);
}

below an example of how the game controller states flow


Timestamp 0.378737: GFX - [FSM] Switching from: , to:Initializing
Timestamp 0.41285: GFX - [FSM] Switching from: Initializing, to:Tutorials
Timestamp 0.41285: //TODO Implement Tutorials
Timestamp 0.612851: GFX - [FSM] Switching from: Tutorials, to:SpawnLeaders
Timestamp 0.612851: GFX ##########################################################
Timestamp 0.612851: GFX -#AI#-----------------------------------------------------------------------------------------------------
Timestamp 0.612851: GFX -#AI#----------------------------- NEW GWINT GAME ------------------------------------
Timestamp 0.612851: GFX -#AI#-----------------------------------------------------------------------------------------------------
Timestamp 0.612851: GFX ====== Adding card with instance ID: 1, to List ID: LEADER, for player: 0
Timestamp 0.612851: Leader Player 0 ID is 1001
Timestamp 0.612851: GFX ====== Adding card with instance ID: 2, to List ID: LEADER, for player: 1
Timestamp 0.612851: Leader Player 1 ID is 2001
Timestamp 0.812852: GFX - [FSM] Switching from: SpawnLeaders, to:CoinToss
Timestamp 0.812852: GFX - Flip coin logic, player1 faction: 3 , player2 faction:2
Timestamp 0.812852: CoinToss result is 1
Timestamp 0.812852: Player 1 starts! gwint_player_will_go_first_message | coin_flip_loss
Timestamp 1.012853: GFX - [FSM] Switching from: CoinToss, to:Mulligan
Timestamp 1.012853: //TODO Implement Mulligan
Timestamp 1.212856: GFX - [FSM] Switching from: Mulligan, to:RoundStart
Timestamp 1.412858: GFX - [FSM] Switching from: RoundStart, to:PlayerTurn
Timestamp 1.412858: GFX -#AI# starting player turn for player: 1
Timestamp 1.412858: [gwint_opponent_turn_start_message]] | Opponents_turn

Some details: player 0 is human and player 1 is AI. Player 0 is Northern Kingdom faction and player 1 is Nilfgaard. In this example the AI wins the coin toss and game controller finite state machine hands over to the AI finite state machine, coming soon.

I skipped Tutorial and Mulligan states for now, because they are visual/interactive and am currently interested in the AI finite state machine flow.

here is the class that the AI is using to choose the attitude/tactic for the round it currently plays


void UAIPlayerController::ChooseAttitude()
{
	int32 nHandCounter = 0;
	int32 nRoundsCounter = 0;
	int32 nWinner = 0;
	
	float fMedian = 0.f;

	UCardInstance* dummyCardInstance = nullptr;

	TArray<UCardInstance*> cardsHandList = GetCardManager()->GetCardInstanceList(CARD_LIST_LOC_HAND, nPlayer);
	if (cardsHandList.Num() == 0)
	{
		nAttitude = TACTIC_PASS;
		return;
	}

	bool bWinnerPlayer = false;
	bool bWinnerOpponent = false;
	
	int32 nCounterCreature = 0;
	int32 nCounterDummy = 0;
	int32 nCounterSpyPlayer = 0;

	while (nRoundsCounter < GetCardManager()->roundResults.Num())
	{
		if (GetCardManager()->roundResults[nRoundsCounter]->Played())
		{
			nWinner = GetCardManager()->roundResults[nRoundsCounter]->GetRoundWinningPlayer();
			
			if (nWinner == nPlayer || nWinner == PLAYER_INVALID)
			{
				bWinnerPlayer = true;
			}
			if (nWinner == nOpponent || nWinner == PLAYER_INVALID)
			{
				bWinnerOpponent = true;
			}
		}
		
		nRoundsCounter++;
	}
	
	bCurrentRoundCritical = bWinnerOpponent;
	
	nHandCounter = 0;
	while (nHandCounter < cardsHandList.Num())
	{
		if (cardsHandList[nHandCounter]->templateRef->IsType(CardType_Creature))
		{
			nCounterCreature++;
		}
		
		nHandCounter++;
	}
	
	int32 nHandCardsPlayer = cardsHandList.Num();
	int32 nHandCardsOpponent = GetCardManager()->GetCardInstanceList(CARD_LIST_LOC_HAND, nOpponent).Num();
	int32 nHandCardsDifference = nHandCardsPlayer - nHandCardsOpponent;
	int32 nCurrentScoreDifference = GetCardManager()->currentPlayerScores[nPlayer] - GetCardManager()->currentPlayerScores[nOpponent];
	int32 nOpponentRoundStatus = GetGameFlowController()->playerControllers[nOpponent]->nCurrentRoundStatus;

#ifdef DEBUG
	LogWarning("GFX -#AI# ###############################################################################");
	LogWarning("GFX -#AI#---------------------------- AI Deciding his next move --------------------------------");
	LogWarning("GFX -#AI#------ previousTactic: " + ConvertAttitudeToString(nAttitude));
	LogWarning("GFX -#AI#------ playerCardsInHand: " + FString::FromInt(nHandCardsPlayer));
	LogWarning("GFX -#AI#------ opponentCardsInHand: " + FString::FromInt(nHandCardsOpponent));
	LogWarning("GFX -#AI#------ cardAdvantage: " + FString::FromInt(nHandCardsDifference));
	LogWarning("GFX -#AI#------ scoreDifference: " + FString::FromInt(nCurrentScoreDifference) + ", his score: " + FString::FromInt(GetCardManager()->currentPlayerScores[nPlayer]) + ", enemy score: " + FString::FromInt(GetCardManager()->currentPlayerScores[nOpponent]));
	LogWarning("GFX -#AI#------ opponent has won: " + bWinnerOpponent);
	LogWarning("GFX -#AI#------ has won: " + bWinnerPlayer);
	LogWarning("GFX -#AI#------ Num units in hand: " + nCounterCreature);

	if (GetGameFlowController()->playerControllers[nOpponent]->nCurrentRoundStatus == ROUND_PLAYER_STATUS_DONE)
	{
		LogWarning("GFX -#AI#------ has opponent passed: true");
	}
	else
	{
		LogWarning("GFX -#AI#------ has opponent passed: false");
	}

	LogWarning("GFX =#AI#=======================================================================================");
	LogWarning("GFX -#AI#-----------------------------   AI CARDS AT HAND   ------------------------------------");

	nHandCounter = 0;

	while (nHandCounter < cardsHandList.Num())
	{
		LogWarning("GFX -#AI# Card Points " + FString::FromInt(cardsHandList[nHandCounter]->templateRef->nPower) + " ], Card -"+ cardsHandList[nHandCounter]->GetName());

		nHandCounter++;
	}
	LogWarning("GFX =#AI#=======================================================================================");

#endif // DEBUG

	int32 nFactionPlayer = GetCardManager()->playerDeckDefinitions[nPlayer]->GetDeckFaction();
	int32 nFactionOpponent = GetCardManager()->playerDeckDefinitions[nOpponent]->GetDeckFaction();
	int32 nCounterSpyOpponent = GetCardManager()->GetCardsInSlotIdWithEffect(CardEffect_Draw2, nOpponent).Num();

	if (nFactionPlayer == FactionId_Nilfgaard && nFactionOpponent != FactionId_Nilfgaard 
		&& nOpponentRoundStatus == ROUND_PLAYER_STATUS_DONE && nCurrentScoreDifference == 0)
	{
		nAttitude = TACTIC_PASS;
	}
	else if (!bWinnerOpponent && nAttitude == TACTIC_SPY_DUMMY_BEST_THEN_PASS)
	{
		if (nOpponentRoundStatus != ROUND_PLAYER_STATUS_DONE)
		{
			nAttitude = TACTIC_SPY_DUMMY_BEST_THEN_PASS;
		}
	}
	else if (!bWinnerOpponent && GetCardManager()->GetFirstCardInHandWithEffect(CardEffect_Draw2, nPlayer) != nullptr 
		&& (UKismetMathLibrary::RandomFloatInRange(0, 1) < 0.2 || nCounterSpyOpponent > 1) 
		&& nAttitude != TACTIC_SPY_DUMMY_BEST_THEN_PASS)
	{
		nAttitude = TACTIC_SPY;
	}
	else if (nAttitude == TACTIC_SPY 
		&& GetCardManager()->GetFirstCardInHandWithEffect(CardEffect_Draw2, nPlayer) != nullptr)
	{
		nAttitude = TACTIC_SPY;
	}
	else if (nOpponentRoundStatus != ROUND_PLAYER_STATUS_DONE)
	{
		if (nCurrentScoreDifference > 0)
		{
			if (bWinnerOpponent)
			{
				nAttitude = TACTIC_JUST_WAIT;
			}
			else
			{
				fMedian = nCounterCreature * nCounterCreature / 36;
				
				nAttitude = TACTIC_NONE;
				
				if (bWinnerPlayer)
				{
					nCounterDummy = GetCardManager()->GetCardsInHandWithEffect(CardEffect_UnsummonDummy, nPlayer).Num();
					nCounterSpyPlayer = GetCardManager()->GetCardsInHandWithEffect(CardEffect_Draw2, nPlayer).Num();
					
					if (UKismetMathLibrary::RandomFloatInRange(0, 1) < 0.2 
						|| nHandCardsPlayer == nCounterDummy + nCounterSpyPlayer)
					{
						nAttitude = TACTIC_SPY_DUMMY_BEST_THEN_PASS;
					}
					else
					{
						dummyCardInstance = GetCardManager()->GetFirstCardInHandWithEffect(CardEffect_UnsummonDummy, nPlayer);
						
						if (dummyCardInstance != nullptr 
							&& GetCardManager()->GetHigherOrLowerValueTargetCardOnBoard(dummyCardInstance, nPlayer, false) != nullptr)
						{
							nAttitude = TACTIC_WAIT_DUMMY;
						}
						else if (UKismetMathLibrary::RandomFloatInRange(0, 1) < nCurrentScoreDifference / 30 
							&& UKismetMathLibrary::RandomFloatInRange(0, 1) < fMedian)
						{
							nAttitude = TACTIC_MAXIMIZE_WIN;
						}
					}
				}
				if (nAttitude == TACTIC_NONE)
				{
					if (UKismetMathLibrary::RandomFloatInRange(0, 1) < nHandCardsPlayer / 10 || nHandCardsPlayer > 8)
					{
						if (UKismetMathLibrary::RandomFloatInRange(0, 1) < 0.2 
							|| nHandCardsPlayer == nCounterDummy + nCounterSpyPlayer)
						{
							nAttitude = TACTIC_SPY_DUMMY_BEST_THEN_PASS;
						}
						else
						{
							nAttitude = TACTIC_JUST_WAIT;
						}
					}
					else
					{
						nAttitude = TACTIC_PASS;
					}
				}
			}
		}
		else if (bWinnerPlayer)
		{
			nCounterDummy = GetCardManager()->GetCardsInHandWithEffect(CardEffect_UnsummonDummy, nPlayer).Num();
			nCounterSpyPlayer = GetCardManager()->GetCardsInHandWithEffect(CardEffect_Draw2, nPlayer).Num();
			
			if (!bWinnerOpponent && (UKismetMathLibrary::RandomFloatInRange(0, 1) < 0.2 || nHandCardsPlayer == nCounterDummy + nCounterSpyPlayer))
			{
				nAttitude = TACTIC_SPY_DUMMY_BEST_THEN_PASS;
			}
			else
			{
				nAttitude = TACTIC_MAXIMIZE_WIN;
			}
		}
		else if (bWinnerOpponent)
		{
			nAttitude = TACTIC_MINIMAL_WIN;
		}
		else if (!GetCardManager()->roundResults[0]->Played() && nCurrentScoreDifference < -11 
			&& UKismetMathLibrary::RandomFloatInRange(0, 1) < (FMath::Abs(nCurrentScoreDifference) - 10) / 20)
		{
			if (UKismetMathLibrary::RandomFloatInRange(0, 1) < 0.9)
			{
				nAttitude = TACTIC_SPY_DUMMY_BEST_THEN_PASS;
			}
			else
			{
				nAttitude = TACTIC_PASS;
			}
		}
		else if (UKismetMathLibrary::RandomFloatInRange(0, 1) < nHandCardsPlayer / 10)
		{
			nAttitude = TACTIC_MINIMAL_WIN;
		}
		else if (UKismetMathLibrary::RandomFloatInRange(0, 1) < nHandCardsPlayer / 10)
		{
			nAttitude = TACTIC_AVERAGE_WIN;
		}
		else if (UKismetMathLibrary::RandomFloatInRange(0, 1) < nHandCardsPlayer / 10)
		{
			nAttitude = TACTIC_MAXIMIZE_WIN;
		}
		else if (nHandCardsPlayer <= 8 && UKismetMathLibrary::RandomFloatInRange(0, 1) > nHandCardsPlayer / 10)
		{
			nAttitude = TACTIC_PASS;
		}
		else
		{
			nAttitude = TACTIC_JUST_WAIT;
		}
	}
	else if (nAttitude != TACTIC_MINIMIZE_LOSS)
	{
		if (!bWinnerOpponent && nCurrentScoreDifference <= 0 
			&& UKismetMathLibrary::RandomFloatInRange(0, 1) < nCurrentScoreDifference / 20)
		{
			nAttitude = TACTIC_MINIMIZE_LOSS;
		}
		else if (!bWinnerPlayer && nCurrentScoreDifference > 0)
		{
			nAttitude = TACTIC_MINIMIZE_WIN;
		}
		else if (nCurrentScoreDifference > 0)
		{
			nAttitude = TACTIC_PASS;
		}
		else
		{
			nAttitude = TACTIC_MINIMAL_WIN;
		}
	}
	else
	{
		nAttitude = TACTIC_MINIMIZE_LOSS;
	}
}


as you can see, this class makes use of some randomization, just in case :slight_smile:

Ha, this is awesome. I just finished a functional complete AI evaluation, and it spit out the following:



LogTemp:Warning: 1.614382) AIPlayerController_1 | GFX - [FSM] Switching from: Idle, to:ChoosingMove
LogTemp:Warning: 1.614382) attitude chosen based on criteria: NO CARDS IN HAND
LogTemp:Warning: 1.614382) GFX -#AI# ai has decided to use the following attitude: PASS

it’s cracking me up, the poor AI trying to play Gwent without any cards :smiley:

Anyways, I’ll give them cards soon so the AI can play at least one hand…

as a trivia, few words about reverse engineering used for the project.

my working assumption was that Witcher 3 Gwent is actually some sort of UI. And from past experience in AAA games UI is either proprietary or uses Scaleform. so after extracting all the resources from the original archives, I looked for any file that has ‘gwent’ in its name. The fun part is that actually the original developers call this game ‘gwint’ instead of ‘gwent’, no idea why, but lucky me I eventually found some file with ‘gwint’ in his name that had header looking like this:

after staring at it for a while I noticed what I was looking for, in this case ‘CFX’ which is uncompressed Scaleform. right before these beloved letters there was a double representing the length of the CFX file, so it was just a matter of carving it out from its parent, replace CFX with FWS and happily extract the actionscript 3 code, there are ubiquitous tools for this. And that’s how I ended up with the code that I started to translate and port to UE4.

I’m currently testing a simple manual deck builder for the AI/human player controllers to pit them against each other hehehe…

as a bonus, an example of a power/strategic sorter for the cards


inline static bool PowerChangeSorter(const UCardInstance& card1, const UCardInstance& card2)
	{
		if (card1.GetOptimalTransaction()->powerChangeResult == card2.GetOptimalTransaction()->powerChangeResult)
		{
			return card1.GetOptimalTransaction()->strategicValue > card2.GetOptimalTransaction()->strategicValue;
		}
		return (card1.GetOptimalTransaction()->powerChangeResult > card2.GetOptimalTransaction()->powerChangeResult);
	}

hehehe, apparently I messed something up. So I gave the players some cards in their decks, moved 10 of them in their hand, and I was waiting anxiously for the AI to show me how smart it is, and the AI went through the motions of the finite states, got to the point where it evaluated and chose a tactic as ‘MINIMAL WIN’ as expected because it’s the first hand in the game, and then proudly spit out the creature card with the maximum value in its hand ( a 10) instead of the expected creature card with a minimum value (a 1). but… But… What just happened? :smiley:

Anyways, I think I have to shift the polarity of my sorting, hopefully this would turn the AI from Forrest Gump into an Einstein hehehe

Success! Check out some relevant excerpts from the logs

About generating the decks, for the player I’m using the same deck as the first instance in the original Witcher 3 game. Also, it may not be obvious but in the first game, when the tutorial is active, the developers decided that the time when the cards are being drawn from the deck into hand to actually force it manually, so we did the same every single time for every single person who starts the game the first time.
as for the AI deck, I’m using a simple code to randomly generate a deck that is close in strength with what ever do player deck strength currently is, see below the logs. The simple code just tries to match cards power and effect, it’s basically an attempt to auto balance the decks while randomly generating AI decks.


LogTemp:Warning: 1.01438) GFX -#AI#------------------- DECK STRENGTH --------------------
LogTemp:Warning: 1.01438) GFX -#AI#--- PLAYER 1:
LogTemp:Warning: 1.01438) GFX -#AI#----- > Original Strength: 113
LogTemp:Warning: 1.01438) GFX -#AI#--- PLAYER 2:
LogTemp:Warning: 1.01438) GFX -#AI#----- > Original Strength: 107
LogTemp:Warning: 1.01438) GFX -#AI#------------------------------------------------------

Now the AI logs

First it looks at the game history


LogTemp:Warning: 1.614995) GFX -#AI# ###############################################################################
LogTemp:Warning: 1.614995) GFX -#AI#---------------------------- AI Deciding his next move --------------------------------
LogTemp:Warning: 1.614995) GFX -#AI#------ previousTactic: NONE - ERROR
LogTemp:Warning: 1.614995) GFX -#AI#------ playerCardsInHand: 10
LogTemp:Warning: 1.614995) GFX -#AI#------ opponentCardsInHand: 10
LogTemp:Warning: 1.614995) GFX -#AI#------ cardAdvantage: 0
LogTemp:Warning: 1.614995) GFX -#AI#------ scoreDifference: 0, his score: 0, enemy score: 0
LogTemp:Warning: 1.614995) GFX -#AI#------ opponent has won: False
LogTemp:Warning: 1.614995) GFX -#AI#------ has won: False
LogTemp:Warning: 1.614995) GFX -#AI#------ Num units in hand: 7
LogTemp:Warning: 1.614995) GFX -#AI#------ has opponent passed: False
LogTemp:Warning: 1.614995) GFX =#AI#=======================================================================================

then it looks at the cards it has in hand


LogTemp:Warning: 1.614995) GFX =#AI#=======================================================================================
LogTemp:Warning: 1.614995) GFX -#AI#-----------------------------   AI CARDS AT HAND   ------------------------------------
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 5 ], Card: gwint_name_renuald
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 0 ], Card: gwint_name_frost
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 4 ], Card: gwint_name_vanhemar
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 10 ], Card: gwint_name_menno
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 0 ], Card: gwint_name_clear_sky
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 10 ], Card: gwint_name_moorvran
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 10 ], Card: gwint_name_letho
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 0 ], Card: gwint_name_fog
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 6 ], Card: gwint_name_assire
LogTemp:Warning: 1.614995) GFX -#AI# Card Points 2 ], Card: gwint_name_sweers
LogTemp:Warning: 1.614995) GFX =#AI#=======================================================================================

then he chooses the card that matches the evaluated tactic, in our case ‘MINIMAL WIN’ because of the first-hand played in the game, in our case it correctly chooses the creature card with the smallest value in hand which happens to be ‘gwint_name_sweers’


LogTemp:Warning: 1.811853) GFX -#AI# AI is sending the following card into transaction: gwint_name_sweers

after that it plays the card and the gain flow controller switches players


LogTemp:Warning: 2.011855) AIPlayerController_1 | GFX - [FSM] Switching from: SendingCardToTransaction, to:DelayBetweenActions
LogTemp:Warning: 3.611877) AIPlayerController_1 | GFX - [FSM] Switching from: DelayBetweenActions, to:ApplyingCard
LogTemp:Warning: 3.811882) AIPlayerController_1 | GFX - [FSM] Switching from: ApplyingCard, to:Idle
LogTemp:Warning: 4.011885) GwintGameFlowController_0 | GFX - [FSM] Switching from: PlayerTurn, to:ChangingPlayer
LogTemp:Warning: 4.211882) GwintGameFlowController_0 | GFX - [FSM] Switching from: ChangingPlayer, to:PlayerTurn
LogTemp:Warning: 4.211882) GFX -#AI# starting player turn for player: 0
LogTemp:Warning: 4.211882) [gwint_player_turn_start_message]] | your_turn

And now it’s the human player turn. To be implemented…

example of end result on tablet (currently on Surface and Android)

and a surreal example, DAI vs MEA