diff --git a/Content/EmptierThanVoid/UI/HUD/WBP_GameUI_ShipStatus.uasset b/Content/EmptierThanVoid/UI/HUD/WBP_GameUI_ShipStatus.uasset index d47171b..33df13a 100644 Binary files a/Content/EmptierThanVoid/UI/HUD/WBP_GameUI_ShipStatus.uasset and b/Content/EmptierThanVoid/UI/HUD/WBP_GameUI_ShipStatus.uasset differ diff --git a/Source/ETV/ETVAI.cpp b/Source/ETV/ETVAI.cpp new file mode 100644 index 0000000..93157fe --- /dev/null +++ b/Source/ETV/ETVAI.cpp @@ -0,0 +1,144 @@ +// Copyright (C) Team13. All rights reserved. + +#include "ETVAI.h" +#include "ETVGameModeBase.h" +#include "ETVActionTarget_Fire.h" +#include "ETVActionTarget_Move.h" + +// TODO Returns duplicate objects of all the ships on the board for AI to calculate the next move +TArray GetBoardStateClone(TArray BoardState) +{ + TArray Clones; + for(auto ShipRef : BoardState) + { + // Check for class? + + // Cast and create clone + + // Get clone's reference + + // Push reference to clones + + } + return Clones; +} + +// TODO Calculates maximum score and the coresponding board state if the given ship makes a move +TArray GetNextBoardStateAndScore(TArray BoardState, int32 ShipIndex, TArray> &MoveInstructions) +{ + return TArray(); +} + +// TODO Returns the 5 best moves for the given boardState +TArray> GetTop5Moves(TArray BoardState, TArray> &MoveInstructions) +{ + return TArray>(); +} + + +UETVAI::UETVAI() +{ + +} +// TODO Better Implementation +TArray UETVAI::GetMove(TArray Ships) +{ + TArray MoveInstructions; + + // Get gamemode + AETVGameModeBase* GameMode = Cast(Ships[0]->GetWorld()->GetAuthGameMode()); + + int32 MostImportantPlayerShip = -1; + int32 MostImportantAIShip = -1; + int32 RandomAIShip = -1; + // Find most important ships + int index = 0; + for (auto ShipReference : Ships) + { + // AI Ship + if (ShipReference->IsEnemy()) + { + if (MostImportantAIShip != -1) + { + // This will always be the capital + if (ShipReference->GetScore() > Ships[MostImportantAIShip]->GetScore()) + { + MostImportantAIShip = index; + } + } + else + { + MostImportantAIShip = index; + RandomAIShip = index; + } + // 1/8 chance + if (FMath::RandBool() && FMath::RandBool() && FMath::RandBool()) + { + RandomAIShip = index; + } + } + else if (GameMode->IsTileVisible(ShipReference->GetTilePosition(), EETVShipType::EnemyShip) == true) + { + if (MostImportantPlayerShip != -1) + { + if (ShipReference->GetScore() > Ships[MostImportantPlayerShip]->GetScore()) + { + MostImportantPlayerShip = index; + } + } + else + { + MostImportantPlayerShip = index; + } + } + ++index; + } + // Does AI see enemy ship + if (MostImportantPlayerShip != -1) + { + int jndex = 0; + // If possible try and attack + for (auto ShipReference : Ships) + { + if (ShipReference->IsEnemy()) + { + // Currently fire laser is always 0 and torpedo is 1 on fighters + int ActionIndex = 0; + if(ShipReference->GetShipClass() == "Fighter" || ShipReference->GetShipClass() == "Capital") + ActionIndex = 1; + UETVActionTarget_Fire* Laser = Cast(ShipReference->GetActions()[ActionIndex]); + // TODO - Add check if torped avaliable + Laser->SetTarget(Ships[MostImportantPlayerShip], Ships[MostImportantPlayerShip]->GetX(), Ships[MostImportantPlayerShip]->GetY()); + if (Laser->CanPerform()) + { + MoveInstructions.SetNum(4); + MoveInstructions[0] = (Ships[MostImportantPlayerShip]->GetScore()); + MoveInstructions[1] = (jndex); + MoveInstructions[2] = (ActionIndex); + MoveInstructions[3] = (MostImportantPlayerShip); + return MoveInstructions; + } + } + ++jndex; + } + } + // We we didn't find enemy ship or we can't attack it + // We move a ship randomly + // TODO Add checker if we can heal most important ship + UETVActionTarget_Move* MoveAction = Cast(Ships[RandomAIShip]->GetActions().Last(0)); + int x = -1; + int y = -1; + while (!MoveAction->CanPerform()) + { + x = Ships[RandomAIShip]->GetTilePosition().X + FMath::RandRange(1, Ships[RandomAIShip]->GetMoveRange()); + y = Ships[RandomAIShip]->GetTilePosition().Y + FMath::RandRange(1, Ships[RandomAIShip]->GetMoveRange()); + MoveAction->SetTarget(GameMode->GetTileMapActor(), x, y); + } + MoveInstructions.SetNum(5); + MoveInstructions[0] = (Ships[RandomAIShip]->GetScore()); + MoveInstructions[1] = (RandomAIShip); + MoveInstructions[2] = (Ships[RandomAIShip]->GetActions().Num()-1); + MoveInstructions[3] = (x); + MoveInstructions[4] = (y); + return MoveInstructions; +}; diff --git a/Source/ETV/ETVAI.h b/Source/ETV/ETVAI.h new file mode 100644 index 0000000..8b6e4b0 --- /dev/null +++ b/Source/ETV/ETVAI.h @@ -0,0 +1,49 @@ +// Copyright (C) Team13. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "ETVShip.h" +#include "ETVAI.generated.h" + +/** + * Class that contains all the logic for the AI in the game + * It passes move instructions to the game mode in a form of An Array of int32 Arrays, where ints represent ship indexes, + * action indexesm coordinates or traget ship indexes and the board state if this move is used, 1 array of this ints represents 1 posssible move + * -------------------------------------------- + * 0: Score + * 1: Ship index + * 2: Action index + * 3: Target ship index/X coordinate + * 4: Y coordinate + * -------------------------------------------- + * If array returned contains 4 ints it can be deduced that the last int represnts an index for the target ship else it represents the X coordinate + * of the board + * -------------------------------------------- + * Ai will look a certain amount of turns into the future, predict the best moves and choose the move that will + * lead to the best outcome in set amount of turns + * -------------------------------------------- + * It currently does not work like this. + * Problems to solve: Dynamic Deep Copying of actors or support for dummy properties in actors that AI can use to calculate board states. + * -------------------------------------------- + * AI currently atack the highes priority target of enemy, heals the ship with highest priority on his sie or moves a ship randomly + * -------------------------------------------- + */ +UCLASS() +class ETV_API UETVAI : public UObject +{ + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + UETVAI(); + + +public: + // Returns the move AI will make this turn depending on the given board state + TArray GetMove(TArray Ships); + + + +}; diff --git a/Source/ETV/ETVAction.cpp b/Source/ETV/ETVAction.cpp index c8c31b6..7aa79f6 100644 --- a/Source/ETV/ETVAction.cpp +++ b/Source/ETV/ETVAction.cpp @@ -112,7 +112,8 @@ void UETVAction::OnEndPerform() AETVGameModeBase* GameMode = Cast(GetWorld()->GetAuthGameMode()); GameMode->UpdateVisibleTiles(EETVShipType::PlayerShip); - if (bEndsTurn) + // End turn if action automatically ends it and turn is not AI controlled (AI automatically goes to next turn) + if (bEndsTurn && !GameMode->IsCurrentTurnAI()) { // TODO Delay this until all effects are done GameMode->EndTurn(); diff --git a/Source/ETV/ETVActionTarget_Fire.cpp b/Source/ETV/ETVActionTarget_Fire.cpp index 3e9fa06..726512b 100644 --- a/Source/ETV/ETVActionTarget_Fire.cpp +++ b/Source/ETV/ETVActionTarget_Fire.cpp @@ -55,9 +55,9 @@ bool UETVActionTarget_Fire::CanPerform() return true; } - // Check if enemy + // Check if enemy (not of same side, for AI compatibility) AETVShip* SelectedShip = Cast(SelectedTarget); // Required type is ship (checked in parent) so casting is safe - return SelectedShip->IsEnemy(); + return SelectedShip->GetType() != OwnerShip->GetType(); } return false; diff --git a/Source/ETV/ETVCalculator.cpp b/Source/ETV/ETVCalculator.cpp index 9adcbfe..391de96 100644 --- a/Source/ETV/ETVCalculator.cpp +++ b/Source/ETV/ETVCalculator.cpp @@ -20,6 +20,10 @@ void UETVCalculator::CalculateWeaponEffect(AETVShip *User, AETVWeapon *WeaponUse // Calculate the value of change for this effect int32 ChangeValue = User->GetMultiplier()*WeaponUsed->GetDMG(); + if(ChangeValue < 3) + { + ChangeValue = 3; + } // Weapon ignores shields if (WeaponUsed->GetType() == AETVWeapon::DamageHull) diff --git a/Source/ETV/ETVGameModeBase.cpp b/Source/ETV/ETVGameModeBase.cpp index 3be95a7..607265b 100644 --- a/Source/ETV/ETVGameModeBase.cpp +++ b/Source/ETV/ETVGameModeBase.cpp @@ -10,6 +10,7 @@ #include "ETVCalculator.h" #include "Runtime/Engine/Classes/Engine/UserInterfaceSettings.h" #include "Runtime/Core/Public/Misc/FileHelper.h" +#include "ETVAI.h" //#include "DrawDebugHelpers.h" // Uncomment for debug drawing // Sets default values @@ -25,6 +26,9 @@ AETVGameModeBase::AETVGameModeBase() /* Game Loop */ + // Set first turn to player + CurrentTurnSide = EETVShipType::PlayerShip; + // Disable game time (until everything is generated) ElapsedTime = -1.0f; CurrentTurn = 0; @@ -574,6 +578,7 @@ void AETVGameModeBase::EndTurn() CurShip->UnconditionallyCloseContextMenu(); } + CurrentTurnSide = EETVShipType::EnemyShip; CurrentTurnTime = 0.0f; // Handle turn end @@ -594,8 +599,23 @@ void AETVGameModeBase::EndTurn() } // Move control to AI - // TODO Call into AI to do its thing - // TODO AI calls NextTurn() when done + UETVAI* AI = NewObject(); + TArray Instructions = AI->GetMove(Ships); + if (Instructions.Num() == 4) + { + UETVActionTarget* Action = Cast(Ships[Instructions[1]]->GetActions()[Instructions[2]]); + Action->SetTarget(Ships[Instructions[3]], Ships[Instructions[3]]->GetX(), Ships[Instructions[3]]->GetY()); + Action->Perform(); + } + else if (Instructions.Num() == 5) + { + UETVActionTarget* Action = Cast(Ships[Instructions[1]]->GetActions()[Instructions[2]]); + Action->SetTarget(TileMapActor, Instructions[3], Instructions[4]); + Action->Perform(); + } + + // AI continue to next turn when done + NextTurn(); } } @@ -608,6 +628,7 @@ void AETVGameModeBase::NextTurn() GetShipListWidget()->Update(); // Apply next turn + CurrentTurnSide = EETVShipType::PlayerShip; CurrentTurn++; CurrentTurnTime = static_cast(TurnTime); @@ -898,3 +919,8 @@ bool AETVGameModeBase::TileHasShip(int32 x, int32 y) FPaperTileInfo TileInfo = TileMapComp->GetTile(x, y, EETVTileLayer::Ship); return TileInfo.TileSet != nullptr; } + +UPaperTileSet* AETVGameModeBase::GetShipSprite(AETVShip* Ship) +{ + return TileMapComp->GetTile(Ship->GetX(), Ship->GetY(), EETVTileLayer::Ship).TileSet; +} diff --git a/Source/ETV/ETVGameModeBase.h b/Source/ETV/ETVGameModeBase.h index de69d43..e3ad2ae 100644 --- a/Source/ETV/ETVGameModeBase.h +++ b/Source/ETV/ETVGameModeBase.h @@ -55,6 +55,7 @@ class ETV_API AETVGameModeBase : public AGameModeBase /* Game Loop */ + EETVShipType CurrentTurnSide; TArray MultiTurnActions; @@ -241,6 +242,10 @@ class ETV_API AETVGameModeBase : public AGameModeBase UFUNCTION(BlueprintCallable, Category = "ETV Game") float GetCurrentTurnPercentage(); + // Returns if current turn is AI controlled + UFUNCTION(BlueprintCallable, Category = "ETV Game") + bool IsCurrentTurnAI() const { return !bDisableAI && CurrentTurnSide == EETVShipType::EnemyShip; } + // Add multi-turn action for execution in subsequent turns automatically UFUNCTION(BlueprintCallable, Category = "ETV Game") void AddMultiTurnAction(UETVAction* Action); @@ -320,7 +325,7 @@ class ETV_API AETVGameModeBase : public AGameModeBase AETVShip* GetLastClickedShip() const { return LastClickedShip; } - /* Get Widgets */ + /* Widget and Ship Interaction */ // Get log widget UFUNCTION(BlueprintCallable, Category = "ETV UI") UETVActionLogWidget* GetLogWidget() const { return ActionLogClass; } @@ -334,6 +339,12 @@ class ETV_API AETVGameModeBase : public AGameModeBase UFUNCTION() bool TileHasShip(int32 x, int32 y); + + UFUNCTION() + UPaperTileSet* GetShipSprite(AETVShip* Ship); + + UFUNCTION() + APaperTileMapActor* GetTileMapActor() const { return TileMapActor; } }; diff --git a/Source/ETV/ETVShip.cpp b/Source/ETV/ETVShip.cpp index f8adf53..da27d59 100644 --- a/Source/ETV/ETVShip.cpp +++ b/Source/ETV/ETVShip.cpp @@ -128,10 +128,6 @@ void AETVShip::RechargeShields() { } -void AETVShip::GetReport() -{ -} - void AETVShip::SpawnContextMenu(AActor *Actor, FKey Key) { AETVGameModeBase* GameMode = Cast(GetWorld()->GetAuthGameMode()); @@ -249,15 +245,33 @@ void AETVShip::UnconditionallyCloseContextMenu() } } +int32 AETVShip::GetScore() +{ + int Score = 0; + for (auto WeaponSlot : Weapons) + { + Score += WeaponSlot->GetWeapon()->GetDMG() * GetMultiplier(); + } + Score += HealthPoints * GetMultiplier(); + return Score; +} + +TArray AETVShip::GetActions() +{ + return Actions; +} + float AETVShip::GetMultiplier() { // Calculate how much HealthPoints the ship has compared to its' initial HealthPoints - float HpStatus = HealthPoints / MaximumHealthPoints; + float HpStatus = float(HealthPoints) / float(MaximumHealthPoints); + // If between 75% and 100% retrun this percentege if (HpStatus >= 0.75 && HpStatus <= 1) return HpStatus; + // Weaker ships loose less per HP lost - We don't want ships to become useless - else if (HpStatus < 0.75 && HpStatus >= 0.50) + if (HpStatus < 0.75 && HpStatus >= 0.50) { // 75 is the minimal value for the previous case float Difference = 0.75 - HpStatus; @@ -265,7 +279,8 @@ float AETVShip::GetMultiplier() Difference = Difference / 2; return (0.75 - Difference); } - else if (HpStatus < 0.50 && HpStatus > 0.0) + + if (HpStatus < 0.50 && HpStatus > 0.0) { // 0.50 is the minimal value for the previous case float Difference = 0.50 - HpStatus; @@ -274,13 +289,11 @@ float AETVShip::GetMultiplier() // 0.625 is the minimum Mupltiplier returned by previous case return (0.625 - Difference); } - else - { - // If Ship is dead or MaxHP is less then Current HP - // We shouldn't get here! - UE_LOG(LogTemp, Error, TEXT("GetMultiplier(): Ship is dead or MaxHP is less then Current HP!")); - return 0; - } + + // If Ship is dead or MaxHP is less then Current HP + // We shouldn't get here! + UE_LOG(LogTemp, Error, TEXT("GetMultiplier(): Ship is dead or MaxHP is less then Current HP!")); + return 0; } void AETVShip::ClosingContextMenu() diff --git a/Source/ETV/ETVShip.h b/Source/ETV/ETVShip.h index 845bf8a..f8a4111 100644 --- a/Source/ETV/ETVShip.h +++ b/Source/ETV/ETVShip.h @@ -150,9 +150,6 @@ class ETV_API AETVShip : public APaperSpriteActor UFUNCTION() virtual void RechargeShields(); - UFUNCTION() - virtual void GetReport(); - UFUNCTION() void SpawnContextMenu(AActor *Actor, FKey Key); @@ -201,6 +198,17 @@ class ETV_API AETVShip : public APaperSpriteActor UFUNCTION() void UnconditionallyCloseContextMenu(); + // Score used by AI to evaluate the ship + UFUNCTION(Category = "AI") + virtual int32 GetScore(); + + // Multiplier of Score for certain Classes + const static int32 CAPITAL_SCORE_MP = 175; + + // Used by AI to call ships actions + UFUNCTION(Category = "AI") + TArray GetActions(); + // Returns a multiplier for the effectiveness of ship's actions depending on its status UFUNCTION() virtual float GetMultiplier(); @@ -222,7 +230,6 @@ class ETV_API AETVShip : public APaperSpriteActor UFUNCTION() FString GetShipType(); - // Just for checking in LOG UFUNCTION() FString GetShipClass(); diff --git a/Source/ETV/ETVShipCapital.cpp b/Source/ETV/ETVShipCapital.cpp index 27960c2..da6f9fb 100644 --- a/Source/ETV/ETVShipCapital.cpp +++ b/Source/ETV/ETVShipCapital.cpp @@ -99,3 +99,11 @@ void AETVShipCapital::SpawnWeapons() WeaponSlotShieldBattery->FitWeapon(ShieldBattery); AddWeapon(WeaponSlotShieldBattery); } + +int32 AETVShipCapital::GetScore() +{ + // TODO Find a way to call parent class GetScore and Multiply that + int Score = Super::GetScore(); + Score = Score*(double(CAPITAL_SCORE_MP)/100.00); + return Score; +} diff --git a/Source/ETV/ETVShipCapital.h b/Source/ETV/ETVShipCapital.h index b1395d6..407b5c8 100644 --- a/Source/ETV/ETVShipCapital.h +++ b/Source/ETV/ETVShipCapital.h @@ -42,6 +42,9 @@ class ETV_API AETVShipCapital : public AETVShip UFUNCTION() void SpawnWeapons(); + UFUNCTION(Category = "AI") + int32 GetScore() override; + // TODO Add methods // UseHyperdrive(int32 x, int32 y); // RepairShipInHangar(AETVShip ship, int32 hangar); diff --git a/Source/ETV/ETVWeapon.cpp b/Source/ETV/ETVWeapon.cpp index 87d6790..94009f4 100644 --- a/Source/ETV/ETVWeapon.cpp +++ b/Source/ETV/ETVWeapon.cpp @@ -25,11 +25,11 @@ void AETVWeapon::InitRandom(FName NewName, int32 PowerLvl) PowerLvl = 200; // It should take an avarage of 10 shots to kill a ship of similiar lvl - int32 AvgShotsToKill = 10; + int32 AvgShotsToKill = 7; // The range of values we can generate - int32 DMGRangeMin = PowerLvl - 25; - int32 DMGRangeMax = PowerLvl + 25; + int32 DMGRangeMin = PowerLvl - 5; + int32 DMGRangeMax = PowerLvl + 5; int32 ReqRangeMin = PowerLvl / 10; int32 ReqRangeMax = ReqRangeMin * 2;