diff --git a/Docs.meta b/Docs.meta new file mode 100644 index 0000000..66cd592 --- /dev/null +++ b/Docs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4b04b84ea3c92a340a9d48a92d141ae3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Docs/GOAP_ClassDiagram.png b/Docs/GOAP_ClassDiagram.png new file mode 100644 index 0000000..c934888 Binary files /dev/null and b/Docs/GOAP_ClassDiagram.png differ diff --git a/Docs/GOAP_ClassDiagram.png.meta b/Docs/GOAP_ClassDiagram.png.meta new file mode 100644 index 0000000..5a38049 --- /dev/null +++ b/Docs/GOAP_ClassDiagram.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 92ca0d3f08666f3418e4b9b5316c70ec +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Docs/GOAP_ClassDiagram.puml b/Docs/GOAP_ClassDiagram.puml new file mode 100644 index 0000000..d6a9fdd --- /dev/null +++ b/Docs/GOAP_ClassDiagram.puml @@ -0,0 +1,63 @@ +@startuml + +interface GoapValueInterface { + +Type type +} + +class GoapState { + -Dictionary values + +GoapValueInterface GetValue(string stateIndex) + +void SetValue(string stateIndex, GoapValueInterface value) + +GoapState Clone() +} +GoapState o-- GoapValueInterface : consists of > + +interface ConditionInterface { + +bool IsSatisfied(GoapState state) + +double EstimateCost(GoapState state, Dictionary costPerDiffes) +} +ConditionInterface ..> GoapState : reads > + +interface StateDiffInterface { + +string stateIndex + +GoapState Operate(GoapState state, bool overwrite) + +double diff +} +StateDiffInterface --> GoapState : operates on > + +class StateDiffSet { + -StateDiffInterface[] stateDiffs + +GoapState Apply(GoapState state, bool overwrite) +} +StateDiffSet o-- StateDiffInterface : delegates to > + +class GoapAction { + +string name + +ConditionInterface condition + +StateDiffSet stateDiffSet + +double cost + +bool IsAvailable(GoapState state) + +GoapState Simulate(GoapState state, bool overwrite) +} +GoapAction *-- ConditionInterface : delegates to > +GoapAction *-- StateDiffSet : delegates to > + +class GoapResult { + +GoapAction[] actions + +double cost + +int length + +bool success +} +GoapResult o-- GoapAction : has > + +class GoapSolver { + -GoapAction[] actionPool + +GoapResult Solve(GoapState stateCurrent, ConditionInterface goal, int maxLength) + +void AddAction(GoapAction action) +} +GoapSolver --> GoapResult : Generates > +GoapSolver --> GoapState : uses > +GoapSolver --> ConditionInterface : uses > +GoapSolver o-- GoapAction : stores > + +@enduml \ No newline at end of file diff --git a/Docs/GOAP_ClassDiagram.puml.meta b/Docs/GOAP_ClassDiagram.puml.meta new file mode 100644 index 0000000..572965c --- /dev/null +++ b/Docs/GOAP_ClassDiagram.puml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b2df6d4814bb41b4f9c06cc2d27bab46 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 8b13789..a050a70 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/b5fe6529e18f4843876976907689c563)](https://app.codacy.com/gh/tsunagi-ai/TsunagiModuleUnity/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) + +[![Maintainability](https://qlty.sh/badges/8bcac4c3-7aeb-4c26-b2e1-6aa8b3cf8843/maintainability.svg)](https://qlty.sh/gh/tsunagi-ai/projects/TsunagiModuleUnity) \ No newline at end of file diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..06d9a57 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 86de5bca44e6ae34db8903a877f0033a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts.meta b/Scripts.meta new file mode 100644 index 0000000..5a3d76f --- /dev/null +++ b/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c76e76e5b0e590a44875d223b3d70618 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap.meta b/Scripts/Goap.meta new file mode 100644 index 0000000..2fdcbd5 --- /dev/null +++ b/Scripts/Goap.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b78773f5467699b4bbc03c3a2be2451d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap/Conditions.meta b/Scripts/Goap/Conditions.meta new file mode 100644 index 0000000..409f5a2 --- /dev/null +++ b/Scripts/Goap/Conditions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 534b8e054d2b8e24fa9f772a7ec79866 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap/Conditions/Condition.cs b/Scripts/Goap/Conditions/Condition.cs new file mode 100644 index 0000000..50fd19b --- /dev/null +++ b/Scripts/Goap/Conditions/Condition.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + /// + /// Basic conditioning in the GOAP system. + /// Compares a state value with a target value using a specified operator. + /// + /// + /// Available conditioning method is listed in the enum. + /// + /// + /// This means (> 5) + /// + /// new Condition("correspondingindex", ConditionOperator.Greater, 5) + /// + /// + /// This must be the same type as the value in the State. + public struct Condition : ConditionInterface + where T : struct, IEquatable + { + /// + /// The index of the state value to compare. + /// + public string stateIndex { get; private set; } + + /// + /// The value to compare against. + /// + public T valueComparing { get; set; } + + /// + /// Conditioning method + /// + public ConditionOperator conditionOperator { get; set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The index of the state value to compare. + /// Conditioning method + /// Value to compare against. + public Condition(string stateIndex, ConditionOperator conditionOperator, T valueComparing) + { + this.stateIndex = stateIndex; + this.valueComparing = valueComparing; + this.conditionOperator = conditionOperator; + } + + /// + /// Determines whether the condition is satisfied given the current state. + /// + /// The current GOAP state. This class will read the state value associated with the stateIndex. + /// True if the condition is satisfied; otherwise, false. + /// Thrown when the state value type does not match the expected type. + public bool IsSatisfied(GoapState state) + { + GoapValueInterface valueGivenInterface = state.GetValue(stateIndex); + + // if type check passed... + if (valueGivenInterface is GoapValue valueGiven) + { + // ...compare value + return Compare(valueGiven.value, valueComparing); + } + // if different type... + else + { + // ...panic + throw new InvalidCastException( + $"State index '{stateIndex}' is not of type '{typeof(T)}'." + ); + } + } + + /// + /// Estimates the cost of satisfying the condition given the current state. + /// + /// + /// If boolean, the diff is set as 1.0. + /// If numerical (), the diff is the absolute difference of them. + /// If others, the diff assumed to be 1.0 if not equal. + /// + /// The current GOAP state. This class will read the state value associated with the stateIndex. + /// Optional dictionary of costs per state difference. If null, this will assume as 1.0 + /// The estimated cost of satisfying the condition. + /// Thrown when the state value type does not match the expected type. + public double EstimateCost(GoapState state, Dictionary costPerDiffes) + { + // if already satisfied... + if (IsSatisfied(state)) + { + // ... early return + return 0.0; + } + + // not satisfied! + + // get weight + double costPerDiff = 1.0; + // if corresponding weight given... + if ( + (costPerDiffes != null) + && costPerDiffes.TryGetValue(stateIndex, out double weightFound) + ) + { + // ...use it + costPerDiff = weightFound; + } + + // compute distance + double distance = 0.0; + GoapValueInterface valueGivenInterface = state.GetValue(stateIndex); + // if type check passed... + if (valueGivenInterface is GoapValue valueGiven) + { + // bool + if (valueGiven.value is bool) + { + distance = 1.0; + } + // numeric + else if (valueGiven.value is IConvertible) + { + // convert to double + double valueGivenDouble = Convert.ToDouble(valueGiven.value); + double valueComparingDouble = Convert.ToDouble(valueComparing); + + // compute distance + distance = Math.Abs(valueGivenDouble - valueComparingDouble); + } + // unknown + else + { + distance = 1.0; + } + } + // if different type... + else + { + // ...panic + throw new InvalidCastException( + $"State index '{stateIndex}' is not of type '{typeof(T)}'." + ); + } + + return distance * costPerDiff; + } + + /// + /// Compares the given value with the target value using the specified operator. + /// + /// + /// This function routes to corresponding comparison method. + /// + /// The value to compare. + /// The target value to compare against. + /// True if the comparison is successful; otherwise, false. + /// Thrown when the operator is not supported for the value type. + /// Thrown when the operator is not implemented. + private bool Compare(T valueGiven, T valueComparing) + { + // delegate to corresponding comparison method + switch (conditionOperator) + { + case ConditionOperator.Equal: + case ConditionOperator.NotEqual: + // if operation available... + if (valueGiven is IEquatable valueGivenEquatable) + { + // ...compare value + return CompareEquatable(valueGivenEquatable, valueComparing); + } + else + { + // ...panic + throw new InvalidOperationException( + $"Condition operator '{conditionOperator}' is not supported for type '{typeof(T)}'." + ); + } + case ConditionOperator.Greater: + case ConditionOperator.GreaterOrEqual: + case ConditionOperator.Less: + case ConditionOperator.LessOrEqual: + // if operation available... + if (valueGiven is IComparable valueGivenComparable) + { + // ...compare value + return CompareComparable(valueGivenComparable, valueComparing); + } + else + { + // ...panic + throw new InvalidOperationException( + $"Condition operator '{conditionOperator}' is not supported for type '{typeof(T)}'." + ); + } + + // unknown + default: + // panic + throw new NotImplementedException( + $"Condition operator '{conditionOperator}' not implemented yet." + ); + } + } + + /// + /// Compares two values using equatable operators. + /// + /// The value to compare. + /// The target value to compare against. + /// True if the comparison is successful; otherwise, false. + /// Thrown when the operator is not implemented. + private bool CompareEquatable(IEquatable valueGivenEquatable, T valueComparing) + { + switch (conditionOperator) + { + case ConditionOperator.Equal: + return valueGivenEquatable.Equals(valueComparing); + case ConditionOperator.NotEqual: + return !valueGivenEquatable.Equals(valueComparing); + default: + throw new NotImplementedException( + $"Condition operator '{conditionOperator}' not implemented yet." + ); + } + } + + /// + /// Compares two values using comparable operators. + /// + /// The value to compare. + /// The target value to compare against. + /// True if the comparison is successful; otherwise, false. + /// Thrown when the operator is not implemented. + private bool CompareComparable(IComparable valueGivenComparable, T valueComparing) + { + // https://learn.microsoft.com/en-us/dotnet/api/system.icomparable?view=net-9.0 + // A.CompareTo(B) < 0 means A < B + // A.CompareTo(B) == 0 means A == B + // A.CompareTo(B) > 0 means A > B + + switch (conditionOperator) + { + case ConditionOperator.Greater: + return valueGivenComparable.CompareTo(valueComparing) > 0; + case ConditionOperator.GreaterOrEqual: + return valueGivenComparable.CompareTo(valueComparing) >= 0; + case ConditionOperator.Less: + return valueGivenComparable.CompareTo(valueComparing) < 0; + case ConditionOperator.LessOrEqual: + return valueGivenComparable.CompareTo(valueComparing) <= 0; + default: + throw new NotImplementedException( + $"Condition operator '{conditionOperator}' not implemented yet." + ); + } + } + } +} diff --git a/Scripts/Goap/Conditions/Condition.cs.meta b/Scripts/Goap/Conditions/Condition.cs.meta new file mode 100644 index 0000000..f2758a3 --- /dev/null +++ b/Scripts/Goap/Conditions/Condition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eaef8e2c64b14954aa8fce230e802c50 \ No newline at end of file diff --git a/Scripts/Goap/Conditions/ConditionAnd.cs b/Scripts/Goap/Conditions/ConditionAnd.cs new file mode 100644 index 0000000..f6828f7 --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionAnd.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + /// + /// Represents a logical AND condition composed of multiple sub-conditions. + /// + public struct ConditionAnd : ConditionInterface + { + /// + /// The array of sub-conditions that make up this AND condition. + /// + public ConditionInterface[] conditions { get; private set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The array of sub-conditions. + public ConditionAnd(ConditionInterface[] conditions) + { + this.conditions = conditions; + } + + /// + /// Determines whether all sub-conditions are satisfied given the current state. + /// + /// The current GOAP state. + /// True if all sub-conditions are satisfied; otherwise, false. + public bool IsSatisfied(GoapState state) + { + foreach (ConditionInterface condition in conditions) + { + if (!condition.IsSatisfied(state)) + { + return false; + } + } + return true; + } + + /// + /// Estimates the cost of satisfying this AND condition given the current state. + /// + /// + /// This method uses the square root of the sum of squares to estimate the cost of the sub-conditions + /// + /// The current GOAP state. + /// Optional dictionary of costs per state difference. + /// The estimated cost of satisfying the condition. + public double EstimateCost(GoapState state, Dictionary costPerDiffes) + { + // square root of sum of squares + double sum = 0; + foreach (ConditionInterface condition in conditions) + { + double cost = condition.EstimateCost(state, costPerDiffes: costPerDiffes); + sum += cost * cost; + } + return Math.Sqrt(sum); + } + } +} diff --git a/Scripts/Goap/Conditions/ConditionAnd.cs.meta b/Scripts/Goap/Conditions/ConditionAnd.cs.meta new file mode 100644 index 0000000..12d2b23 --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionAnd.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 657fef7846b91c7469be75587d22fdb0 \ No newline at end of file diff --git a/Scripts/Goap/Conditions/ConditionInterface.cs b/Scripts/Goap/Conditions/ConditionInterface.cs new file mode 100644 index 0000000..401e2f7 --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionInterface.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + /// + /// interface for conditions in the GOAP system. + /// + /// + /// If you want basical conditioning, see . + /// If you DONT want to use the default condition, see . + /// + public interface ConditionInterface + { + /// + /// Determines whether the condition is satisfied given the current state. + /// + /// The current GOAP state. + /// True if the condition is satisfied; otherwise, false. + public bool IsSatisfied(GoapState state); + + /// + /// Estimates the cost of satisfying the condition given the current state. + /// + /// The current GOAP state. + /// Optional dictionary of costs per state difference. + /// The estimated cost of satisfying the condition. + public double EstimateCost(GoapState state, Dictionary costPerDiffes); + } +} diff --git a/Scripts/Goap/Conditions/ConditionInterface.cs.meta b/Scripts/Goap/Conditions/ConditionInterface.cs.meta new file mode 100644 index 0000000..e1f6e9d --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionInterface.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 48cd3a5fd8565f94b8e162e3704b8098 \ No newline at end of file diff --git a/Scripts/Goap/Conditions/ConditionOperator.cs b/Scripts/Goap/Conditions/ConditionOperator.cs new file mode 100644 index 0000000..7d5dec2 --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionOperator.cs @@ -0,0 +1,38 @@ +namespace TsunagiModule.Goap +{ + /// + /// Defines the comparing method in + /// + public enum ConditionOperator + { + /// + /// StateValue > ConditionValue + /// + Greater, + + /// + /// StateValue >= ConditionValue + /// + GreaterOrEqual, + + /// + /// StateValue < ConditionValue + /// + Less, + + /// + /// StateValue <= ConditionValue + /// + LessOrEqual, + + /// + /// StateValue == ConditionValue + /// + Equal, + + /// + /// StateValue != ConditionValue + /// + NotEqual, + } +} diff --git a/Scripts/Goap/Conditions/ConditionOperator.cs.meta b/Scripts/Goap/Conditions/ConditionOperator.cs.meta new file mode 100644 index 0000000..6c6af6c --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionOperator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8698ddb9307e0094bae9fe705539db66 \ No newline at end of file diff --git a/Scripts/Goap/Conditions/ConditionOr.cs b/Scripts/Goap/Conditions/ConditionOr.cs new file mode 100644 index 0000000..e4a6d1d --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionOr.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + /// + /// Represents a logical OR condition composed of multiple sub-conditions. + /// + public struct ConditionOr : ConditionInterface + { + /// + /// The array of sub-conditions that make up this OR condition. + /// + public ConditionInterface[] conditions { get; private set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The array of sub-conditions. + public ConditionOr(ConditionInterface[] conditions) + { + this.conditions = conditions; + } + + /// + /// Determines whether any of the sub-conditions are satisfied given the current state. + /// + /// The current state. + /// True if any sub-condition is satisfied; otherwise, false. + public bool IsSatisfied(GoapState state) + { + foreach (ConditionInterface condition in conditions) + { + if (condition.IsSatisfied(state)) + { + return true; + } + } + return false; + } + + /// + /// Estimates the cost of satisfying this OR condition given the current state. + /// + /// + /// This method uses the minimum cost of the sub-conditions to estimate the cost of satisfying the OR condition. + /// + /// The current state. + /// Optional dictionary of costs per state difference. + /// The estimated cost of satisfying the condition. + public double EstimateCost(GoapState state, Dictionary costPerDiffes) + { + double min = double.MaxValue; + foreach (ConditionInterface condition in conditions) + { + double cost = condition.EstimateCost(state, costPerDiffes: costPerDiffes); + if (cost < min) + { + min = cost; + } + } + return min; + } + } +} diff --git a/Scripts/Goap/Conditions/ConditionOr.cs.meta b/Scripts/Goap/Conditions/ConditionOr.cs.meta new file mode 100644 index 0000000..7a0abd0 --- /dev/null +++ b/Scripts/Goap/Conditions/ConditionOr.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 93c5b8c5c190ebf4cb1c67977c6b7e31 \ No newline at end of file diff --git a/Scripts/Goap/Conditions/NoCondition.cs b/Scripts/Goap/Conditions/NoCondition.cs new file mode 100644 index 0000000..da5bf4d --- /dev/null +++ b/Scripts/Goap/Conditions/NoCondition.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + /// + /// Represents a condition that is always satisfied + /// + public struct NoCondition : ConditionInterface + { + /// + /// Determines whether the condition is satisfied. + /// + /// The current GOAP state. + /// Always returns true. + public bool IsSatisfied(GoapState state) + { + return true; + } + + /// + /// Estimates the cost of satisfying the condition. + /// + /// + /// this is always 0 because the condition is always satisfied. + /// + /// The current GOAP state. + /// Optional dictionary of costs per state difference. + /// Always returns 0. + public double EstimateCost(GoapState state, Dictionary costPerDiffes) + { + return 0; + } + } +} diff --git a/Scripts/Goap/Conditions/NoCondition.cs.meta b/Scripts/Goap/Conditions/NoCondition.cs.meta new file mode 100644 index 0000000..188ccdb --- /dev/null +++ b/Scripts/Goap/Conditions/NoCondition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fa2a2ec7aa07b9542948720f4f4f2e5b \ No newline at end of file diff --git a/Scripts/Goap/GoapAction.cs b/Scripts/Goap/GoapAction.cs new file mode 100644 index 0000000..98e4de4 --- /dev/null +++ b/Scripts/Goap/GoapAction.cs @@ -0,0 +1,105 @@ +namespace TsunagiModule.Goap +{ + /// + /// Represents an action in the GOAP system. + /// + /// + /// This contains the condition and effect of the action. + /// The will conduct a pathfinding according to these conditions and effects. + /// + public struct GoapAction + { + /// + /// The name of the action. + /// + public string name { get; private set; } + + /// + /// The condition that must be satisfied for the action to be available. + /// + public ConditionInterface condition { get; private set; } + + /// + /// The set of state differences that this action applies. + /// + public StateDiffSet stateDiffSet { get; private set; } + + /// + /// The cost of performing this action. + /// + public double cost { get; private set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The name of the action. + /// The condition for the action. + /// The state differences applied by the action. + /// The cost of the action. + public GoapAction( + string name, + ConditionInterface condition, + StateDiffSet stateDiffSet, + double cost + ) + { + this.name = name; + this.condition = condition; + this.stateDiffSet = stateDiffSet; + this.cost = cost; + } + + /// + /// Initializes a new instance of the struct with an array of state differences. + /// + /// The name of the action. + /// The condition for the action. + /// The array of state differences applied by the action. + /// The cost of the action. + public GoapAction( + string name, + ConditionInterface condition, + StateDiffInterface[] stateDiffSet, + double cost + ) + { + this.name = name; + this.condition = condition; + this.stateDiffSet = new StateDiffSet(stateDiffSet); + this.cost = cost; + } + + /// + /// Determines whether the action is available given the current state. + /// + /// The current state. + /// True if the action is available; otherwise, false. + public bool IsAvailable(GoapState state) + { + return condition.IsSatisfied(state); + } + + /// + /// Simulates the application of the action on the given state. + /// + /// The current state. + /// Whether to overwrite the current state or clone it. + /// The resulting state after applying the action. + public GoapState Simulate(GoapState state, bool overwrite) + { + GoapState stateTarget; + if (overwrite) + { + stateTarget = state; + } + else + { + stateTarget = state.Clone(); + } + + stateTarget = stateDiffSet.Apply(stateTarget, overwrite); + + return stateTarget; + } + } +} diff --git a/Scripts/Goap/GoapAction.cs.meta b/Scripts/Goap/GoapAction.cs.meta new file mode 100644 index 0000000..6a31887 --- /dev/null +++ b/Scripts/Goap/GoapAction.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 76c4afcdeb4ace943a41e9f78b871aa5 \ No newline at end of file diff --git a/Scripts/Goap/GoapSolver.meta b/Scripts/Goap/GoapSolver.meta new file mode 100644 index 0000000..c33f273 --- /dev/null +++ b/Scripts/Goap/GoapSolver.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6be95c9a7ca333c4998dae17d848b073 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap/GoapSolver/GoapResult.cs b/Scripts/Goap/GoapSolver/GoapResult.cs new file mode 100644 index 0000000..c71d46e --- /dev/null +++ b/Scripts/Goap/GoapSolver/GoapResult.cs @@ -0,0 +1,44 @@ +namespace TsunagiModule.Goap +{ + /// + /// The result returned by the + /// + /// + /// You may want to check to check if the goal was achieved or not, before accessing + /// + public struct GoapResult + { + /// + /// The sequence of actions that lead to the goal. + /// + public GoapAction[] actions; + + /// + /// The total cost of the actions. + /// + public double cost; + + /// + /// Gets the number of actions in the result. + /// + public int length => actions.Length; + + /// + /// Indicates whether the goal was successfully achieved. + /// + public bool success; + + /// + /// Initializes a new instance of the struct. + /// + /// The sequence of actions that lead to the goal. + /// The total cost of the actions. + /// Indicates whether the goal was successfully achieved. + public GoapResult(GoapAction[] actions, double cost, bool success) + { + this.actions = actions; + this.cost = cost; + this.success = success; + } + } +} diff --git a/Scripts/Goap/GoapSolver/GoapResult.cs.meta b/Scripts/Goap/GoapSolver/GoapResult.cs.meta new file mode 100644 index 0000000..65c6a73 --- /dev/null +++ b/Scripts/Goap/GoapSolver/GoapResult.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 50aecb45af83a2c43a6638fd3ca3fd3f \ No newline at end of file diff --git a/Scripts/Goap/GoapSolver/GoapSolver_ActionControl.cs b/Scripts/Goap/GoapSolver/GoapSolver_ActionControl.cs new file mode 100644 index 0000000..cc78592 --- /dev/null +++ b/Scripts/Goap/GoapSolver/GoapSolver_ActionControl.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + public partial class GoapSolver + { + /// + /// Pool of actions. + /// + /// + /// Maps action names to their corresponding instances. + /// + private readonly Dictionary actionPool = + new Dictionary(); + + /// + /// Adds a new action to the action pool. + /// + /// The action to add. + public void AddAction(GoapAction action) + { + actionPool.Add(action.name, action); + } + + /// + /// Removes an action from the action pool by its name. + /// + /// The name of the action to remove. + public void RemoveAction(string name) + { + actionPool.Remove(name); + } + + /// + /// Replaces an existing action in the action pool with a new one. + /// + /// The name of the action to replace. + /// The new action to replace the existing one. + public void ReplaceAction(string name, GoapAction action) + { + actionPool[name] = action; + } + + /// + /// Clear all actions in the action pool. + /// + public void ClearActionPool() + { + actionPool.Clear(); + } + } +} diff --git a/Scripts/Goap/GoapSolver/GoapSolver_ActionControl.cs.meta b/Scripts/Goap/GoapSolver/GoapSolver_ActionControl.cs.meta new file mode 100644 index 0000000..7e5ab3c --- /dev/null +++ b/Scripts/Goap/GoapSolver/GoapSolver_ActionControl.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 196ccfb8bd4148346812874761d8c7c9 \ No newline at end of file diff --git a/Scripts/Goap/GoapSolver/GoapSolver_Pathfinding.cs b/Scripts/Goap/GoapSolver/GoapSolver_Pathfinding.cs new file mode 100644 index 0000000..5866fbc --- /dev/null +++ b/Scripts/Goap/GoapSolver/GoapSolver_Pathfinding.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using TsunagiModule.Goap.Utils; + +namespace TsunagiModule.Goap +{ + /// + /// Implements the A* pathfinding algorithm for the GOAP solver. + /// + public partial class GoapSolver + { + /// + /// Represents a node in the A* search queue. + /// + private class AstarQueue : IComparable + { + /// + /// simulated State + /// + public GoapState state; + + /// + /// previous queue + /// + public AstarQueue parent; + public GoapAction? action; + public double currentCost; + public double estimatedCost; + public double totalCost => currentCost + estimatedCost; + public int depth => parent == null ? 0 : parent.depth + 1; + + public AstarQueue( + GoapState state, + AstarQueue parent, + double currentCost, + double estimatedCost, + GoapAction? action + ) + { + this.state = state; + this.parent = parent; + this.currentCost = currentCost; + this.estimatedCost = estimatedCost; + this.action = action; + } + + // for sorting + public int CompareTo(AstarQueue other) + { + if (totalCost < other.totalCost) + { + return -1; + } + + if (totalCost > other.totalCost) + { + return 1; + } + + return 0; + } + } + + /// + /// Cost estimation weights for each state index. + /// This is simply multipied with the diff from the goal of State + /// + private Dictionary costPerDiffes = new Dictionary(); + + /// + /// Solves the GOAP problem using the A* algorithm. + /// + /// The current state. + /// The goal condition to satisfy. + /// The maximum depth of the search tree. + /// If the search exceeds this depth, it will terminate early. + /// The result of the GOAP problem-solving process. + public GoapResult Solve(GoapState stateCurrent, ConditionInterface goal, int maxLength) + { + // compute cost weights + costPerDiffes = ComputeCostWeights(stateCurrent); + + return SolveAstar(stateCurrent, goal, maxLength); + } + + /// + /// Computes the cost weights for each state index based on the current state. + /// + /// + /// This assume that the weight should be the largest costPerDiff about each State values + /// + /// The current state. + /// A dictionary mapping state indices to their cost weights. + private Dictionary ComputeCostWeights(GoapState stateCurrent) + { + // state index -> cost per diff + Dictionary largestCostPerDiff = new Dictionary(); + foreach (string stateIndex in stateCurrent.indices) + { + largestCostPerDiff[stateIndex] = 0.0; + } + + foreach (GoapAction action in actionPool.Values) + { + foreach (StateDiffInterface stateDiff in action.stateDiffSet.stateDiffes) + { + // get cost per diff + double costPerDiff = Math.Abs(action.cost / stateDiff.diff); + if (largestCostPerDiff[stateDiff.stateIndex] < costPerDiff) + { + largestCostPerDiff[stateDiff.stateIndex] = costPerDiff; + } + } + } + return largestCostPerDiff; + } + + /// + /// Performs the A* search to solve the GOAP problem. + /// + /// The current state. + /// The goal condition to satisfy. + /// The maximum depth of the search tree. + /// The result of the A* search process. + /// + /// Implementation of A* Pathfinding + /// + private GoapResult SolveAstar(GoapState stateCurrent, ConditionInterface goal, int maxDepth) + { + PriorityQueue queue = new PriorityQueue(); + HashSet closedSet = new HashSet(); + + // starting point + queue.Enqueue( + new AstarQueue(stateCurrent, null, 0, EstimateCost(stateCurrent, goal), null) + ); + + while (queue.Count > 0) + { + // to next node + AstarQueue current = queue.Dequeue(); + + // if already closed... + if (closedSet.Contains(current.state)) + { + // ...skip + continue; + } + + // if arrived at goal... + if (goal.IsSatisfied(current.state)) + { + // ...early return result + return CreateSuccessResult(current); + } + + // close the current node + closedSet.Add(current.state); + + // if not arrived to the max depth... + if (current.depth < maxDepth) + { + // ...enqueue next nodes + EnqueueNextActions(queue, current, goal); + } + } + + return CreateFailureResult(); + } + + private void EnqueueNextActions( + PriorityQueue queue, + AstarQueue current, + ConditionInterface goal + ) + { + // ...find next nodes + foreach (GoapAction action in actionPool.Values) + { + // if the action is available... + if (action.IsAvailable(current.state)) + { + // ...go to this state + GoapState nextState = action.Simulate(current.state, false); + double costCurrent = current.currentCost + action.cost; + double costEstimated = EstimateCost(nextState, goal); + queue.Enqueue( + new AstarQueue(nextState, current, costCurrent, costEstimated, action) + ); + } + } + } + + private GoapResult CreateSuccessResult(AstarQueue latestQueue) + { + // action list + List actions = new List(); + while (latestQueue.action != null) + { + actions.Add(latestQueue.action ?? throw new InvalidOperationException()); + latestQueue = latestQueue.parent; + } + actions.Reverse(); + + // create GoapResult + return new GoapResult(actions.ToArray(), latestQueue.currentCost, true); + } + + private GoapResult CreateFailureResult() + { + return new GoapResult(null, -1, false); + } + + /// + /// Estimates the cost to satisfy the goal condition from the given state. + /// + /// The current state. + /// The goal condition to satisfy. + /// The estimated cost to satisfy the goal condition. + private double EstimateCost(GoapState state, ConditionInterface goal) + { + return goal.EstimateCost(state, costPerDiffes: costPerDiffes); + } + } +} diff --git a/Scripts/Goap/GoapSolver/GoapSolver_Pathfinding.cs.meta b/Scripts/Goap/GoapSolver/GoapSolver_Pathfinding.cs.meta new file mode 100644 index 0000000..ad0635e --- /dev/null +++ b/Scripts/Goap/GoapSolver/GoapSolver_Pathfinding.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b3884298cf1091944be3fc4122477e39 \ No newline at end of file diff --git a/Scripts/Goap/GoapState.cs b/Scripts/Goap/GoapState.cs new file mode 100644 index 0000000..9114317 --- /dev/null +++ b/Scripts/Goap/GoapState.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TsunagiModule.Goap +{ + /// + /// Represents the state in the GOAP (Goal-Oriented Action Planning) system. + /// + /// + /// You need to write what is happening in the game world into this . + /// + public struct GoapState : IEquatable + { + /// + /// The main body of the state vector, storing state values by their indices. + /// + private Dictionary values { get; set; } + + /// + /// Gets the indices of all state values. + /// + public string[] indices => values.Keys.ToArray(); + + /// + /// Initializes a new instance of the struct. + /// + /// The dictionary of state values. + public GoapState(Dictionary values) + { + this.values = values; + } + + /// + /// Retrieves the value associated with the specified state index. + /// + /// The index of the state value to retrieve. + /// The value associated with the specified state index. + /// Thrown when the specified state index is not found. + public GoapValueInterface GetValue(string stateIndex) + { + GuardValueNull(); + + if (values.TryGetValue(stateIndex, out GoapValueInterface value)) + { + return value; + } + else + { + throw new KeyNotFoundException($"State index '{stateIndex}' not found."); + } + } + + /// + /// Sets the value for the specified state index. + /// + /// The index of the state value to set. + /// The value to set. + public void SetValue(string stateIndex, GoapValueInterface value) + { + GuardValueNull(); + + values[stateIndex] = value; + } + + /// + /// Sets the raw value for the specified state index. + /// + /// The type of the value. + /// The index of the state value to set. + /// The raw value to set. + public void SetRawValue(string stateIndex, T value) + where T : struct, IEquatable + { + // wrapping + SetValue(stateIndex, new GoapValue(value)); + } + + /// + /// Determines whether the current state is equal to another state. + /// + /// The other state to compare with. + /// True if the states are equal; otherwise, false. + public bool Equals(GoapState other) + { + GuardValueNull(); + + // HACK: there could be a better data structure + // since length of dictionary tends not to be too large, + // this is fast enough. + return values.SequenceEqual(other.values); + } + + /// + /// Returns the hash code for the current state. + /// + /// The hash code for the current state. + public override int GetHashCode() + { + // HACK: there could be a better data structure + // since length of dictionary tends not to be too large, + // hash collision is not likely to happen. + + int hash = 17; + foreach (var pair in values) + { + hash = hash * 31 + pair.Key.GetHashCode(); + hash = hash * 31 + pair.Value.GetHashCode(); + } + return hash; + } + + /// + /// Creates a clone of the current state. + /// + /// A new instance of that is a clone of the current state. + public GoapState Clone() + { + // HACK: there could be a better data structure + return new GoapState { values = new Dictionary(values) }; + } + + /// + /// Ensures that the state values dictionary is not null. + /// + private void GuardValueNull() + { + if (values == null) + { + values = new Dictionary(); + } + } + } +} diff --git a/Scripts/Goap/GoapState.cs.meta b/Scripts/Goap/GoapState.cs.meta new file mode 100644 index 0000000..7d5e541 --- /dev/null +++ b/Scripts/Goap/GoapState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8304c669d816bfa48894f5f58923ec1d \ No newline at end of file diff --git a/Scripts/Goap/GoapValue.meta b/Scripts/Goap/GoapValue.meta new file mode 100644 index 0000000..071704c --- /dev/null +++ b/Scripts/Goap/GoapValue.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72c7755c0bd64224d8419621fb42a70b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap/GoapValue/GoapValue.cs b/Scripts/Goap/GoapValue/GoapValue.cs new file mode 100644 index 0000000..2ec52aa --- /dev/null +++ b/Scripts/Goap/GoapValue/GoapValue.cs @@ -0,0 +1,63 @@ +using System; + +namespace TsunagiModule.Goap +{ + /// + /// Value used in Goap system. + /// + /// + /// This is implemented for State supporting as much type as possible.
+ /// You can use:
+ /// - + /// - + /// - + /// - + ///
is recommended instead of for accurate pathfinding. + ///
+ /// The type of the value. Must be a struct and implement . + /// + public struct GoapValue : GoapValueInterface, IEquatable> + where T : struct, IEquatable + { + /// + /// Gets the type of the GOAP value. + /// + public Type type => typeof(T); + + /// + /// Gets or sets the value. + /// + public T value { get; set; } + + /// + /// Initializes a new instance of the struct. + /// + /// The value to initialize with. + public GoapValue(T value) + { + this.value = value; + } + + /// + /// Determines whether the current value is equal to another value of the same type. + /// + /// The other value to compare with. + /// True if the values are equal; otherwise, false. + public bool Equals(GoapValue other) + { + return value.Equals(other.value); + } + + /// + /// Returns the hash code for the current value. + /// + /// + /// the GetHashCode() of the original value should be implemented + /// + /// The hash code for the current value. + public override int GetHashCode() + { + return value.GetHashCode(); + } + } +} diff --git a/Scripts/Goap/GoapValue/GoapValue.cs.meta b/Scripts/Goap/GoapValue/GoapValue.cs.meta new file mode 100644 index 0000000..4e35e38 --- /dev/null +++ b/Scripts/Goap/GoapValue/GoapValue.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 67e1b44ee3d9b5d49b1689e029dd3c54 \ No newline at end of file diff --git a/Scripts/Goap/GoapValue/GoapValueInterface.cs b/Scripts/Goap/GoapValue/GoapValueInterface.cs new file mode 100644 index 0000000..2a8bbbe --- /dev/null +++ b/Scripts/Goap/GoapValue/GoapValueInterface.cs @@ -0,0 +1,19 @@ +using System; + +namespace TsunagiModule.Goap +{ + /// + /// Defines an interface for GOAP value types. + /// + /// + /// You may want to use for real instance. + /// This is implemented for State supporting as much type as possible. + /// + public interface GoapValueInterface + { + /// + /// Gets the type of the GOAP value. + /// + public Type type { get; } + } +} diff --git a/Scripts/Goap/GoapValue/GoapValueInterface.cs.meta b/Scripts/Goap/GoapValue/GoapValueInterface.cs.meta new file mode 100644 index 0000000..515a1be --- /dev/null +++ b/Scripts/Goap/GoapValue/GoapValueInterface.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 05a9d8f79c779834a9097c7825fc5ad0 \ No newline at end of file diff --git a/Scripts/Goap/StateDiff.meta b/Scripts/Goap/StateDiff.meta new file mode 100644 index 0000000..3b48dd8 --- /dev/null +++ b/Scripts/Goap/StateDiff.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b22791e46cd4ba540b4df40099f6be66 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap/StateDiff/StateDiffAddition.cs b/Scripts/Goap/StateDiff/StateDiffAddition.cs new file mode 100644 index 0000000..004ac16 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffAddition.cs @@ -0,0 +1,100 @@ +using System; + +namespace TsunagiModule.Goap +{ + /// + /// Represents an addition operation for state differences in the GOAP system. + /// + /// + /// This is example of adding 3 at index0 + /// + /// new StateDiffAddition("index0", 1) + /// + /// + /// The type of the state values. Must be a struct and implement , , and . + public struct StateDiffAddition : StateDiffInterface + where T : struct, IConvertible, IComparable, IComparable, IEquatable + { + /// + /// The index of the state to which this addition applies. + /// + public string stateIndex { get; private set; } + + /// + /// The value to be added to the state. + /// + public T additionValue { get; set; } + + /// + /// Gets the difference value associated with this addition operation. + /// + public double diff => Convert.ToDouble(additionValue); + + /// + /// Initializes a new instance of the struct. + /// + /// The index of the state to which this addition applies. + /// The value to be added to the state. + public StateDiffAddition(string stateIndex, T additionValue) + { + this.stateIndex = stateIndex; + this.additionValue = additionValue; + } + + /// + /// Applies the addition operation to the given GOAP state. + /// + /// The GOAP state to apply the addition to. + /// Whether to overwrite the current state or clone it. + /// The resulting state after applying the addition. + /// Thrown when there is a type mismatch between the addition operation and the state value type. + public GoapState Operate(GoapState state, bool overwrite) + { + // cloning or not + GoapState stateTarget; + if (overwrite) + { + stateTarget = state; + } + else + { + stateTarget = state.Clone(); + } + + GoapValueInterface targetValueInterface = stateTarget.GetValue(stateIndex); + + // if both are same type... + if (targetValueInterface is GoapValue targetValue) + { + // ...operate addtion + + // compute value + double newValueDouble = + Convert.ToDouble(targetValue.value) + Convert.ToDouble(additionValue); + + // type converting + T newValue; + if (typeof(T) == typeof(int)) + { + // int support + newValue = (T)Convert.ChangeType(Math.Round(newValueDouble), typeof(T)); + } + else + { + newValue = (T)Convert.ChangeType(newValueDouble, typeof(T)); + } + + // update state + stateTarget.SetRawValue(stateIndex, newValue); + return stateTarget; + } + else + { + // ...panic + throw new ArgumentException( + "StateDiffAddition: Type mismatch in addition operation and State value type." + ); + } + } + } +} diff --git a/Scripts/Goap/StateDiff/StateDiffAddition.cs.meta b/Scripts/Goap/StateDiff/StateDiffAddition.cs.meta new file mode 100644 index 0000000..e8a9124 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffAddition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b196d756c04ec7841956ad88e971131d \ No newline at end of file diff --git a/Scripts/Goap/StateDiff/StateDiffInterface.cs b/Scripts/Goap/StateDiff/StateDiffInterface.cs new file mode 100644 index 0000000..fd3086f --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffInterface.cs @@ -0,0 +1,29 @@ +namespace TsunagiModule.Goap +{ + /// + /// Defines an interface for state difference operations in the GOAP system. + /// + public interface StateDiffInterface + { + /// + /// Gets the index of the state to which this difference applies. + /// + public string stateIndex { get; } + + /// + /// Applies the state difference operation to the given GOAP state. + /// + /// The GOAP state to apply the difference to. + /// Whether to overwrite the current state or clone it. + /// The resulting state after applying the difference. + public GoapState Operate(GoapState state, bool overwrite); + + /// + /// Gets the difference value associated with this operation. + /// + /// + /// This is used to estimate cost weight by + /// + public double diff { get; } + } +} diff --git a/Scripts/Goap/StateDiff/StateDiffInterface.cs.meta b/Scripts/Goap/StateDiff/StateDiffInterface.cs.meta new file mode 100644 index 0000000..aa50fd3 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffInterface.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0f8ac22e296f4f24496ce84a48755804 \ No newline at end of file diff --git a/Scripts/Goap/StateDiff/StateDiffMapping.cs b/Scripts/Goap/StateDiff/StateDiffMapping.cs new file mode 100644 index 0000000..7244dc8 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffMapping.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; + +namespace TsunagiModule.Goap +{ + /// + /// Represents a mapping operation for state differences in the GOAP system. + /// + /// + /// the State value is replaced according to the given mapping dictionary. + /// The unwritten state value in the dictionary will not be changed. + /// + /// The type of the state values. Must be a struct and implement . + public struct StateDiffMapping : StateDiffInterface + where T : struct, IEquatable + { + /// + /// The index of the state to which this mapping applies. + /// + public string stateIndex { get; private set; } + + /// + /// The dictionary defining the mapping of state values. + /// + public Dictionary mapping { get; set; } + + /// + /// Gets the difference value for this mapping operation. + /// + public double diff => 1.0; + + /// + /// Initializes a new instance of the struct. + /// + /// The index of the state to which this mapping applies. + /// The dictionary defining the mapping of state values. + public StateDiffMapping(string stateIndex, Dictionary mapping) + { + this.stateIndex = stateIndex; + this.mapping = mapping; + } + + /// + /// Applies the mapping operation to the given GOAP state. + /// + /// The GOAP state to apply the mapping to. + /// Whether to overwrite the current state or clone it. + /// The resulting state after applying the mapping. + /// Thrown when there is a type mismatch between the mapping operation and the state value type. + public GoapState Operate(GoapState state, bool overwrite) + { + // cloning or not + GoapState stateTarget; + if (overwrite) + { + stateTarget = state; + } + else + { + stateTarget = state.Clone(); + } + + GoapValueInterface targetValueInterface = stateTarget.GetValue(stateIndex); + + // if both are same type... + if (targetValueInterface is GoapValue targetValue) + { + // ...operate addtion + + T currentValue = targetValue.value; + + // if the current value is in the map... + if (mapping.TryGetValue(currentValue, out T value)) + { + // ...update the state with the mapped value + stateTarget.SetRawValue(stateIndex, value); + } + // if there isn't... + else + { + // ...do nothing + } + + return stateTarget; + } + else + { + // ...panic + throw new ArgumentException( + "StateDiffMapping: Type mismatch in mapping operation and State value type." + ); + } + } + } +} diff --git a/Scripts/Goap/StateDiff/StateDiffMapping.cs.meta b/Scripts/Goap/StateDiff/StateDiffMapping.cs.meta new file mode 100644 index 0000000..a1f8aa0 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffMapping.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 33fc616dabc4fdf4b9ba1d95d7d1fde1 \ No newline at end of file diff --git a/Scripts/Goap/StateDiff/StateDiffSet.cs b/Scripts/Goap/StateDiff/StateDiffSet.cs new file mode 100644 index 0000000..4ba9731 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffSet.cs @@ -0,0 +1,50 @@ +namespace TsunagiModule.Goap +{ + /// + /// Represents a collection of state differences and provides functionality to apply them to a GOAP state. + /// + public struct StateDiffSet + { + /// + /// The array of state differences in this set. + /// + public StateDiffInterface[] stateDiffes; + + /// + /// Initializes a new instance of the struct. + /// + /// The array of state differences. + public StateDiffSet(StateDiffInterface[] stateDiffes) + { + this.stateDiffes = stateDiffes; + } + + /// + /// Applies all state differences in this set to the given GOAP state. + /// + /// The GOAP state to apply the differences to. + /// Whether to overwrite the current state or clone it. + /// The resulting state after applying the differences. + public GoapState Apply(GoapState state, bool overwrite) + { + // cloning or not + GoapState stateTarget; + if (overwrite) + { + stateTarget = state; + } + else + { + stateTarget = state.Clone(); + } + + // apply all operations + foreach (StateDiffInterface stateDiff in stateDiffes) + { + stateDiff.Operate(stateTarget, true); + } + + return stateTarget; + } + } +} diff --git a/Scripts/Goap/StateDiff/StateDiffSet.cs.meta b/Scripts/Goap/StateDiff/StateDiffSet.cs.meta new file mode 100644 index 0000000..256faf5 --- /dev/null +++ b/Scripts/Goap/StateDiff/StateDiffSet.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 788968bbd4bf8cc42889f60828dbb29b \ No newline at end of file diff --git a/Scripts/Goap/Utils.meta b/Scripts/Goap/Utils.meta new file mode 100644 index 0000000..f614288 --- /dev/null +++ b/Scripts/Goap/Utils.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 97249b122072eaa48a0c498015dbc899 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Goap/Utils/PriorityQueue.cs b/Scripts/Goap/Utils/PriorityQueue.cs new file mode 100644 index 0000000..51a0461 --- /dev/null +++ b/Scripts/Goap/Utils/PriorityQueue.cs @@ -0,0 +1,104 @@ +// This entire file is written by GitHub Copilot +using System; +using System.Collections.Generic; + +namespace TsunagiModule.Goap.Utils +{ + /// + /// Represents a generic priority queue that organizes elements based on their priority. + /// + /// The type of elements in the priority queue. Must implement . + /// + public class PriorityQueue + where T : IComparable + { + /// + /// The list of elements in the priority queue. + /// + private readonly List elements = new List(); + + /// + /// Gets the number of elements in the priority queue. + /// + public int Count => elements.Count; + + /// + /// Adds an element to the priority queue. + /// + /// The element to add. + public void Enqueue(T item) + { + elements.Add(item); + int childIndex = elements.Count - 1; + while (childIndex > 0) + { + int parentIndex = (childIndex - 1) / 2; + if (elements[childIndex].CompareTo(elements[parentIndex]) >= 0) + { + break; + } + + T temp = elements[childIndex]; + elements[childIndex] = elements[parentIndex]; + elements[parentIndex] = temp; + + childIndex = parentIndex; + } + } + + /// + /// Removes and returns the element with the highest priority from the priority queue. + /// + /// The element with the highest priority. + /// Thrown when the priority queue is empty. + public T Dequeue() + { + if (elements.Count == 0) + { + throw new InvalidOperationException("The priority queue is empty."); + } + + T result = elements[0]; + int lastIndex = elements.Count - 1; + elements[0] = elements[lastIndex]; + elements.RemoveAt(lastIndex); + + int parentIndex = 0; + while (true) + { + int leftChildIndex = 2 * parentIndex + 1; + int rightChildIndex = 2 * parentIndex + 2; + int smallestIndex = parentIndex; + + if ( + leftChildIndex < elements.Count + && elements[leftChildIndex].CompareTo(elements[smallestIndex]) < 0 + ) + { + smallestIndex = leftChildIndex; + } + + if ( + rightChildIndex < elements.Count + && elements[rightChildIndex].CompareTo(elements[smallestIndex]) < 0 + ) + { + smallestIndex = rightChildIndex; + } + + if (smallestIndex == parentIndex) + { + break; + } + + T temp = elements[parentIndex]; + elements[parentIndex] = elements[smallestIndex]; + elements[smallestIndex] = temp; + + parentIndex = smallestIndex; + } + + return result; + } + } +} diff --git a/Scripts/Goap/Utils/PriorityQueue.cs.meta b/Scripts/Goap/Utils/PriorityQueue.cs.meta new file mode 100644 index 0000000..d903722 --- /dev/null +++ b/Scripts/Goap/Utils/PriorityQueue.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 101d9294a56e00e43bbfa6afc096e367 \ No newline at end of file diff --git a/Tests.asmdef b/Tests.asmdef new file mode 100644 index 0000000..02091dd --- /dev/null +++ b/Tests.asmdef @@ -0,0 +1,23 @@ +{ + "name": "Tests", + "rootNamespace": "", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests.asmdef.meta b/Tests.asmdef.meta new file mode 100644 index 0000000..656e015 --- /dev/null +++ b/Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c6cce9f195ef6c479540225cff2ec90 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..3c9590c --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c67e08ee36fbe4f41a957c7694801f98 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/GoapTest.cs b/Tests/GoapTest.cs new file mode 100644 index 0000000..5cab34a --- /dev/null +++ b/Tests/GoapTest.cs @@ -0,0 +1,216 @@ +using System.Collections.Generic; +using NUnit.Framework; +using TsunagiModule.Goap; + +/// +/// Contains unit tests for the GOAP (Goal-Oriented Action Planning) system. +/// +public class GoapTest +{ + /// + /// Constant representing the name of the first action. + /// + private const string ACTION_1 = "#1 increase int"; + + /// + /// Constant representing the name of the second action. + /// + private const string ACTION_2 = "#2 increase float"; + + /// + /// Constant representing the name of the third action. + /// + private const string ACTION_3 = "#3 increase double and switch boolean"; + + /// + /// Tests a simple GOAP scenario. + /// + [Test] + public void GoapTestSimple() + { + Condition goal = new Condition("int", ConditionOperator.GreaterOrEqual, 2); + GoapState state = GenerateState(); + GoapSolver solver = GenerateSolverWithActionPool(); + + //run + GoapResult result = solver.Solve(state, goal, 10); + + // #1 -> #1 + Assert.AreEqual(true, result.success); + Assert.AreEqual(2, result.length); + Assert.AreEqual(ACTION_1, result.actions[0].name); + Assert.AreEqual(ACTION_1, result.actions[1].name); + } + + /// + /// Tests the pathfinding capabilities of the GOAP solver. + /// + [Test] + public void GoapTestPathFinding() + { + Condition goal = new Condition( + "float", + ConditionOperator.GreaterOrEqual, + 0.5f + ); + + GoapState state = GenerateState(); + GoapSolver solver = GenerateSolverWithActionPool(); + + //run + GoapResult result = solver.Solve(state, goal, 10); + + // #1 -> #1 -> #1 -> #2 + Assert.AreEqual(true, result.success); + Assert.AreEqual(4, result.length); + Assert.AreEqual(ACTION_1, result.actions[0].name); + Assert.AreEqual(ACTION_1, result.actions[1].name); + Assert.AreEqual(ACTION_1, result.actions[2].name); + Assert.AreEqual(ACTION_2, result.actions[3].name); + } + + /// + /// Tests the mapping functionality of the GOAP solver. + /// + [Test] + public void GoapTestMapping() + { + ConditionInterface goal = new ConditionAnd( + new ConditionInterface[] + { + new Condition("double", ConditionOperator.GreaterOrEqual, 0.1), + new Condition("boolean", ConditionOperator.Equal, false) + } + ); + GoapState state = GenerateState(); + GoapSolver solver = GenerateSolverWithActionPool(); + + GoapResult result = solver.Solve(state, goal, 10); + + // #3 -> #3 + Assert.AreEqual(true, result.success); + Assert.AreEqual(2, result.length); + Assert.AreEqual(ACTION_3, result.actions[0].name); + Assert.AreEqual(ACTION_3, result.actions[1].name); + } + + /// + /// Tests the solver's ability to choose actions with better costs. + /// + [Test] + public void GoapTestBetterCost() + { + const string ACTION_X = "#X int + 100 (cost 100)"; + + Condition goal = new Condition("int", ConditionOperator.Equal, 3); + GoapState state = GenerateState(); + GoapSolver solver = GenerateSolverWithActionPool(); + solver.AddAction( + new GoapAction( + ACTION_X, + new NoCondition(), + new StateDiffInterface[] { new StateDiffAddition("int", 100) }, + 100.0 + ) + ); + + //run + GoapResult result = solver.Solve(state, goal, 10); + + // #1 -> #1 -> #1 + Assert.AreEqual(true, result.success); + Assert.AreEqual(3, result.length); + Assert.AreEqual(ACTION_1, result.actions[0].name); + Assert.AreEqual(ACTION_1, result.actions[1].name); + Assert.AreEqual(ACTION_1, result.actions[2].name); + } + + /// + /// Tests the solver's behavior when the goal depth is too deep. + /// + [Test] + public void GoapTestTooDeep() + { + Condition goal = new Condition("int", ConditionOperator.Equal, 100); + GoapState state = GenerateState(); + GoapSolver solver = GenerateSolverWithActionPool(); + + //run + GoapResult result = solver.Solve(state, goal, 10); + + // error + Assert.AreEqual(false, result.success); + } + + /// + /// Tests the solver's behavior when the goal is impossible to achieve. + /// + [Test] + public void GoapTestImpossible() + { + Condition goal = new Condition("int", ConditionOperator.Equal, -1); + GoapState state = GenerateState(); + GoapSolver solver = GenerateSolverWithActionPool(); + + //run + GoapResult result = solver.Solve(state, goal, 10); + + // error + Assert.AreEqual(false, result.success); + } + + /// + /// Generates a sample GOAP state for testing. + /// + /// A sample GOAP state. + private GoapState GenerateState() + { + GoapState state = new GoapState(); + state.SetRawValue("int", 0); + state.SetRawValue("float", 0f); + state.SetRawValue("double", 0.0); + state.SetRawValue("boolean", false); + return state; + } + + /// + /// Generates a GOAP solver with a predefined action pool for testing. + /// + /// A GOAP solver with predefined actions. + private GoapSolver GenerateSolverWithActionPool() + { + GoapSolver solver = new GoapSolver(); + solver.AddAction( + new GoapAction( + ACTION_1, + new NoCondition(), + new StateDiffInterface[] { new StateDiffAddition("int", 1) }, + 1.0 + ) + ); + solver.AddAction( + new GoapAction( + ACTION_2, + new Condition("int", ConditionOperator.Greater, 2), + new StateDiffInterface[] { new StateDiffAddition("float", 1f) }, + 2.0 + ) + ); + solver.AddAction( + new GoapAction( + ACTION_3, + new NoCondition(), + new StateDiffInterface[] + { + new StateDiffAddition("double", 1.0), + new StateDiffMapping( + "boolean", + new Dictionary() { { false, true }, { true, false } } + ) + }, + 3.0 + ) + ); + return solver; + } +} diff --git a/Tests/GoapTest.cs.meta b/Tests/GoapTest.cs.meta new file mode 100644 index 0000000..d58b26a --- /dev/null +++ b/Tests/GoapTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 54bf1392730ce124d92d928539d9158c \ No newline at end of file