GitHub
4 年前
当前提交
90a9d214
共有 65 个文件被更改,包括 6579 次插入 和 1 次删除
-
5com.unity.ml-agents.extensions/Documentation~/com.unity.ml-agents.extensions.md
-
7com.unity.ml-agents.extensions/Tests/Editor/Sensors/RigidBodySensorTests.cs
-
2com.unity.ml-agents/Runtime/SensorHelper.cs
-
21docs/Learning-Environment-Examples.md
-
8Project/Assets/ML-Agents/Examples/Match3.meta
-
67com.unity.ml-agents.extensions/Documentation~/Match3.md
-
3com.unity.ml-agents.extensions/Runtime/Match3.meta
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3.meta
-
75config/ppo/Match3.yaml
-
77docs/images/match3.png
-
8Project/Assets/ML-Agents/Examples/Match3/Prefabs.meta
-
174Project/Assets/ML-Agents/Examples/Match3/Prefabs/Match3Heuristic.prefab
-
7Project/Assets/ML-Agents/Examples/Match3/Prefabs/Match3Heuristic.prefab.meta
-
170Project/Assets/ML-Agents/Examples/Match3/Prefabs/Match3VectorObs.prefab
-
7Project/Assets/ML-Agents/Examples/Match3/Prefabs/Match3VectorObs.prefab.meta
-
170Project/Assets/ML-Agents/Examples/Match3/Prefabs/Match3VisualObs.prefab
-
7Project/Assets/ML-Agents/Examples/Match3/Prefabs/Match3VisualObs.prefab.meta
-
8Project/Assets/ML-Agents/Examples/Match3/Scenes.meta
-
1001Project/Assets/ML-Agents/Examples/Match3/Scenes/Match3.unity
-
7Project/Assets/ML-Agents/Examples/Match3/Scenes/Match3.unity.meta
-
8Project/Assets/ML-Agents/Examples/Match3/Scripts.meta
-
373Project/Assets/ML-Agents/Examples/Match3/Scripts/Match3Agent.cs
-
3Project/Assets/ML-Agents/Examples/Match3/Scripts/Match3Agent.cs.meta
-
272Project/Assets/ML-Agents/Examples/Match3/Scripts/Match3Board.cs
-
11Project/Assets/ML-Agents/Examples/Match3/Scripts/Match3Board.cs.meta
-
102Project/Assets/ML-Agents/Examples/Match3/Scripts/Match3Drawer.cs
-
3Project/Assets/ML-Agents/Examples/Match3/Scripts/Match3Drawer.cs.meta
-
8Project/Assets/ML-Agents/Examples/Match3/TFModels.meta
-
1001Project/Assets/ML-Agents/Examples/Match3/TFModels/Match3VectorObs.onnx
-
14Project/Assets/ML-Agents/Examples/Match3/TFModels/Match3VectorObs.onnx.meta
-
1001Project/Assets/ML-Agents/Examples/Match3/TFModels/Match3VisualObs.nn
-
11Project/Assets/ML-Agents/Examples/Match3/TFModels/Match3VisualObs.nn.meta
-
233com.unity.ml-agents.extensions/Runtime/Match3/AbstractBoard.cs
-
3com.unity.ml-agents.extensions/Runtime/Match3/AbstractBoard.cs.meta
-
120com.unity.ml-agents.extensions/Runtime/Match3/Match3Actuator.cs
-
3com.unity.ml-agents.extensions/Runtime/Match3/Match3Actuator.cs.meta
-
49com.unity.ml-agents.extensions/Runtime/Match3/Match3ActuatorComponent.cs
-
3com.unity.ml-agents.extensions/Runtime/Match3/Match3ActuatorComponent.cs.meta
-
297com.unity.ml-agents.extensions/Runtime/Match3/Match3Sensor.cs
-
3com.unity.ml-agents.extensions/Runtime/Match3/Match3Sensor.cs.meta
-
43com.unity.ml-agents.extensions/Runtime/Match3/Match3SensorComponent.cs
-
3com.unity.ml-agents.extensions/Runtime/Match3/Match3SensorComponent.cs.meta
-
260com.unity.ml-agents.extensions/Runtime/Match3/Move.cs
-
3com.unity.ml-agents.extensions/Runtime/Match3/Move.cs.meta
-
152com.unity.ml-agents.extensions/Tests/Editor/Match3/AbstractBoardTests.cs
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/AbstractBoardTests.cs.meta
-
115com.unity.ml-agents.extensions/Tests/Editor/Match3/Match3ActuatorTests.cs
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/Match3ActuatorTests.cs.meta
-
314com.unity.ml-agents.extensions/Tests/Editor/Match3/Match3SensorTests.cs
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/Match3SensorTests.cs.meta
-
60com.unity.ml-agents.extensions/Tests/Editor/Match3/MoveTests.cs
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/MoveTests.cs.meta
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/match3obs0.png
-
88com.unity.ml-agents.extensions/Tests/Editor/Match3/match3obs0.png.meta
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/match3obs_special0.png
-
88com.unity.ml-agents.extensions/Tests/Editor/Match3/match3obs_special0.png.meta
-
3com.unity.ml-agents.extensions/Tests/Editor/Match3/match3obs_special1.png
-
88com.unity.ml-agents.extensions/Tests/Editor/Match3/match3obs_special1.png.meta
|
|||
fileFormatVersion: 2 |
|||
guid: 85094c6352d9e43c497a54fef35e4d76 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
# Match-3 Game Support |
|||
|
|||
We provide some utilities to integrate ML-Agents with Match-3 games. |
|||
|
|||
## AbstractBoard class |
|||
The `AbstractBoard` is the bridge between ML-Agents and your game. It allows ML-Agents to |
|||
* ask your game what the "color" of a cell is |
|||
* ask whether the cell is a "special" piece type or not |
|||
* ask your game whether a move is allowed |
|||
* request that your game make a move |
|||
|
|||
These are handled by implementing the `GetCellType()`, `IsMoveValid()`, and `MakeMove()` abstract methods. |
|||
|
|||
The AbstractBoard also tracks the number of rows, columns, and potential piece types that the board can have. |
|||
|
|||
#### `public abstract int GetCellType(int row, int col)` |
|||
Returns the "color" of piece at the given row and column. |
|||
This should be between 0 and NumCellTypes-1 (inclusive). |
|||
The actual order of the values doesn't matter. |
|||
|
|||
#### `public abstract int GetSpecialType(int row, int col)` |
|||
Returns the special type of the piece at the given row and column. |
|||
This should be between 0 and NumSpecialTypes (inclusive). |
|||
The actual order of the values doesn't matter. |
|||
|
|||
#### `public abstract bool IsMoveValid(Move m)` |
|||
Check whether the particular `Move` is valid for the game. |
|||
The actual results will depend on the rules of the game, but we provide the `SimpleIsMoveValid()` method |
|||
that handles basic match3 rules with no special or immovable pieces. |
|||
|
|||
#### `public abstract bool MakeMove(Move m)` |
|||
Instruct the game to make the given move. Returns true if the move was made. |
|||
Note that during training, a move that was marked as invalid may occasionally still be |
|||
requested. If this happens, it is safe to do nothing and request another move. |
|||
|
|||
## Move struct |
|||
The Move struct encapsulates a swap of two adjacent cells. You can get the number of potential moves |
|||
for a board of a given size with. `Move.NumPotentialMoves(NumRows, NumColumns)`. There are two helper |
|||
functions to create a new `Move`: |
|||
* `public static Move FromMoveIndex(int moveIndex, int maxRows, int maxCols)` can be used to |
|||
iterate over all potential moves for the board by looping from 0 to `Move.NumPotentialMoves()` |
|||
* `public static Move FromPositionAndDirection(int row, int col, Direction dir, int maxRows, int maxCols)` creates |
|||
a `Move` from a row, column, and direction (and board size). |
|||
|
|||
## `Match3Sensor` and `Match3SensorComponent` classes |
|||
The `Match3Sensor` generates observations about the state using the `AbstractBoard` interface. You can |
|||
choose whether to use vector or "visual" observations; in theory, visual observations should perform |
|||
better because they are 2-dimensional like the board, but we need to experiment more on this. |
|||
|
|||
A `Match3SensorComponent` generates a `Match3Sensor` at runtime, and should be added to the same GameObject |
|||
as your `Agent` implementation. You do not need to write any additional code to use them. |
|||
|
|||
## `Match3Actuator` and `Match3ActuatorComponent` classes |
|||
The `Match3Actuator` converts actions from training or inference into a `Move` that is sent to` AbstractBoard.MakeMove()` |
|||
It also checks `AbstractBoard.IsMoveValid` for each potential move and uses this to set the action mask for Agent. |
|||
|
|||
A `Match3ActuatorComponent` generates a `Match3Actuator` at runtime, and should be added to the same GameObject |
|||
as your `Agent` implementation. You do not need to write any additional code to use them. |
|||
|
|||
# Setting up match-3 simulation |
|||
* Implement the `AbstractBoard` methods to integrate with your game. |
|||
* Give the `Agent` rewards when it does what you want it to (match multiple pieces in a row, clears pieces of a certain |
|||
type, etc). |
|||
* Add the `Agent`, `AbstractBoard` implementation, `Match3SensorComponent`, and `Match3ActuatorComponent` to the same |
|||
`GameObject`. |
|||
* Call `Agent.RequestDecision()` when you're ready for the `Agent` to make a move on the next `Academy` step. During |
|||
the next `Academy` step, the `MakeMove()` method on the board will be called. |
|
|||
fileFormatVersion: 2 |
|||
guid: 569f8fa2b7dd477c9b71f09e9d633832 |
|||
timeCreated: 1600465975 |
|
|||
fileFormatVersion: 2 |
|||
guid: 77b0212dde404f7c8ce9aac13bd550b8 |
|||
timeCreated: 1601332716 |
|
|||
behaviors: |
|||
Match3VectorObs: |
|||
trainer_type: ppo |
|||
hyperparameters: |
|||
batch_size: 64 |
|||
buffer_size: 12000 |
|||
learning_rate: 0.0003 |
|||
beta: 0.001 |
|||
epsilon: 0.2 |
|||
lambd: 0.99 |
|||
num_epoch: 3 |
|||
learning_rate_schedule: constant |
|||
network_settings: |
|||
normalize: true |
|||
hidden_units: 128 |
|||
num_layers: 2 |
|||
vis_encode_type: match3 |
|||
reward_signals: |
|||
extrinsic: |
|||
gamma: 0.99 |
|||
strength: 1.0 |
|||
keep_checkpoints: 5 |
|||
max_steps: 5000000 |
|||
time_horizon: 1000 |
|||
summary_freq: 10000 |
|||
threaded: true |
|||
Match3VisualObs: |
|||
trainer_type: ppo |
|||
hyperparameters: |
|||
batch_size: 64 |
|||
buffer_size: 12000 |
|||
learning_rate: 0.0003 |
|||
beta: 0.001 |
|||
epsilon: 0.2 |
|||
lambd: 0.99 |
|||
num_epoch: 3 |
|||
learning_rate_schedule: constant |
|||
network_settings: |
|||
normalize: true |
|||
hidden_units: 128 |
|||
num_layers: 2 |
|||
vis_encode_type: match3 |
|||
reward_signals: |
|||
extrinsic: |
|||
gamma: 0.99 |
|||
strength: 1.0 |
|||
keep_checkpoints: 5 |
|||
max_steps: 5000000 |
|||
time_horizon: 1000 |
|||
summary_freq: 10000 |
|||
threaded: true |
|||
Match3SimpleHeuristic: |
|||
# Settings can be very simple since we don't care about actually training the model |
|||
trainer_type: ppo |
|||
hyperparameters: |
|||
batch_size: 64 |
|||
buffer_size: 128 |
|||
network_settings: |
|||
hidden_units: 4 |
|||
num_layers: 1 |
|||
max_steps: 5000000 |
|||
summary_freq: 10000 |
|||
threaded: true |
|||
Match3GreedyHeuristic: |
|||
# Settings can be very simple since we don't care about actually training the model |
|||
trainer_type: ppo |
|||
hyperparameters: |
|||
batch_size: 64 |
|||
buffer_size: 128 |
|||
network_settings: |
|||
hidden_units: 4 |
|||
num_layers: 1 |
|||
max_steps: 5000000 |
|||
summary_freq: 10000 |
|||
threaded: true |
|
|||
fileFormatVersion: 2 |
|||
guid: 8519802844d8d4233b4c6f6758ab8322 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
%YAML 1.1 |
|||
%TAG !u! tag:unity3d.com,2011: |
|||
--- !u!1 &3508723250470608007 |
|||
GameObject: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
serializedVersion: 6 |
|||
m_Component: |
|||
- component: {fileID: 3508723250470608008} |
|||
- component: {fileID: 3508723250470608010} |
|||
- component: {fileID: 3508723250470608012} |
|||
- component: {fileID: 3508723250470608011} |
|||
- component: {fileID: 3508723250470608009} |
|||
- component: {fileID: 3508723250470608013} |
|||
- component: {fileID: 3508723250470608014} |
|||
m_Layer: 0 |
|||
m_Name: Match3 Agent |
|||
m_TagString: Untagged |
|||
m_Icon: {fileID: 0} |
|||
m_NavMeshLayer: 0 |
|||
m_StaticEditorFlags: 0 |
|||
m_IsActive: 1 |
|||
--- !u!4 &3508723250470608008 |
|||
Transform: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
|||
m_LocalPosition: {x: 0, y: 0, z: 0} |
|||
m_LocalScale: {x: 1, y: 1, z: 1} |
|||
m_Children: [] |
|||
m_Father: {fileID: 3508723250774301920} |
|||
m_RootOrder: 0 |
|||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} |
|||
--- !u!114 &3508723250470608010 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 5d1c4e0b1822b495aa52bc52839ecb30, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
m_BrainParameters: |
|||
VectorObservationSize: 0 |
|||
NumStackedVectorObservations: 1 |
|||
VectorActionSize: |
|||
VectorActionDescriptions: [] |
|||
VectorActionSpaceType: 0 |
|||
m_Model: {fileID: 11400000, guid: c34da50737a3c4a50918002b20b2b927, type: 3} |
|||
m_InferenceDevice: 0 |
|||
m_BehaviorType: 0 |
|||
m_BehaviorName: Match3SmartHeuristic |
|||
TeamId: 0 |
|||
m_UseChildSensors: 1 |
|||
m_UseChildActuators: 1 |
|||
m_ObservableAttributeHandling: 0 |
|||
--- !u!114 &3508723250470608012 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: d982f0cd92214bd2b689be838fa40c44, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
agentParameters: |
|||
maxStep: 0 |
|||
hasUpgradedFromAgentParameters: 1 |
|||
MaxStep: 0 |
|||
Board: {fileID: 0} |
|||
MoveTime: 0.25 |
|||
MaxMoves: 500 |
|||
UseSmartHeuristic: 1 |
|||
--- !u!114 &3508723250470608011 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: abebb7ad4a5547d7a3b04373784ff195, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
DebugEdgeIndex: -1 |
|||
--- !u!114 &3508723250470608009 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 6d852a063770348b68caa91b8e7642a5, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
Rows: 9 |
|||
Columns: 8 |
|||
NumCellTypes: 6 |
|||
NumSpecialTypes: 2 |
|||
RandomSeed: -1 |
|||
BasicCellPoints: 1 |
|||
SpecialCell1Points: 2 |
|||
SpecialCell2Points: 3 |
|||
--- !u!114 &3508723250470608013 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 08e4b0da54cb4d56bfcbae22dd49ab8d, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
ForceHeuristic: 1 |
|||
--- !u!114 &3508723250470608014 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250470608007} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 530d2f105aa145bd8a00e021bdd925fd, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
ObservationType: 0 |
|||
--- !u!1 &3508723250774301855 |
|||
GameObject: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
serializedVersion: 6 |
|||
m_Component: |
|||
- component: {fileID: 3508723250774301920} |
|||
m_Layer: 0 |
|||
m_Name: Match3Heuristic |
|||
m_TagString: Untagged |
|||
m_Icon: {fileID: 0} |
|||
m_NavMeshLayer: 0 |
|||
m_StaticEditorFlags: 0 |
|||
m_IsActive: 1 |
|||
--- !u!4 &3508723250774301920 |
|||
Transform: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3508723250774301855} |
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
|||
m_LocalPosition: {x: 0, y: 0, z: 0} |
|||
m_LocalScale: {x: 1, y: 1, z: 1} |
|||
m_Children: |
|||
- {fileID: 3508723250470608008} |
|||
m_Father: {fileID: 0} |
|||
m_RootOrder: 0 |
|||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} |
|
|||
fileFormatVersion: 2 |
|||
guid: 2fafdcd0587684641b03b11f04454f1b |
|||
PrefabImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
%YAML 1.1 |
|||
%TAG !u! tag:unity3d.com,2011: |
|||
--- !u!1 &2118285883905619929 |
|||
GameObject: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
serializedVersion: 6 |
|||
m_Component: |
|||
- component: {fileID: 2118285883905619878} |
|||
m_Layer: 0 |
|||
m_Name: Match3VectorObs |
|||
m_TagString: Untagged |
|||
m_Icon: {fileID: 0} |
|||
m_NavMeshLayer: 0 |
|||
m_StaticEditorFlags: 0 |
|||
m_IsActive: 1 |
|||
--- !u!4 &2118285883905619878 |
|||
Transform: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285883905619929} |
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
|||
m_LocalPosition: {x: 0, y: 0, z: 0} |
|||
m_LocalScale: {x: 1, y: 1, z: 1} |
|||
m_Children: |
|||
- {fileID: 2118285884327540686} |
|||
m_Father: {fileID: 0} |
|||
m_RootOrder: 0 |
|||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} |
|||
--- !u!1 &2118285884327540673 |
|||
GameObject: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
serializedVersion: 6 |
|||
m_Component: |
|||
- component: {fileID: 2118285884327540686} |
|||
- component: {fileID: 2118285884327540684} |
|||
- component: {fileID: 2118285884327540682} |
|||
- component: {fileID: 2118285884327540685} |
|||
- component: {fileID: 2118285884327540687} |
|||
- component: {fileID: 2118285884327540683} |
|||
- component: {fileID: 2118285884327540680} |
|||
m_Layer: 0 |
|||
m_Name: Match3 Agent |
|||
m_TagString: Untagged |
|||
m_Icon: {fileID: 0} |
|||
m_NavMeshLayer: 0 |
|||
m_StaticEditorFlags: 0 |
|||
m_IsActive: 1 |
|||
--- !u!4 &2118285884327540686 |
|||
Transform: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
|||
m_LocalPosition: {x: 0, y: 0, z: 0} |
|||
m_LocalScale: {x: 1, y: 1, z: 1} |
|||
m_Children: [] |
|||
m_Father: {fileID: 2118285883905619878} |
|||
m_RootOrder: 0 |
|||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} |
|||
--- !u!114 &2118285884327540684 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 5d1c4e0b1822b495aa52bc52839ecb30, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
m_BrainParameters: |
|||
VectorObservationSize: 0 |
|||
NumStackedVectorObservations: 1 |
|||
VectorActionSize: |
|||
VectorActionDescriptions: [] |
|||
VectorActionSpaceType: 0 |
|||
m_Model: {fileID: 11400000, guid: 9e89b8e81974148d3b7213530d00589d, type: 3} |
|||
m_InferenceDevice: 0 |
|||
m_BehaviorType: 0 |
|||
m_BehaviorName: Match3VectorObs |
|||
TeamId: 0 |
|||
m_UseChildSensors: 1 |
|||
m_UseChildActuators: 1 |
|||
m_ObservableAttributeHandling: 0 |
|||
--- !u!114 &2118285884327540682 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: d982f0cd92214bd2b689be838fa40c44, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
agentParameters: |
|||
maxStep: 0 |
|||
hasUpgradedFromAgentParameters: 1 |
|||
MaxStep: 0 |
|||
Board: {fileID: 0} |
|||
MoveTime: 0.25 |
|||
MaxMoves: 500 |
|||
--- !u!114 &2118285884327540685 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: abebb7ad4a5547d7a3b04373784ff195, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
DebugEdgeIndex: -1 |
|||
--- !u!114 &2118285884327540687 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 6d852a063770348b68caa91b8e7642a5, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
Rows: 9 |
|||
Columns: 8 |
|||
NumCellTypes: 6 |
|||
NumSpecialTypes: 2 |
|||
RandomSeed: -1 |
|||
--- !u!114 &2118285884327540683 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 08e4b0da54cb4d56bfcbae22dd49ab8d, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
ForceRandom: 0 |
|||
--- !u!114 &2118285884327540680 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 2118285884327540673} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 530d2f105aa145bd8a00e021bdd925fd, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
ObservationType: 0 |
|
|||
fileFormatVersion: 2 |
|||
guid: 6944ca02359f5427aa13c8551236a824 |
|||
PrefabImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
%YAML 1.1 |
|||
%TAG !u! tag:unity3d.com,2011: |
|||
--- !u!1 &3019509691567202678 |
|||
GameObject: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
serializedVersion: 6 |
|||
m_Component: |
|||
- component: {fileID: 3019509691567202569} |
|||
m_Layer: 0 |
|||
m_Name: Match3VisualObs |
|||
m_TagString: Untagged |
|||
m_Icon: {fileID: 0} |
|||
m_NavMeshLayer: 0 |
|||
m_StaticEditorFlags: 0 |
|||
m_IsActive: 1 |
|||
--- !u!4 &3019509691567202569 |
|||
Transform: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509691567202678} |
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
|||
m_LocalPosition: {x: 0, y: 0, z: 0} |
|||
m_LocalScale: {x: 1, y: 1, z: 1} |
|||
m_Children: |
|||
- {fileID: 3019509692332007777} |
|||
m_Father: {fileID: 0} |
|||
m_RootOrder: 0 |
|||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} |
|||
--- !u!1 &3019509692332007790 |
|||
GameObject: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
serializedVersion: 6 |
|||
m_Component: |
|||
- component: {fileID: 3019509692332007777} |
|||
- component: {fileID: 3019509692332007779} |
|||
- component: {fileID: 3019509692332007781} |
|||
- component: {fileID: 3019509692332007778} |
|||
- component: {fileID: 3019509692332007776} |
|||
- component: {fileID: 3019509692332007780} |
|||
- component: {fileID: 3019509692332007783} |
|||
m_Layer: 0 |
|||
m_Name: Match3 Agent |
|||
m_TagString: Untagged |
|||
m_Icon: {fileID: 0} |
|||
m_NavMeshLayer: 0 |
|||
m_StaticEditorFlags: 0 |
|||
m_IsActive: 1 |
|||
--- !u!4 &3019509692332007777 |
|||
Transform: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
|||
m_LocalPosition: {x: 0, y: 0, z: 0} |
|||
m_LocalScale: {x: 1, y: 1, z: 1} |
|||
m_Children: [] |
|||
m_Father: {fileID: 3019509691567202569} |
|||
m_RootOrder: 0 |
|||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} |
|||
--- !u!114 &3019509692332007779 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 5d1c4e0b1822b495aa52bc52839ecb30, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
m_BrainParameters: |
|||
VectorObservationSize: 0 |
|||
NumStackedVectorObservations: 1 |
|||
VectorActionSize: |
|||
VectorActionDescriptions: [] |
|||
VectorActionSpaceType: 0 |
|||
m_Model: {fileID: 11400000, guid: 48d14da88fea74d0693c691c6e3f2e34, type: 3} |
|||
m_InferenceDevice: 0 |
|||
m_BehaviorType: 0 |
|||
m_BehaviorName: Match3VisualObs |
|||
TeamId: 0 |
|||
m_UseChildSensors: 1 |
|||
m_UseChildActuators: 1 |
|||
m_ObservableAttributeHandling: 0 |
|||
--- !u!114 &3019509692332007781 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: d982f0cd92214bd2b689be838fa40c44, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
agentParameters: |
|||
maxStep: 0 |
|||
hasUpgradedFromAgentParameters: 1 |
|||
MaxStep: 0 |
|||
Board: {fileID: 0} |
|||
MoveTime: 0.25 |
|||
MaxMoves: 500 |
|||
--- !u!114 &3019509692332007778 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: abebb7ad4a5547d7a3b04373784ff195, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
DebugEdgeIndex: -1 |
|||
--- !u!114 &3019509692332007776 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 6d852a063770348b68caa91b8e7642a5, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
Rows: 9 |
|||
Columns: 8 |
|||
NumCellTypes: 6 |
|||
NumSpecialTypes: 2 |
|||
RandomSeed: -1 |
|||
--- !u!114 &3019509692332007780 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 08e4b0da54cb4d56bfcbae22dd49ab8d, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
ForceRandom: 0 |
|||
--- !u!114 &3019509692332007783 |
|||
MonoBehaviour: |
|||
m_ObjectHideFlags: 0 |
|||
m_CorrespondingSourceObject: {fileID: 0} |
|||
m_PrefabInstance: {fileID: 0} |
|||
m_PrefabAsset: {fileID: 0} |
|||
m_GameObject: {fileID: 3019509692332007790} |
|||
m_Enabled: 1 |
|||
m_EditorHideFlags: 0 |
|||
m_Script: {fileID: 11500000, guid: 530d2f105aa145bd8a00e021bdd925fd, type: 3} |
|||
m_Name: |
|||
m_EditorClassIdentifier: |
|||
ObservationType: 2 |
|
|||
fileFormatVersion: 2 |
|||
guid: aaa471bd5e2014848a66917476671aed |
|||
PrefabImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: e033fb0df67684ebf961ed115870ff10 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
1001
Project/Assets/ML-Agents/Examples/Match3/Scenes/Match3.unity
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
fileFormatVersion: 2 |
|||
guid: 2e09c5458f1494f9dad9cd6d09dff964 |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: be7a27f4291944d3dba4f696e1af4209 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using UnityEngine; |
|||
using Unity.MLAgents; |
|||
using Unity.MLAgents.Actuators; |
|||
using Unity.MLAgents.Extensions.Match3; |
|||
|
|||
namespace Unity.MLAgentsExamples |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// State of the "game" when showing all steps of the simulation. This is only used outside of training.
|
|||
/// The state diagram is
|
|||
///
|
|||
/// | <--------------------------------------- ^
|
|||
/// | |
|
|||
/// v |
|
|||
/// +--------+ +-------+ +-----+ +------+
|
|||
/// |Find | ---> |Clear | ---> |Drop | ---> |Fill |
|
|||
/// |Matches | |Matched| | | |Empty |
|
|||
/// +--------+ +-------+ +-----+ +------+
|
|||
///
|
|||
/// | ^
|
|||
/// | |
|
|||
/// v |
|
|||
///
|
|||
/// +--------+
|
|||
/// |Wait for|
|
|||
/// |Move |
|
|||
/// +--------+
|
|||
///
|
|||
/// The stats advances each "MoveTime" seconds.
|
|||
/// </summary>
|
|||
enum State |
|||
{ |
|||
/// <summary>
|
|||
/// Guard value, should never happen.
|
|||
/// </summary>
|
|||
Invalid = -1, |
|||
|
|||
/// <summary>
|
|||
/// Look for matches. If there are matches, the next state is ClearMatched, otherwise WaitForMove.
|
|||
/// </summary>
|
|||
FindMatches = 0, |
|||
|
|||
/// <summary>
|
|||
/// Remove matched cells and replace them with a placeholder value.
|
|||
/// </summary>
|
|||
ClearMatched = 1, |
|||
|
|||
/// <summary>
|
|||
/// Move cells "down" to fill empty space.
|
|||
/// </summary>
|
|||
Drop = 2, |
|||
|
|||
/// <summary>
|
|||
/// Replace empty cells with new random values.
|
|||
/// </summary>
|
|||
FillEmpty = 3, |
|||
|
|||
/// <summary>
|
|||
/// Request a move from the Agent.
|
|||
/// </summary>
|
|||
WaitForMove = 4, |
|||
} |
|||
|
|||
public enum HeuristicQuality |
|||
{ |
|||
/// <summary>
|
|||
/// The heuristic will pick any valid move at random.
|
|||
/// </summary>
|
|||
RandomValidMove, |
|||
|
|||
/// <summary>
|
|||
/// The heuristic will pick the move that scores the most points.
|
|||
/// This only looks at the immediate move, and doesn't consider where cells will fall.
|
|||
/// </summary>
|
|||
Greedy |
|||
} |
|||
|
|||
public class Match3Agent : Agent |
|||
{ |
|||
[HideInInspector] |
|||
public Match3Board Board; |
|||
|
|||
public float MoveTime = 1.0f; |
|||
public int MaxMoves = 500; |
|||
|
|||
|
|||
public HeuristicQuality HeuristicQuality = HeuristicQuality.RandomValidMove; |
|||
|
|||
State m_CurrentState = State.WaitForMove; |
|||
float m_TimeUntilMove; |
|||
private int m_MovesMade; |
|||
|
|||
private System.Random m_Random; |
|||
private const float k_RewardMultiplier = 0.01f; |
|||
|
|||
void Awake() |
|||
{ |
|||
Board = GetComponent<Match3Board>(); |
|||
var seed = Board.RandomSeed == -1 ? gameObject.GetInstanceID() : Board.RandomSeed + 1; |
|||
m_Random = new System.Random(seed); |
|||
} |
|||
|
|||
public override void OnEpisodeBegin() |
|||
{ |
|||
base.OnEpisodeBegin(); |
|||
|
|||
Board.InitSettled(); |
|||
m_CurrentState = State.FindMatches; |
|||
m_TimeUntilMove = MoveTime; |
|||
m_MovesMade = 0; |
|||
} |
|||
|
|||
private void FixedUpdate() |
|||
{ |
|||
if (Academy.Instance.IsCommunicatorOn) |
|||
{ |
|||
FastUpdate(); |
|||
} |
|||
else |
|||
{ |
|||
AnimatedUpdate(); |
|||
} |
|||
|
|||
// We can't use the normal MaxSteps system to decide when to end an episode,
|
|||
// since different agents will make moves at different frequencies (depending on the number of
|
|||
// chained moves). So track a number of moves per Agent and manually interrupt the episode.
|
|||
if (m_MovesMade >= MaxMoves) |
|||
{ |
|||
EpisodeInterrupted(); |
|||
} |
|||
} |
|||
|
|||
void FastUpdate() |
|||
{ |
|||
while (true) |
|||
{ |
|||
var hasMatched = Board.MarkMatchedCells(); |
|||
if (!hasMatched) |
|||
{ |
|||
break; |
|||
} |
|||
var pointsEarned = Board.ClearMatchedCells(); |
|||
AddReward(k_RewardMultiplier * pointsEarned); |
|||
Board.DropCells(); |
|||
Board.FillFromAbove(); |
|||
} |
|||
|
|||
while (!HasValidMoves()) |
|||
{ |
|||
// Shuffle the board until we have a valid move.
|
|||
Board.InitSettled(); |
|||
} |
|||
RequestDecision(); |
|||
m_MovesMade++; |
|||
} |
|||
|
|||
void AnimatedUpdate() |
|||
{ |
|||
m_TimeUntilMove -= Time.deltaTime; |
|||
if (m_TimeUntilMove > 0.0f) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
m_TimeUntilMove = MoveTime; |
|||
|
|||
var nextState = State.Invalid; |
|||
switch (m_CurrentState) |
|||
{ |
|||
case State.FindMatches: |
|||
var hasMatched = Board.MarkMatchedCells(); |
|||
nextState = hasMatched ? State.ClearMatched : State.WaitForMove; |
|||
if (nextState == State.WaitForMove) |
|||
{ |
|||
m_MovesMade++; |
|||
} |
|||
break; |
|||
case State.ClearMatched: |
|||
var pointsEarned = Board.ClearMatchedCells(); |
|||
AddReward(k_RewardMultiplier * pointsEarned); |
|||
nextState = State.Drop; |
|||
break; |
|||
case State.Drop: |
|||
Board.DropCells(); |
|||
nextState = State.FillEmpty; |
|||
break; |
|||
case State.FillEmpty: |
|||
Board.FillFromAbove(); |
|||
nextState = State.FindMatches; |
|||
break; |
|||
case State.WaitForMove: |
|||
while (true) |
|||
{ |
|||
// Shuffle the board until we have a valid move.
|
|||
bool hasMoves = HasValidMoves(); |
|||
if (hasMoves) |
|||
{ |
|||
break; |
|||
} |
|||
Board.InitSettled(); |
|||
} |
|||
RequestDecision(); |
|||
|
|||
nextState = State.FindMatches; |
|||
break; |
|||
default: |
|||
throw new ArgumentOutOfRangeException(); |
|||
} |
|||
|
|||
m_CurrentState = nextState; |
|||
} |
|||
|
|||
bool HasValidMoves() |
|||
{ |
|||
foreach (var move in Board.ValidMoves()) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public override void Heuristic(in ActionBuffers actionsOut) |
|||
{ |
|||
var discreteActions = actionsOut.DiscreteActions; |
|||
discreteActions[0] = GreedyMove(); |
|||
} |
|||
|
|||
int GreedyMove() |
|||
{ |
|||
var pointsByType = new[] { Board.BasicCellPoints, Board.SpecialCell1Points, Board.SpecialCell2Points }; |
|||
|
|||
var bestMoveIndex = 0; |
|||
var bestMovePoints = -1; |
|||
var numMovesAtCurrentScore = 0; |
|||
|
|||
foreach (var move in Board.ValidMoves()) |
|||
{ |
|||
var movePoints = HeuristicQuality == HeuristicQuality.Greedy ? EvalMovePoints(move, pointsByType) : 1; |
|||
if (movePoints < bestMovePoints) |
|||
{ |
|||
// Worse, skip
|
|||
continue; |
|||
} |
|||
|
|||
if (movePoints > bestMovePoints) |
|||
{ |
|||
// Better, keep
|
|||
bestMovePoints = movePoints; |
|||
bestMoveIndex = move.MoveIndex; |
|||
numMovesAtCurrentScore = 1; |
|||
} |
|||
else |
|||
{ |
|||
// Tied for best - use reservoir sampling to make sure we select from equal moves uniformly.
|
|||
// See https://en.wikipedia.org/wiki/Reservoir_sampling#Simple_algorithm
|
|||
numMovesAtCurrentScore++; |
|||
var randVal = m_Random.Next(0, numMovesAtCurrentScore); |
|||
if (randVal == 0) |
|||
{ |
|||
// Keep the new one
|
|||
bestMoveIndex = move.MoveIndex; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return bestMoveIndex; |
|||
} |
|||
|
|||
int EvalMovePoints(Move move, int[] pointsByType) |
|||
{ |
|||
// Counts the expected points for making the move.
|
|||
var moveVal = Board.GetCellType(move.Row, move.Column); |
|||
var moveSpecial = Board.GetSpecialType(move.Row, move.Column); |
|||
var (otherRow, otherCol) = move.OtherCell(); |
|||
var oppositeVal = Board.GetCellType(otherRow, otherCol); |
|||
var oppositeSpecial = Board.GetSpecialType(otherRow, otherCol); |
|||
|
|||
|
|||
int movePoints = EvalHalfMove( |
|||
otherRow, otherCol, moveVal, moveSpecial, move.Direction, pointsByType |
|||
); |
|||
int otherPoints = EvalHalfMove( |
|||
move.Row, move.Column, oppositeVal, oppositeSpecial, move.OtherDirection(), pointsByType |
|||
); |
|||
return movePoints + otherPoints; |
|||
} |
|||
|
|||
int EvalHalfMove(int newRow, int newCol, int newValue, int newSpecial, Direction incomingDirection, int[] pointsByType) |
|||
{ |
|||
// This is a essentially a duplicate of AbstractBoard.CheckHalfMove but also counts the points for the move.
|
|||
int matchedLeft = 0, matchedRight = 0, matchedUp = 0, matchedDown = 0; |
|||
int scoreLeft = 0, scoreRight = 0, scoreUp = 0, scoreDown = 0; |
|||
|
|||
if (incomingDirection != Direction.Right) |
|||
{ |
|||
for (var c = newCol - 1; c >= 0; c--) |
|||
{ |
|||
if (Board.GetCellType(newRow, c) == newValue) |
|||
{ |
|||
matchedLeft++; |
|||
scoreLeft += pointsByType[Board.GetSpecialType(newRow, c)]; |
|||
} |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (incomingDirection != Direction.Left) |
|||
{ |
|||
for (var c = newCol + 1; c < Board.Columns; c++) |
|||
{ |
|||
if (Board.GetCellType(newRow, c) == newValue) |
|||
{ |
|||
matchedRight++; |
|||
scoreRight += pointsByType[Board.GetSpecialType(newRow, c)]; |
|||
} |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (incomingDirection != Direction.Down) |
|||
{ |
|||
for (var r = newRow + 1; r < Board.Rows; r++) |
|||
{ |
|||
if (Board.GetCellType(r, newCol) == newValue) |
|||
{ |
|||
matchedUp++; |
|||
scoreUp += pointsByType[Board.GetSpecialType(r, newCol)]; |
|||
} |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (incomingDirection != Direction.Up) |
|||
{ |
|||
for (var r = newRow - 1; r >= 0; r--) |
|||
{ |
|||
if (Board.GetCellType(r, newCol) == newValue) |
|||
{ |
|||
matchedDown++; |
|||
scoreDown += pointsByType[Board.GetSpecialType(r, newCol)]; |
|||
} |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if ((matchedUp + matchedDown >= 2) || (matchedLeft + matchedRight >= 2)) |
|||
{ |
|||
// It's a match. Start from counting the piece being moved
|
|||
var totalScore = pointsByType[newSpecial]; |
|||
if (matchedUp + matchedDown >= 2) |
|||
{ |
|||
totalScore += scoreUp + scoreDown; |
|||
} |
|||
|
|||
if (matchedLeft + matchedRight >= 2) |
|||
{ |
|||
totalScore += scoreLeft + scoreRight; |
|||
} |
|||
return totalScore; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: d982f0cd92214bd2b689be838fa40c44 |
|||
timeCreated: 1598221207 |
|
|||
using Unity.MLAgents.Extensions.Match3; |
|||
using UnityEngine; |
|||
|
|||
namespace Unity.MLAgentsExamples |
|||
{ |
|||
|
|||
|
|||
public class Match3Board : AbstractBoard |
|||
{ |
|||
public int RandomSeed = -1; |
|||
|
|||
public const int k_EmptyCell = -1; |
|||
[Tooltip("Points earned for clearing a basic cell (cube)")] |
|||
public int BasicCellPoints = 1; |
|||
|
|||
[Tooltip("Points earned for clearing a special cell (sphere)")] |
|||
public int SpecialCell1Points = 2; |
|||
|
|||
[Tooltip("Points earned for clearing an extra special cell (plus)")] |
|||
public int SpecialCell2Points = 3; |
|||
|
|||
(int, int)[,] m_Cells; |
|||
bool[,] m_Matched; |
|||
|
|||
System.Random m_Random; |
|||
|
|||
void Awake() |
|||
{ |
|||
m_Cells = new (int, int)[Columns, Rows]; |
|||
m_Matched = new bool[Columns, Rows]; |
|||
|
|||
m_Random = new System.Random(RandomSeed == -1 ? gameObject.GetInstanceID() : RandomSeed); |
|||
|
|||
InitRandom(); |
|||
} |
|||
|
|||
public override bool MakeMove(Move move) |
|||
{ |
|||
if (!IsMoveValid(move)) |
|||
{ |
|||
return false; |
|||
} |
|||
var originalValue = m_Cells[move.Column, move.Row]; |
|||
var (otherRow, otherCol) = move.OtherCell(); |
|||
var destinationValue = m_Cells[otherCol, otherRow]; |
|||
|
|||
m_Cells[move.Column, move.Row] = destinationValue; |
|||
m_Cells[otherCol, otherRow] = originalValue; |
|||
return true; |
|||
} |
|||
|
|||
public override int GetCellType(int row, int col) |
|||
{ |
|||
return m_Cells[col, row].Item1; |
|||
} |
|||
|
|||
public override int GetSpecialType(int row, int col) |
|||
{ |
|||
return m_Cells[col, row].Item2; |
|||
} |
|||
|
|||
public override bool IsMoveValid(Move m) |
|||
{ |
|||
if (m_Cells == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return SimpleIsMoveValid(m); |
|||
} |
|||
|
|||
public bool MarkMatchedCells(int[,] cells = null) |
|||
{ |
|||
ClearMarked(); |
|||
bool madeMatch = false; |
|||
for (var i = 0; i < Rows; i++) |
|||
{ |
|||
for (var j = 0; j < Columns; j++) |
|||
{ |
|||
// Check vertically
|
|||
var matchedRows = 0; |
|||
for (var iOffset = i; iOffset < Rows; iOffset++) |
|||
{ |
|||
if (m_Cells[j, i].Item1 != m_Cells[j, iOffset].Item1) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
matchedRows++; |
|||
} |
|||
|
|||
if (matchedRows >= 3) |
|||
{ |
|||
madeMatch = true; |
|||
for (var k = 0; k < matchedRows; k++) |
|||
{ |
|||
m_Matched[j, i + k] = true; |
|||
} |
|||
} |
|||
|
|||
// Check vertically
|
|||
var matchedCols = 0; |
|||
for (var jOffset = j; jOffset < Columns; jOffset++) |
|||
{ |
|||
if (m_Cells[j, i].Item1 != m_Cells[jOffset, i].Item1) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
matchedCols++; |
|||
} |
|||
|
|||
if (matchedCols >= 3) |
|||
{ |
|||
madeMatch = true; |
|||
for (var k = 0; k < matchedCols; k++) |
|||
{ |
|||
m_Matched[j + k, i] = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return madeMatch; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets cells that are matched to the empty cell, and returns the score earned.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public int ClearMatchedCells() |
|||
{ |
|||
var pointsByType = new[] { BasicCellPoints, SpecialCell1Points, SpecialCell2Points }; |
|||
int pointsEarned = 0; |
|||
for (var i = 0; i < Rows; i++) |
|||
{ |
|||
for (var j = 0; j < Columns; j++) |
|||
{ |
|||
if (m_Matched[j, i]) |
|||
{ |
|||
var speciaType = GetSpecialType(i, j); |
|||
pointsEarned += pointsByType[speciaType]; |
|||
m_Cells[j, i] = (k_EmptyCell, 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
ClearMarked(); // TODO clear here or at start of matching?
|
|||
return pointsEarned; |
|||
} |
|||
|
|||
public bool DropCells() |
|||
{ |
|||
var madeChanges = false; |
|||
// Gravity is applied in the negative row direction
|
|||
for (var j = 0; j < Columns; j++) |
|||
{ |
|||
var writeIndex = 0; |
|||
for (var readIndex = 0; readIndex < Rows; readIndex++) |
|||
{ |
|||
m_Cells[j, writeIndex] = m_Cells[j, readIndex]; |
|||
if (m_Cells[j, readIndex].Item1 != k_EmptyCell) |
|||
{ |
|||
writeIndex++; |
|||
} |
|||
} |
|||
|
|||
// Fill in empties at the end
|
|||
for (; writeIndex < Rows; writeIndex++) |
|||
{ |
|||
madeChanges = true; |
|||
m_Cells[j, writeIndex] = (k_EmptyCell, 0); |
|||
} |
|||
} |
|||
|
|||
return madeChanges; |
|||
} |
|||
|
|||
public bool FillFromAbove() |
|||
{ |
|||
bool madeChanges = false; |
|||
for (var i = 0; i < Rows; i++) |
|||
{ |
|||
for (var j = 0; j < Columns; j++) |
|||
{ |
|||
if (m_Cells[j, i].Item1 == k_EmptyCell) |
|||
{ |
|||
madeChanges = true; |
|||
m_Cells[j, i] = (GetRandomCellType(), GetRandomSpecialType()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return madeChanges; |
|||
} |
|||
|
|||
public (int, int)[,] Cells |
|||
{ |
|||
get { return m_Cells; } |
|||
} |
|||
|
|||
public bool[,] Matched |
|||
{ |
|||
get { return m_Matched; } |
|||
} |
|||
|
|||
// Initialize the board to random values.
|
|||
public void InitRandom() |
|||
{ |
|||
for (var i = 0; i < Rows; i++) |
|||
{ |
|||
for (var j = 0; j < Columns; j++) |
|||
{ |
|||
m_Cells[j, i] = (GetRandomCellType(), GetRandomSpecialType()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void InitSettled() |
|||
{ |
|||
InitRandom(); |
|||
while (true) |
|||
{ |
|||
var anyMatched = MarkMatchedCells(); |
|||
if (!anyMatched) |
|||
{ |
|||
return; |
|||
} |
|||
ClearMatchedCells(); |
|||
DropCells(); |
|||
FillFromAbove(); |
|||
} |
|||
} |
|||
|
|||
void ClearMarked() |
|||
{ |
|||
for (var i = 0; i < Rows; i++) |
|||
{ |
|||
for (var j = 0; j < Columns; j++) |
|||
{ |
|||
m_Matched[j, i] = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
int GetRandomCellType() |
|||
{ |
|||
return m_Random.Next(0, NumCellTypes); |
|||
} |
|||
|
|||
int GetRandomSpecialType() |
|||
{ |
|||
// 1 in N chance to get a type-2 special
|
|||
// 2 in N chance to get a type-1 special
|
|||
// otherwise 0 (boring)
|
|||
var N = 10; |
|||
var val = m_Random.Next(0, N); |
|||
if (val == 0) |
|||
{ |
|||
return 2; |
|||
} |
|||
|
|||
if (val <= 2) |
|||
{ |
|||
return 1; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6d852a063770348b68caa91b8e7642a5 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEngine; |
|||
using Unity.MLAgents.Extensions.Match3; |
|||
|
|||
namespace Unity.MLAgentsExamples |
|||
{ |
|||
public class Match3Drawer : MonoBehaviour |
|||
{ |
|||
public int DebugMoveIndex = -1; |
|||
|
|||
static Color[] s_Colors = new[] |
|||
{ |
|||
Color.red, |
|||
Color.green, |
|||
Color.blue, |
|||
Color.cyan, |
|||
Color.magenta, |
|||
Color.yellow, |
|||
Color.gray, |
|||
Color.black, |
|||
}; |
|||
|
|||
private static Color s_EmptyColor = new Color(0.5f, 0.5f, 0.5f, .25f); |
|||
|
|||
|
|||
void OnDrawGizmos() |
|||
{ |
|||
// TODO replace Gizmos for drawing the game state with proper GameObjects and animations.
|
|||
var cubeSize = .5f; |
|||
var cubeSpacing = .75f; |
|||
var matchedWireframeSize = .5f * (cubeSize + cubeSpacing); |
|||
|
|||
var board = GetComponent<Match3Board>(); |
|||
if (board == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
for (var i = 0; i < board.Rows; i++) |
|||
{ |
|||
for (var j = 0; j < board.Columns; j++) |
|||
{ |
|||
var value = board.Cells != null ? board.GetCellType(i, j) : Match3Board.k_EmptyCell; |
|||
if (value >= 0 && value < s_Colors.Length) |
|||
{ |
|||
Gizmos.color = s_Colors[value]; |
|||
} |
|||
else |
|||
{ |
|||
Gizmos.color = s_EmptyColor; |
|||
} |
|||
|
|||
var pos = new Vector3(j, i, 0); |
|||
pos *= cubeSpacing; |
|||
|
|||
var specialType = board.Cells != null ? board.GetSpecialType(i, j) : 0; |
|||
if (specialType == 2) |
|||
{ |
|||
Gizmos.DrawCube(transform.TransformPoint(pos), cubeSize * new Vector3(1f, .5f, .5f)); |
|||
Gizmos.DrawCube(transform.TransformPoint(pos), cubeSize * new Vector3(.5f, 1f, .5f)); |
|||
Gizmos.DrawCube(transform.TransformPoint(pos), cubeSize * new Vector3(.5f, .5f, 1f)); |
|||
} |
|||
else if (specialType == 1) |
|||
{ |
|||
Gizmos.DrawSphere(transform.TransformPoint(pos), .5f * cubeSize); |
|||
} |
|||
else |
|||
{ |
|||
Gizmos.DrawCube(transform.TransformPoint(pos), cubeSize * Vector3.one); |
|||
} |
|||
|
|||
Gizmos.color = Color.yellow; |
|||
if (board.Matched != null && board.Matched[j, i]) |
|||
{ |
|||
Gizmos.DrawWireCube(transform.TransformPoint(pos), matchedWireframeSize * Vector3.one); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Draw valid moves
|
|||
foreach (var move in board.AllMoves()) |
|||
{ |
|||
if (DebugMoveIndex >= 0 && move.MoveIndex != DebugMoveIndex) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (!board.IsMoveValid(move)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var (otherRow, otherCol) = move.OtherCell(); |
|||
var pos = new Vector3(move.Column, move.Row, 0) * cubeSpacing; |
|||
var otherPos = new Vector3(otherCol, otherRow, 0) * cubeSpacing; |
|||
|
|||
var oneQuarter = Vector3.Lerp(pos, otherPos, .25f); |
|||
var threeQuarters = Vector3.Lerp(pos, otherPos, .75f); |
|||
Gizmos.DrawLine(transform.TransformPoint(oneQuarter), transform.TransformPoint(threeQuarters)); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: abebb7ad4a5547d7a3b04373784ff195 |
|||
timeCreated: 1598221188 |
|
|||
fileFormatVersion: 2 |
|||
guid: 504c8f923fdf448e795936f2900a5fd4 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
1001
Project/Assets/ML-Agents/Examples/Match3/TFModels/Match3VectorObs.onnx
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
fileFormatVersion: 2 |
|||
guid: 9e89b8e81974148d3b7213530d00589d |
|||
ScriptedImporter: |
|||
fileIDToRecycleName: |
|||
11400000: main obj |
|||
11400002: model data |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|||
script: {fileID: 11500000, guid: 683b6cb6d0a474744822c888b46772c9, type: 3} |
|||
optimizeModel: 1 |
|||
forceArbitraryBatchSize: 1 |
|||
treatErrorsAsWarnings: 0 |
1001
Project/Assets/ML-Agents/Examples/Match3/TFModels/Match3VisualObs.nn
文件差异内容过多而无法显示
查看文件
文件差异内容过多而无法显示
查看文件
|
|||
fileFormatVersion: 2 |
|||
guid: 48d14da88fea74d0693c691c6e3f2e34 |
|||
ScriptedImporter: |
|||
fileIDToRecycleName: |
|||
11400000: main obj |
|||
11400002: model data |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|||
script: {fileID: 11500000, guid: 19ed1486aa27d4903b34839f37b8f69f, type: 3} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Match3 |
|||
{ |
|||
public abstract class AbstractBoard : MonoBehaviour |
|||
{ |
|||
/// <summary>
|
|||
/// Number of rows on the board
|
|||
/// </summary>
|
|||
public int Rows; |
|||
|
|||
/// <summary>
|
|||
/// Number of columns on the board
|
|||
/// </summary>
|
|||
public int Columns; |
|||
|
|||
/// <summary>
|
|||
/// Maximum number of different types of cells (colors, pieces, etc).
|
|||
/// </summary>
|
|||
public int NumCellTypes; |
|||
|
|||
/// <summary>
|
|||
/// Maximum number of special types. This can be zero, in which case
|
|||
/// all cells of the same type are assumed to be equivalent.
|
|||
/// </summary>
|
|||
public int NumSpecialTypes; |
|||
|
|||
/// <summary>
|
|||
/// Returns the "color" of the piece at the given row and column.
|
|||
/// This should be between 0 and NumCellTypes-1 (inclusive).
|
|||
/// The actual order of the values doesn't matter.
|
|||
/// </summary>
|
|||
/// <param name="row"></param>
|
|||
/// <param name="col"></param>
|
|||
/// <returns></returns>
|
|||
public abstract int GetCellType(int row, int col); |
|||
|
|||
/// <summary>
|
|||
/// Returns the special type of the piece at the given row and column.
|
|||
/// This should be between 0 and NumSpecialTypes (inclusive).
|
|||
/// The actual order of the values doesn't matter.
|
|||
/// </summary>
|
|||
/// <param name="row"></param>
|
|||
/// <param name="col"></param>
|
|||
/// <returns></returns>
|
|||
public abstract int GetSpecialType(int row, int col); |
|||
|
|||
/// <summary>
|
|||
/// Check whether the particular Move is valid for the game.
|
|||
/// The actual results will depend on the rules of the game, but we provide SimpleIsMoveValid()
|
|||
/// that handles basic match3 rules with no special or immovable pieces.
|
|||
/// </summary>
|
|||
/// <param name="m"></param>
|
|||
/// <returns></returns>
|
|||
public abstract bool IsMoveValid(Move m); |
|||
|
|||
/// <summary>
|
|||
/// Instruct the game to make the given move. Returns true if the move was made.
|
|||
/// Note that during training, a move that was marked as invalid may occasionally still be
|
|||
/// requested. If this happens, it is safe to do nothing and request another move.
|
|||
/// </summary>
|
|||
/// <param name="m"></param>
|
|||
/// <returns></returns>
|
|||
public abstract bool MakeMove(Move m); |
|||
|
|||
/// <summary>
|
|||
/// Return the total number of moves possible for the board.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public int NumMoves() |
|||
{ |
|||
return Move.NumPotentialMoves(Rows, Columns); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// An optional callback for when the all moves are invalid. Ideally, the game state should
|
|||
/// be changed before this happens, but this is a way to get notified if not.
|
|||
/// </summary>
|
|||
public Action OnNoValidMovesAction; |
|||
|
|||
/// <summary>
|
|||
/// Iterate through all Moves on the board.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public IEnumerable<Move> AllMoves() |
|||
{ |
|||
var currentMove = Move.FromMoveIndex(0, Rows, Columns); |
|||
for (var i = 0; i < NumMoves(); i++) |
|||
{ |
|||
yield return currentMove; |
|||
currentMove.Next(Rows, Columns); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Iterate through all valid Moves on the board.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public IEnumerable<Move> ValidMoves() |
|||
{ |
|||
var currentMove = Move.FromMoveIndex(0, Rows, Columns); |
|||
for (var i = 0; i < NumMoves(); i++) |
|||
{ |
|||
if (IsMoveValid(currentMove)) |
|||
{ |
|||
yield return currentMove; |
|||
} |
|||
currentMove.Next(Rows, Columns); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Iterate through all invalid Moves on the board.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public IEnumerable<Move> InvalidMoves() |
|||
{ |
|||
var currentMove = Move.FromMoveIndex(0, Rows, Columns); |
|||
for (var i = 0; i < NumMoves(); i++) |
|||
{ |
|||
if (!IsMoveValid(currentMove)) |
|||
{ |
|||
yield return currentMove; |
|||
} |
|||
currentMove.Next(Rows, Columns); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns true if swapped the cells specified by the move would result in
|
|||
/// 3 or more cells of the same type in a row. This assumes that all pieces are allowed
|
|||
/// to be moved; to add extra logic, incorporate it into you IsMoveValid() method.
|
|||
/// </summary>
|
|||
/// <param name="move"></param>
|
|||
/// <returns></returns>
|
|||
public bool SimpleIsMoveValid(Move move) |
|||
{ |
|||
using (TimerStack.Instance.Scoped("SimpleIsMoveValid")) |
|||
{ |
|||
var moveVal = GetCellType(move.Row, move.Column); |
|||
var (otherRow, otherCol) = move.OtherCell(); |
|||
var oppositeVal = GetCellType(otherRow, otherCol); |
|||
|
|||
// Simple check - if the values are the same, don't match
|
|||
// This might not be valid for all games
|
|||
{ |
|||
if (moveVal == oppositeVal) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
bool moveMatches = CheckHalfMove(otherRow, otherCol, moveVal, move.Direction); |
|||
if (moveMatches) |
|||
{ |
|||
// early out
|
|||
return true; |
|||
} |
|||
|
|||
bool otherMatches = CheckHalfMove(move.Row, move.Column, oppositeVal, move.OtherDirection()); |
|||
return otherMatches; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if one of the cells that is swapped during a move matches 3 or more.
|
|||
/// Since these checks are similar for each cell, we consider the Move as two "half moves".
|
|||
/// </summary>
|
|||
/// <param name="newRow"></param>
|
|||
/// <param name="newCol"></param>
|
|||
/// <param name="newValue"></param>
|
|||
/// <param name="incomingDirection"></param>
|
|||
/// <returns></returns>
|
|||
bool CheckHalfMove(int newRow, int newCol, int newValue, Direction incomingDirection) |
|||
{ |
|||
int matchedLeft = 0, matchedRight = 0, matchedUp = 0, matchedDown = 0; |
|||
|
|||
if (incomingDirection != Direction.Right) |
|||
{ |
|||
for (var c = newCol - 1; c >= 0; c--) |
|||
{ |
|||
if (GetCellType(newRow, c) == newValue) |
|||
matchedLeft++; |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (incomingDirection != Direction.Left) |
|||
{ |
|||
for (var c = newCol + 1; c < Columns; c++) |
|||
{ |
|||
if (GetCellType(newRow, c) == newValue) |
|||
matchedRight++; |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (incomingDirection != Direction.Down) |
|||
{ |
|||
for (var r = newRow + 1; r < Rows; r++) |
|||
{ |
|||
if (GetCellType(r, newCol) == newValue) |
|||
matchedUp++; |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (incomingDirection != Direction.Up) |
|||
{ |
|||
for (var r = newRow - 1; r >= 0; r--) |
|||
{ |
|||
if (GetCellType(r, newCol) == newValue) |
|||
matchedDown++; |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if ((matchedUp + matchedDown >= 2) || (matchedLeft + matchedRight >= 2)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6222defa70dc4c08aaeafd0be4e821d2 |
|||
timeCreated: 1600466051 |
|
|||
using System.Collections.Generic; |
|||
using Unity.MLAgents.Actuators; |
|||
using UnityEngine; |
|||
|
|||
|
|||
namespace Unity.MLAgents.Extensions.Match3 |
|||
{ |
|||
/// <summary>
|
|||
/// Actuator for a Match3 game. It translates valid moves (defined by AbstractBoard.IsMoveValid())
|
|||
/// in action masks, and applies the action to the board via AbstractBoard.MakeMove().
|
|||
/// </summary>
|
|||
public class Match3Actuator : IActuator |
|||
{ |
|||
private AbstractBoard m_Board; |
|||
private ActionSpec m_ActionSpec; |
|||
private bool m_ForceHeuristic; |
|||
private System.Random m_Random; |
|||
private Agent m_Agent; |
|||
|
|||
private int m_Rows; |
|||
private int m_Columns; |
|||
private int m_NumCellTypes; |
|||
|
|||
/// <summary>
|
|||
/// Create a Match3Actuator.
|
|||
/// </summary>
|
|||
/// <param name="board"></param>
|
|||
/// <param name="forceHeuristic">Whether the inference action should be ignored and the Agent's Heuristic
|
|||
/// should be called. This should only be used for generating comparison stats of the Heuristic.</param>
|
|||
/// <param name="agent"></param>
|
|||
/// <param name="name"></param>
|
|||
public Match3Actuator(AbstractBoard board, bool forceHeuristic, Agent agent, string name) |
|||
{ |
|||
m_Board = board; |
|||
m_Rows = board.Rows; |
|||
m_Columns = board.Columns; |
|||
m_NumCellTypes = board.NumCellTypes; |
|||
Name = name; |
|||
|
|||
m_ForceHeuristic = forceHeuristic; |
|||
m_Agent = agent; |
|||
|
|||
var numMoves = Move.NumPotentialMoves(m_Board.Rows, m_Board.Columns); |
|||
m_ActionSpec = ActionSpec.MakeDiscrete(numMoves); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public ActionSpec ActionSpec => m_ActionSpec; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void OnActionReceived(ActionBuffers actions) |
|||
{ |
|||
if (m_ForceHeuristic) |
|||
{ |
|||
m_Agent.Heuristic(actions); |
|||
} |
|||
var moveIndex = actions.DiscreteActions[0]; |
|||
|
|||
if (m_Board.Rows != m_Rows || m_Board.Columns != m_Columns || m_Board.NumCellTypes != m_NumCellTypes) |
|||
{ |
|||
Debug.LogWarning( |
|||
$"Board shape changes since actuator initialization. This may cause unexpected results. " + |
|||
$"Old shape: Rows={m_Rows} Columns={m_Columns}, NumCellTypes={m_NumCellTypes} " + |
|||
$"Current shape: Rows={m_Board.Rows} Columns={m_Board.Columns}, NumCellTypes={m_Board.NumCellTypes}" |
|||
); |
|||
} |
|||
|
|||
Move move = Move.FromMoveIndex(moveIndex, m_Rows, m_Columns); |
|||
m_Board.MakeMove(move); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void WriteDiscreteActionMask(IDiscreteActionMask actionMask) |
|||
{ |
|||
using (TimerStack.Instance.Scoped("WriteDiscreteActionMask")) |
|||
{ |
|||
actionMask.WriteMask(0, InvalidMoveIndices()); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public string Name { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public void ResetData() |
|||
{ |
|||
} |
|||
|
|||
IEnumerable<int> InvalidMoveIndices() |
|||
{ |
|||
var numValidMoves = m_Board.NumMoves(); |
|||
|
|||
foreach (var move in m_Board.InvalidMoves()) |
|||
{ |
|||
numValidMoves--; |
|||
if (numValidMoves == 0) |
|||
{ |
|||
// If all the moves are invalid and we mask all the actions out, this will cause an assert
|
|||
// later on in IDiscreteActionMask. Instead, fire a callback to the user if they provided one,
|
|||
// (or log a warning if not) and leave the last action unmasked. This isn't great, but
|
|||
// an invalid move should be easier to handle than an exception..
|
|||
if (m_Board.OnNoValidMovesAction != null) |
|||
{ |
|||
m_Board.OnNoValidMovesAction(); |
|||
} |
|||
else |
|||
{ |
|||
Debug.LogWarning( |
|||
"No valid moves are available. The last action will be left unmasked, so " + |
|||
"an invalid move will be passed to AbstractBoard.MakeMove()." |
|||
); |
|||
} |
|||
// This means the last move won't be returned as an invalid index.
|
|||
yield break; |
|||
} |
|||
yield return move.MoveIndex; |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 9083fa4c35dc499aa5a86d8e7447c7cf |
|||
timeCreated: 1600906373 |
|
|||
using Unity.MLAgents.Actuators; |
|||
using UnityEngine; |
|||
using UnityEngine.Serialization; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Match3 |
|||
{ |
|||
/// <summary>
|
|||
/// Actuator component for a Match 3 game. Generates a Match3Actuator at runtime.
|
|||
/// </summary>
|
|||
public class Match3ActuatorComponent : ActuatorComponent |
|||
{ |
|||
/// <summary>
|
|||
/// Name of the generated Match3Actuator object.
|
|||
/// Note that changing this at runtime does not affect how the Agent sorts the actuators.
|
|||
/// </summary>
|
|||
public string ActuatorName = "Match3 Actuator"; |
|||
|
|||
/// <summary>
|
|||
/// Force using the Agent's Heuristic() method to decide the action. This should only be used in testing.
|
|||
/// </summary>
|
|||
[FormerlySerializedAs("ForceRandom")] |
|||
[Tooltip("Force using the Agent's Heuristic() method to decide the action. This should only be used in testing.")] |
|||
public bool ForceHeuristic = false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override IActuator CreateActuator() |
|||
{ |
|||
var board = GetComponent<AbstractBoard>(); |
|||
var agent = GetComponentInParent<Agent>(); |
|||
return new Match3Actuator(board, ForceHeuristic, agent, ActuatorName); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override ActionSpec ActionSpec |
|||
{ |
|||
get |
|||
{ |
|||
var board = GetComponent<AbstractBoard>(); |
|||
if (board == null) |
|||
{ |
|||
return ActionSpec.MakeContinuous(0); |
|||
} |
|||
|
|||
var numMoves = Move.NumPotentialMoves(board.Rows, board.Columns); |
|||
return ActionSpec.MakeDiscrete(numMoves); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 08e4b0da54cb4d56bfcbae22dd49ab8d |
|||
timeCreated: 1600906388 |
|
|||
using System.Collections.Generic; |
|||
using Unity.MLAgents.Sensors; |
|||
using UnityEngine; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Match3 |
|||
{ |
|||
/// <summary>
|
|||
/// Type of observations to generate.
|
|||
///
|
|||
/// </summary>
|
|||
public enum Match3ObservationType |
|||
{ |
|||
/// <summary>
|
|||
/// Generate a one-hot encoding of the cell type for each cell on the board. If there are special types,
|
|||
/// these will also be one-hot encoded.
|
|||
/// </summary>
|
|||
Vector, |
|||
|
|||
/// <summary>
|
|||
/// Generate a one-hot encoding of the cell type for each cell on the board, but arranged as
|
|||
/// a Rows x Columns visual observation. If there are special types, these will also be one-hot encoded.
|
|||
/// </summary>
|
|||
UncompressedVisual, |
|||
|
|||
/// <summary>
|
|||
/// Generate a one-hot encoding of the cell type for each cell on the board, but arranged as
|
|||
/// a Rows x Columns visual observation. If there are special types, these will also be one-hot encoded.
|
|||
/// During training, these will be sent as a concatenated series of PNG images, with 3 channels per image.
|
|||
/// </summary>
|
|||
CompressedVisual |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sensor for Match3 games. Can generate either vector, compressed visual,
|
|||
/// or uncompressed visual observations. Uses AbstractBoard.GetCellType()
|
|||
/// and AbstractBoard.GetSpecialType() to determine the observation values.
|
|||
/// </summary>
|
|||
public class Match3Sensor : ISparseChannelSensor |
|||
{ |
|||
private Match3ObservationType m_ObservationType; |
|||
private AbstractBoard m_Board; |
|||
private int[] m_Shape; |
|||
private int[] m_SparseChannelMapping; |
|||
private string m_Name; |
|||
|
|||
private int m_Rows; |
|||
private int m_Columns; |
|||
private int m_NumCellTypes; |
|||
private int m_NumSpecialTypes; |
|||
private ISparseChannelSensor sparseChannelSensorImplementation; |
|||
|
|||
private int SpecialTypeSize |
|||
{ |
|||
get { return m_NumSpecialTypes == 0 ? 0 : m_NumSpecialTypes + 1; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create a sensor for the board with the specified observation type.
|
|||
/// </summary>
|
|||
/// <param name="board"></param>
|
|||
/// <param name="obsType"></param>
|
|||
/// <param name="name"></param>
|
|||
public Match3Sensor(AbstractBoard board, Match3ObservationType obsType, string name) |
|||
{ |
|||
m_Board = board; |
|||
m_Name = name; |
|||
m_Rows = board.Rows; |
|||
m_Columns = board.Columns; |
|||
m_NumCellTypes = board.NumCellTypes; |
|||
m_NumSpecialTypes = board.NumSpecialTypes; |
|||
|
|||
m_ObservationType = obsType; |
|||
m_Shape = obsType == Match3ObservationType.Vector ? |
|||
new[] { m_Rows * m_Columns * (m_NumCellTypes + SpecialTypeSize) } : |
|||
new[] { m_Rows, m_Columns, m_NumCellTypes + SpecialTypeSize }; |
|||
|
|||
// See comment in GetCompressedObservation()
|
|||
var cellTypePaddedSize = 3 * ((m_NumCellTypes + 2) / 3); |
|||
m_SparseChannelMapping = new int[cellTypePaddedSize + SpecialTypeSize]; |
|||
// If we have 4 cell types and 2 special types (3 special size), we'd have
|
|||
// [0, 1, 2, 3, -1, -1, 4, 5, 6]
|
|||
for (var i = 0; i < m_NumCellTypes; i++) |
|||
{ |
|||
m_SparseChannelMapping[i] = i; |
|||
} |
|||
|
|||
for (var i = m_NumCellTypes; i < cellTypePaddedSize; i++) |
|||
{ |
|||
m_SparseChannelMapping[i] = -1; |
|||
} |
|||
|
|||
for (var i = 0; i < SpecialTypeSize; i++) |
|||
{ |
|||
m_SparseChannelMapping[cellTypePaddedSize + i] = i + m_NumCellTypes; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int[] GetObservationShape() |
|||
{ |
|||
return m_Shape; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int Write(ObservationWriter writer) |
|||
{ |
|||
if (m_Board.Rows != m_Rows || m_Board.Columns != m_Columns || m_Board.NumCellTypes != m_NumCellTypes) |
|||
{ |
|||
Debug.LogWarning( |
|||
$"Board shape changes since sensor initialization. This may cause unexpected results. " + |
|||
$"Old shape: Rows={m_Rows} Columns={m_Columns}, NumCellTypes={m_NumCellTypes} " + |
|||
$"Current shape: Rows={m_Board.Rows} Columns={m_Board.Columns}, NumCellTypes={m_Board.NumCellTypes}" |
|||
); |
|||
} |
|||
|
|||
if (m_ObservationType == Match3ObservationType.Vector) |
|||
{ |
|||
int offset = 0; |
|||
for (var r = 0; r < m_Rows; r++) |
|||
{ |
|||
for (var c = 0; c < m_Columns; c++) |
|||
{ |
|||
var val = m_Board.GetCellType(r, c); |
|||
for (var i = 0; i < m_NumCellTypes; i++) |
|||
{ |
|||
writer[offset] = (i == val) ? 1.0f : 0.0f; |
|||
offset++; |
|||
} |
|||
|
|||
if (m_NumSpecialTypes > 0) |
|||
{ |
|||
var special = m_Board.GetSpecialType(r, c); |
|||
for (var i = 0; i < SpecialTypeSize; i++) |
|||
{ |
|||
writer[offset] = (i == special) ? 1.0f : 0.0f; |
|||
offset++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return offset; |
|||
} |
|||
else |
|||
{ |
|||
// TODO combine loops? Only difference is inner-most statement.
|
|||
int offset = 0; |
|||
for (var r = 0; r < m_Rows; r++) |
|||
{ |
|||
for (var c = 0; c < m_Columns; c++) |
|||
{ |
|||
var val = m_Board.GetCellType(r, c); |
|||
for (var i = 0; i < m_NumCellTypes; i++) |
|||
{ |
|||
writer[r, c, i] = (i == val) ? 1.0f : 0.0f; |
|||
offset++; |
|||
} |
|||
|
|||
if (m_NumSpecialTypes > 0) |
|||
{ |
|||
var special = m_Board.GetSpecialType(r, c); |
|||
for (var i = 0; i < SpecialTypeSize; i++) |
|||
{ |
|||
writer[offset] = (i == special) ? 1.0f : 0.0f; |
|||
offset++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return offset; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public byte[] GetCompressedObservation() |
|||
{ |
|||
var height = m_Rows; |
|||
var width = m_Columns; |
|||
var tempTexture = new Texture2D(width, height, TextureFormat.RGB24, false); |
|||
var converter = new OneHotToTextureUtil(height, width); |
|||
var bytesOut = new List<byte>(); |
|||
|
|||
// Encode the cell types and special types as separate batches of PNGs
|
|||
// This is potentially wasteful, e.g. if there are 4 cell types and 1 special type, we could
|
|||
// fit in in 2 images, but we'll use 3 here (2 PNGs for the 4 cell type channels, and 1 for
|
|||
// the special types). Note that we have to also implement the sparse channel mapping.
|
|||
// Optimize this it later.
|
|||
var numCellImages = (m_NumCellTypes + 2) / 3; |
|||
for (var i = 0; i < numCellImages; i++) |
|||
{ |
|||
converter.EncodeToTexture(m_Board.GetCellType, tempTexture, 3 * i); |
|||
bytesOut.AddRange(tempTexture.EncodeToPNG()); |
|||
} |
|||
|
|||
var numSpecialImages = (SpecialTypeSize + 2) / 3; |
|||
for (var i = 0; i < numSpecialImages; i++) |
|||
{ |
|||
converter.EncodeToTexture(m_Board.GetSpecialType, tempTexture, 3 * i); |
|||
bytesOut.AddRange(tempTexture.EncodeToPNG()); |
|||
} |
|||
|
|||
DestroyTexture(tempTexture); |
|||
return bytesOut.ToArray(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Update() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Reset() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public SensorCompressionType GetCompressionType() |
|||
{ |
|||
return m_ObservationType == Match3ObservationType.CompressedVisual ? |
|||
SensorCompressionType.PNG : |
|||
SensorCompressionType.None; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public string GetName() |
|||
{ |
|||
return m_Name; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int[] GetCompressedChannelMapping() |
|||
{ |
|||
return m_SparseChannelMapping; |
|||
} |
|||
|
|||
static void DestroyTexture(Texture2D texture) |
|||
{ |
|||
if (Application.isEditor) |
|||
{ |
|||
// Edit Mode tests complain if we use Destroy()
|
|||
Object.DestroyImmediate(texture); |
|||
} |
|||
else |
|||
{ |
|||
Object.Destroy(texture); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Utility class for converting a 2D array of ints representing a one-hot encoding into
|
|||
/// a texture, suitable for conversion to PNGs for observations.
|
|||
/// Works by encoding 3 values at a time as pixels in the texture, thus it should be
|
|||
/// called (maxValue + 2) / 3 times, increasing the channelOffset by 3 each time.
|
|||
/// </summary>
|
|||
internal class OneHotToTextureUtil |
|||
{ |
|||
Color[] m_Colors; |
|||
int m_Height; |
|||
int m_Width; |
|||
private static Color[] s_OneHotColors = { Color.red, Color.green, Color.blue }; |
|||
|
|||
public delegate int GridValueProvider(int x, int y); |
|||
|
|||
|
|||
public OneHotToTextureUtil(int height, int width) |
|||
{ |
|||
m_Colors = new Color[height * width]; |
|||
m_Height = height; |
|||
m_Width = width; |
|||
} |
|||
|
|||
public void EncodeToTexture(GridValueProvider gridValueProvider, Texture2D texture, int channelOffset) |
|||
{ |
|||
var i = 0; |
|||
// There's an implicit flip converting to PNG from texture, so make sure we
|
|||
// counteract that when forming the texture by iterating through h in reverse.
|
|||
for (var h = m_Height - 1; h >= 0; h--) |
|||
{ |
|||
for (var w = 0; w < m_Width; w++) |
|||
{ |
|||
int oneHotValue = gridValueProvider(h, w); |
|||
if (oneHotValue < channelOffset || oneHotValue >= channelOffset + 3) |
|||
{ |
|||
m_Colors[i++] = Color.black; |
|||
} |
|||
else |
|||
{ |
|||
m_Colors[i++] = s_OneHotColors[oneHotValue - channelOffset]; |
|||
} |
|||
} |
|||
} |
|||
texture.SetPixels(m_Colors); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 795ad5f211e344e5bf3049abd9499721 |
|||
timeCreated: 1600906663 |
|
|||
using Unity.MLAgents.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Match3 |
|||
{ |
|||
/// <summary>
|
|||
/// Sensor component for a Match3 game.
|
|||
/// </summary>
|
|||
public class Match3SensorComponent : SensorComponent |
|||
{ |
|||
/// <summary>
|
|||
/// Name of the generated Match3Sensor object.
|
|||
/// Note that changing this at runtime does not affect how the Agent sorts the sensors.
|
|||
/// </summary>
|
|||
public string SensorName = "Match3 Sensor"; |
|||
|
|||
/// <summary>
|
|||
/// Type of observation to generate.
|
|||
/// </summary>
|
|||
public Match3ObservationType ObservationType = Match3ObservationType.Vector; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override ISensor CreateSensor() |
|||
{ |
|||
var board = GetComponent<AbstractBoard>(); |
|||
return new Match3Sensor(board, ObservationType, SensorName); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int[] GetObservationShape() |
|||
{ |
|||
var board = GetComponent<AbstractBoard>(); |
|||
if (board == null) |
|||
{ |
|||
return System.Array.Empty<int>(); |
|||
} |
|||
|
|||
var specialSize = board.NumSpecialTypes == 0 ? 0 : board.NumSpecialTypes + 1; |
|||
return ObservationType == Match3ObservationType.Vector ? |
|||
new[] { board.Rows * board.Columns * (board.NumCellTypes + specialSize) } : |
|||
new[] { board.Rows, board.Columns, board.NumCellTypes + specialSize }; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 530d2f105aa145bd8a00e021bdd925fd |
|||
timeCreated: 1600906676 |
|
|||
using System; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Match3 |
|||
{ |
|||
/// <summary>
|
|||
/// Directions for a Move.
|
|||
/// </summary>
|
|||
public enum Direction |
|||
{ |
|||
/// <summary>
|
|||
/// Move up (increasing row direction).
|
|||
/// </summary>
|
|||
Up, |
|||
|
|||
/// <summary>
|
|||
/// Move down (decreasing row direction).
|
|||
/// </summary>
|
|||
Down, // -row direction
|
|||
|
|||
/// <summary>
|
|||
/// Move left (decreasing column direction).
|
|||
/// </summary>
|
|||
Left, // -column direction
|
|||
|
|||
/// <summary>
|
|||
/// Move right (increasing column direction).
|
|||
/// </summary>
|
|||
Right, // +column direction
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Struct that encapsulates a swap of adjacent cells.
|
|||
/// A Move can be constructed from either a starting row, column, and direction,
|
|||
/// or from a "move index" between 0 and NumPotentialMoves()-1.
|
|||
/// Moves are enumerated as the internal edges of the game grid.
|
|||
/// Left/right moves come first. There are (maxCols - 1) * maxRows of these.
|
|||
/// Up/down moves are next. There are (maxRows - 1) * maxCols of these.
|
|||
/// </summary>
|
|||
public struct Move |
|||
{ |
|||
/// <summary>
|
|||
/// Index of the move, from 0 to NumPotentialMoves-1.
|
|||
/// </summary>
|
|||
public int MoveIndex; |
|||
|
|||
/// <summary>
|
|||
/// Row of the cell that will be moved.
|
|||
/// </summary>
|
|||
public int Row; |
|||
|
|||
/// <summary>
|
|||
/// Column of the cell that will be moved.
|
|||
/// </summary>
|
|||
public int Column; |
|||
|
|||
/// <summary>
|
|||
/// Direction that the cell will be moved.
|
|||
/// </summary>
|
|||
public Direction Direction; |
|||
|
|||
/// <summary>
|
|||
/// Construct a Move from its move index and the board size.
|
|||
/// This is useful for iterating through all the Moves on a board, or constructing
|
|||
/// the Move corresponding to an Agent decision.
|
|||
/// </summary>
|
|||
/// <param name="moveIndex">Must be between 0 and NumPotentialMoves(maxRows, maxCols).</param>
|
|||
/// <param name="maxRows"></param>
|
|||
/// <param name="maxCols"></param>
|
|||
/// <returns></returns>
|
|||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|||
public static Move FromMoveIndex(int moveIndex, int maxRows, int maxCols) |
|||
{ |
|||
if (moveIndex < 0 || moveIndex >= NumPotentialMoves(maxRows, maxCols)) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("Invalid move index."); |
|||
} |
|||
Direction dir; |
|||
int row, col; |
|||
if (moveIndex < (maxCols - 1) * maxRows) |
|||
{ |
|||
dir = Direction.Right; |
|||
col = moveIndex % (maxCols - 1); |
|||
row = moveIndex / (maxCols - 1); |
|||
} |
|||
else |
|||
{ |
|||
dir = Direction.Up; |
|||
var offset = moveIndex - (maxCols - 1) * maxRows; |
|||
col = offset % maxCols; |
|||
row = offset / maxCols; |
|||
} |
|||
return new Move |
|||
{ |
|||
MoveIndex = moveIndex, |
|||
Direction = dir, |
|||
Row = row, |
|||
Column = col |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Increment the Move to the next MoveIndex, and update the Row, Column, and Direction accordingly.
|
|||
/// </summary>
|
|||
/// <param name="maxRows"></param>
|
|||
/// <param name="maxCols"></param>
|
|||
public void Next(int maxRows, int maxCols) |
|||
{ |
|||
var switchoverIndex = (maxCols - 1) * maxRows; |
|||
|
|||
MoveIndex++; |
|||
if (MoveIndex < switchoverIndex) |
|||
{ |
|||
Column++; |
|||
if (Column == maxCols - 1) |
|||
{ |
|||
Row++; |
|||
Column = 0; |
|||
} |
|||
} |
|||
else if (MoveIndex == switchoverIndex) |
|||
{ |
|||
// switch from moving right to moving up
|
|||
Row = 0; |
|||
Column = 0; |
|||
Direction = Direction.Up; |
|||
} |
|||
else |
|||
{ |
|||
Column++; |
|||
if (Column == maxCols) |
|||
{ |
|||
Row++; |
|||
Column = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Construct a Move from the row, column, and direction.
|
|||
/// </summary>
|
|||
/// <param name="row"></param>
|
|||
/// <param name="col"></param>
|
|||
/// <param name="dir"></param>
|
|||
/// <param name="maxRows"></param>
|
|||
/// <param name="maxCols"></param>
|
|||
/// <returns></returns>
|
|||
public static Move FromPositionAndDirection(int row, int col, Direction dir, int maxRows, int maxCols) |
|||
{ |
|||
|
|||
// Check for out-of-bounds
|
|||
if (row < 0 || row >= maxRows) |
|||
{ |
|||
throw new IndexOutOfRangeException($"row was {row}, but must be between 0 and {maxRows - 1}."); |
|||
} |
|||
|
|||
if (col < 0 || col >= maxCols) |
|||
{ |
|||
throw new IndexOutOfRangeException($"col was {col}, but must be between 0 and {maxCols - 1}."); |
|||
} |
|||
|
|||
// Check moves that would go out of bounds e.g. col == 0 and dir == Left
|
|||
if ( |
|||
row == 0 && dir == Direction.Down || |
|||
row == maxRows - 1 && dir == Direction.Up || |
|||
col == 0 && dir == Direction.Left || |
|||
col == maxCols - 1 && dir == Direction.Right |
|||
) |
|||
{ |
|||
throw new IndexOutOfRangeException($"Cannot move cell at row={row} col={col} in Direction={dir}"); |
|||
} |
|||
|
|||
// Normalize - only consider Right and Up
|
|||
if (dir == Direction.Left) |
|||
{ |
|||
dir = Direction.Right; |
|||
col = col - 1; |
|||
} |
|||
else if (dir == Direction.Down) |
|||
{ |
|||
dir = Direction.Up; |
|||
row = row - 1; |
|||
} |
|||
|
|||
int moveIndex; |
|||
if (dir == Direction.Right) |
|||
{ |
|||
moveIndex = col + row * (maxCols - 1); |
|||
} |
|||
else |
|||
{ |
|||
var offset = (maxCols - 1) * maxRows; |
|||
moveIndex = offset + col + row * maxCols; |
|||
} |
|||
|
|||
return new Move |
|||
{ |
|||
Row = row, |
|||
Column = col, |
|||
Direction = dir, |
|||
MoveIndex = moveIndex, |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the other row and column that correspond to this move.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|||
public (int Row, int Column) OtherCell() |
|||
{ |
|||
switch (Direction) |
|||
{ |
|||
case Direction.Up: |
|||
return (Row + 1, Column); |
|||
case Direction.Down: |
|||
return (Row - 1, Column); |
|||
case Direction.Left: |
|||
return (Row, Column - 1); |
|||
case Direction.Right: |
|||
return (Row, Column + 1); |
|||
default: |
|||
throw new ArgumentOutOfRangeException(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the opposite direction of this move.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|||
public Direction OtherDirection() |
|||
{ |
|||
switch (Direction) |
|||
{ |
|||
case Direction.Up: |
|||
return Direction.Down; |
|||
case Direction.Down: |
|||
return Direction.Up; |
|||
case Direction.Left: |
|||
return Direction.Right; |
|||
case Direction.Right: |
|||
return Direction.Left; |
|||
default: |
|||
throw new ArgumentOutOfRangeException(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Return the number of potential moves for a board of the given size.
|
|||
/// This is equivalent to the number of internal edges in the board.
|
|||
/// </summary>
|
|||
/// <param name="maxRows"></param>
|
|||
/// <param name="maxCols"></param>
|
|||
/// <returns></returns>
|
|||
public static int NumPotentialMoves(int maxRows, int maxCols) |
|||
{ |
|||
return maxRows * (maxCols - 1) + (maxRows - 1) * (maxCols); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 41d6d7b9e07c4ef1ae075c74a906906b |
|||
timeCreated: 1600466100 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
using NUnit.Framework; |
|||
using Unity.MLAgents.Extensions.Match3; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.Match3 |
|||
{ |
|||
internal class StringBoard : AbstractBoard |
|||
{ |
|||
private string[] m_Board; |
|||
private string[] m_Special; |
|||
|
|||
/// <summary>
|
|||
/// Convert a string like "000\n010\n000" to a board representation
|
|||
/// Row 0 is considered the bottom row
|
|||
/// </summary>
|
|||
/// <param name="newBoard"></param>
|
|||
public void SetBoard(string newBoard) |
|||
{ |
|||
m_Board = newBoard.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); |
|||
Rows = m_Board.Length; |
|||
Columns = m_Board[0].Length; |
|||
NumCellTypes = 0; |
|||
for (var r = 0; r < Rows; r++) |
|||
{ |
|||
for (var c = 0; c < Columns; c++) |
|||
{ |
|||
NumCellTypes = Mathf.Max(NumCellTypes, 1 + GetCellType(r, c)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void SetSpecial(string newSpecial) |
|||
{ |
|||
m_Special = newSpecial.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); |
|||
Debug.Assert(Rows == m_Special.Length); |
|||
Debug.Assert(Columns == m_Special[0].Length); |
|||
NumSpecialTypes = 0; |
|||
for (var r = 0; r < Rows; r++) |
|||
{ |
|||
for (var c = 0; c < Columns; c++) |
|||
{ |
|||
NumSpecialTypes = Mathf.Max(NumSpecialTypes, GetSpecialType(r, c)); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
public override bool MakeMove(Move m) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
public override bool IsMoveValid(Move m) |
|||
{ |
|||
return SimpleIsMoveValid(m); |
|||
} |
|||
|
|||
public override int GetCellType(int row, int col) |
|||
{ |
|||
var character = m_Board[m_Board.Length - 1 - row][col]; |
|||
return (int)(character - '0'); |
|||
} |
|||
|
|||
public override int GetSpecialType(int row, int col) |
|||
{ |
|||
var character = m_Special[m_Board.Length - 1 - row][col]; |
|||
return (int)(character - '0'); |
|||
} |
|||
|
|||
} |
|||
|
|||
public class AbstractBoardTests |
|||
{ |
|||
[Test] |
|||
public void TestBoardInit() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
|
|||
Assert.AreEqual(3, board.Rows); |
|||
Assert.AreEqual(3, board.Columns); |
|||
Assert.AreEqual(2, board.NumCellTypes); |
|||
for (var r = 0; r < 3; r++) |
|||
{ |
|||
for (var c = 0; c < 3; c++) |
|||
{ |
|||
var expected = (r == 0 && c == 1) ? 1 : 0; |
|||
Assert.AreEqual(expected, board.GetCellType(r, c)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Test] |
|||
public void TestCheckValidMoves() |
|||
{ |
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
|
|||
var boardString = |
|||
@"0105
|
|||
1024 |
|||
0203 |
|||
2022";
|
|||
board.SetBoard(boardString); |
|||
|
|||
var validMoves = new[] |
|||
{ |
|||
Move.FromPositionAndDirection(2, 1, Direction.Up, board.Rows, board.Columns), // equivalent to (3, 1, Down)
|
|||
Move.FromPositionAndDirection(2, 1, Direction.Left, board.Rows, board.Columns), // equivalent to (2, 0, Right)
|
|||
Move.FromPositionAndDirection(2, 1, Direction.Down, board.Rows, board.Columns), // equivalent to (1, 1, Up)
|
|||
Move.FromPositionAndDirection(2, 1, Direction.Right, board.Rows, board.Columns), |
|||
Move.FromPositionAndDirection(1, 1, Direction.Down, board.Rows, board.Columns), |
|||
Move.FromPositionAndDirection(1, 1, Direction.Left, board.Rows, board.Columns), |
|||
Move.FromPositionAndDirection(1, 1, Direction.Right, board.Rows, board.Columns), |
|||
Move.FromPositionAndDirection(0, 1, Direction.Left, board.Rows, board.Columns), |
|||
}; |
|||
|
|||
foreach (var m in validMoves) |
|||
{ |
|||
Assert.IsTrue(board.IsMoveValid(m)); |
|||
} |
|||
|
|||
// Run through all moves and make sure those are the only valid ones
|
|||
HashSet<int> validIndices = new HashSet<int>(); |
|||
foreach (var m in validMoves) |
|||
{ |
|||
validIndices.Add(m.MoveIndex); |
|||
} |
|||
|
|||
foreach (var move in board.AllMoves()) |
|||
{ |
|||
var expected = validIndices.Contains(move.MoveIndex); |
|||
Assert.AreEqual(expected, board.IsMoveValid(move), $"({move.Row}, {move.Column}, {move.Direction})"); |
|||
} |
|||
|
|||
HashSet<int> validIndicesFromIterator = new HashSet<int>(); |
|||
foreach (var move in board.ValidMoves()) |
|||
{ |
|||
validIndicesFromIterator.Add(move.MoveIndex); |
|||
} |
|||
Assert.IsTrue(validIndices.SetEquals(validIndicesFromIterator)); |
|||
} |
|||
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a6d0404471364cd5b0b86ef72e6fe653 |
|||
timeCreated: 1601332740 |
|
|||
using NUnit.Framework; |
|||
using Unity.MLAgents.Extensions.Match3; |
|||
using UnityEngine; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.Match3 |
|||
{ |
|||
internal class SimpleBoard : AbstractBoard |
|||
{ |
|||
public int LastMoveIndex; |
|||
public bool MovesAreValid = true; |
|||
|
|||
public bool CallbackCalled; |
|||
|
|||
public override int GetCellType(int row, int col) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
public override int GetSpecialType(int row, int col) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
public override bool IsMoveValid(Move m) |
|||
{ |
|||
return MovesAreValid; |
|||
} |
|||
|
|||
public override bool MakeMove(Move m) |
|||
{ |
|||
LastMoveIndex = m.MoveIndex; |
|||
return MovesAreValid; |
|||
} |
|||
|
|||
public void Callback() |
|||
{ |
|||
CallbackCalled = true; |
|||
} |
|||
} |
|||
|
|||
public class Match3ActuatorTests |
|||
{ |
|||
[SetUp] |
|||
public void SetUp() |
|||
{ |
|||
if (Academy.IsInitialized) |
|||
{ |
|||
Academy.Instance.Dispose(); |
|||
} |
|||
} |
|||
|
|||
[TestCase(true)] |
|||
[TestCase(false)] |
|||
public void TestValidMoves(bool movesAreValid) |
|||
{ |
|||
// Check that a board with no valid moves doesn't raise an exception.
|
|||
var gameObj = new GameObject(); |
|||
var board = gameObj.AddComponent<SimpleBoard>(); |
|||
var agent = gameObj.AddComponent<Agent>(); |
|||
gameObj.AddComponent<Match3ActuatorComponent>(); |
|||
|
|||
board.Rows = 5; |
|||
board.Columns = 5; |
|||
board.NumCellTypes = 5; |
|||
board.NumSpecialTypes = 0; |
|||
|
|||
board.MovesAreValid = movesAreValid; |
|||
board.OnNoValidMovesAction = board.Callback; |
|||
board.LastMoveIndex = -1; |
|||
|
|||
agent.LazyInitialize(); |
|||
agent.RequestDecision(); |
|||
Academy.Instance.EnvironmentStep(); |
|||
|
|||
if (movesAreValid) |
|||
{ |
|||
Assert.IsFalse(board.CallbackCalled); |
|||
} |
|||
else |
|||
{ |
|||
Assert.IsTrue(board.CallbackCalled); |
|||
} |
|||
Assert.AreNotEqual(-1, board.LastMoveIndex); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestActionSpec() |
|||
{ |
|||
var gameObj = new GameObject(); |
|||
var board = gameObj.AddComponent<SimpleBoard>(); |
|||
var actuator = gameObj.AddComponent<Match3ActuatorComponent>(); |
|||
|
|||
board.Rows = 5; |
|||
board.Columns = 5; |
|||
board.NumCellTypes = 5; |
|||
board.NumSpecialTypes = 0; |
|||
|
|||
var actionSpec = actuator.ActionSpec; |
|||
Assert.AreEqual(1, actionSpec.NumDiscreteActions); |
|||
Assert.AreEqual(board.NumMoves(), actionSpec.BranchSizes[0]); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestActionSpecNullBoard() |
|||
{ |
|||
var gameObj = new GameObject(); |
|||
var actuator = gameObj.AddComponent<Match3ActuatorComponent>(); |
|||
|
|||
var actionSpec = actuator.ActionSpec; |
|||
Assert.AreEqual(0, actionSpec.NumDiscreteActions); |
|||
Assert.AreEqual(0, actionSpec.NumContinuousActions); |
|||
} |
|||
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 2edf24df24ac426085cb31a94d063683 |
|||
timeCreated: 1603392289 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using NUnit.Framework; |
|||
using Unity.MLAgents.Actuators; |
|||
using Unity.MLAgents.Extensions.Match3; |
|||
using UnityEngine; |
|||
using Unity.MLAgents.Extensions.Tests.Sensors; |
|||
using Unity.MLAgents.Sensors; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.Match3 |
|||
{ |
|||
public class Match3SensorTests |
|||
{ |
|||
// Whether the expected PNG data should be written to a file.
|
|||
// Only set this to true if the compressed observation format changes.
|
|||
private bool WritePNGDataToFile = false; |
|||
|
|||
[Test] |
|||
public void TestVectorObservations() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
|
|||
var sensorComponent = gameObj.AddComponent<Match3SensorComponent>(); |
|||
sensorComponent.ObservationType = Match3ObservationType.Vector; |
|||
var sensor = sensorComponent.CreateSensor(); |
|||
|
|||
var expectedShape = new[] { 3 * 3 * 2 }; |
|||
Assert.AreEqual(expectedShape, sensorComponent.GetObservationShape()); |
|||
Assert.AreEqual(expectedShape, sensor.GetObservationShape()); |
|||
|
|||
var expectedObs = new float[] |
|||
{ |
|||
1, 0, /**/ 0, 1, /**/ 1, 0, |
|||
1, 0, /**/ 1, 0, /**/ 1, 0, |
|||
1, 0, /**/ 1, 0, /**/ 1, 0, |
|||
}; |
|||
SensorTestHelper.CompareObservation(sensor, expectedObs); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestVectorObservationsSpecial() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var specialString = |
|||
@"010
|
|||
200 |
|||
000";
|
|||
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
board.SetSpecial(specialString); |
|||
|
|||
var sensorComponent = gameObj.AddComponent<Match3SensorComponent>(); |
|||
sensorComponent.ObservationType = Match3ObservationType.Vector; |
|||
var sensor = sensorComponent.CreateSensor(); |
|||
|
|||
var expectedShape = new[] { 3 * 3 * (2 + 3) }; |
|||
Assert.AreEqual(expectedShape, sensorComponent.GetObservationShape()); |
|||
Assert.AreEqual(expectedShape, sensor.GetObservationShape()); |
|||
|
|||
var expectedObs = new float[] |
|||
{ |
|||
1, 0, 1, 0, 0, /* (0, 0) */ 0, 1, 1, 0, 0, /* (0, 1) */ 1, 0, 1, 0, 0, /* (0, 0) */ |
|||
1, 0, 0, 0, 1, /* (0, 2) */ 1, 0, 1, 0, 0, /* (0, 0) */ 1, 0, 1, 0, 0, /* (0, 0) */ |
|||
1, 0, 1, 0, 0, /* (0, 0) */ 1, 0, 0, 1, 0, /* (0, 1) */ 1, 0, 1, 0, 0, /* (0, 0) */ |
|||
}; |
|||
SensorTestHelper.CompareObservation(sensor, expectedObs); |
|||
} |
|||
|
|||
|
|||
[Test] |
|||
public void TestVisualObservations() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
|
|||
var sensorComponent = gameObj.AddComponent<Match3SensorComponent>(); |
|||
sensorComponent.ObservationType = Match3ObservationType.UncompressedVisual; |
|||
var sensor = sensorComponent.CreateSensor(); |
|||
|
|||
var expectedShape = new[] { 3, 3, 2 }; |
|||
Assert.AreEqual(expectedShape, sensorComponent.GetObservationShape()); |
|||
Assert.AreEqual(expectedShape, sensor.GetObservationShape()); |
|||
|
|||
Assert.AreEqual(SensorCompressionType.None, sensor.GetCompressionType()); |
|||
|
|||
var expectedObs = new float[] |
|||
{ |
|||
1, 0, /**/ 0, 1, /**/ 1, 0, |
|||
1, 0, /**/ 1, 0, /**/ 1, 0, |
|||
1, 0, /**/ 1, 0, /**/ 1, 0, |
|||
}; |
|||
SensorTestHelper.CompareObservation(sensor, expectedObs); |
|||
|
|||
var expectedObs3D = new float[,,] |
|||
{ |
|||
{{1, 0}, {0, 1}, {1, 0}}, |
|||
{{1, 0}, {1, 0}, {1, 0}}, |
|||
{{1, 0}, {1, 0}, {1, 0}}, |
|||
}; |
|||
SensorTestHelper.CompareObservation(sensor, expectedObs3D); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestVisualObservationsSpecial() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var specialString = |
|||
@"010
|
|||
200 |
|||
000";
|
|||
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
board.SetSpecial(specialString); |
|||
|
|||
var sensorComponent = gameObj.AddComponent<Match3SensorComponent>(); |
|||
sensorComponent.ObservationType = Match3ObservationType.UncompressedVisual; |
|||
var sensor = sensorComponent.CreateSensor(); |
|||
|
|||
var expectedShape = new[] { 3, 3, 2 + 3 }; |
|||
Assert.AreEqual(expectedShape, sensorComponent.GetObservationShape()); |
|||
Assert.AreEqual(expectedShape, sensor.GetObservationShape()); |
|||
|
|||
Assert.AreEqual(SensorCompressionType.None, sensor.GetCompressionType()); |
|||
|
|||
var expectedObs = new float[] |
|||
{ |
|||
1, 0, 1, 0, 0, /* (0, 0) */ 0, 1, 1, 0, 0, /* (0, 1) */ 1, 0, 1, 0, 0, /* (0, 0) */ |
|||
1, 0, 0, 0, 1, /* (0, 2) */ 1, 0, 1, 0, 0, /* (0, 0) */ 1, 0, 1, 0, 0, /* (0, 0) */ |
|||
1, 0, 1, 0, 0, /* (0, 0) */ 1, 0, 0, 1, 0, /* (0, 1) */ 1, 0, 1, 0, 0, /* (0, 0) */ |
|||
}; |
|||
SensorTestHelper.CompareObservation(sensor, expectedObs); |
|||
|
|||
var expectedObs3D = new float[,,] |
|||
{ |
|||
{{1, 0, 1, 0, 0}, {0, 1, 1, 0, 0}, {1, 0, 1, 0, 0}}, |
|||
{{1, 0, 0, 0, 1}, {1, 0, 1, 0, 0}, {1, 0, 1, 0, 0}}, |
|||
{{1, 0, 1, 0, 0}, {1, 0, 0, 1, 0}, {1, 0, 1, 0, 0}}, |
|||
}; |
|||
SensorTestHelper.CompareObservation(sensor, expectedObs3D); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestCompressedVisualObservations() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
|
|||
var sensorComponent = gameObj.AddComponent<Match3SensorComponent>(); |
|||
sensorComponent.ObservationType = Match3ObservationType.CompressedVisual; |
|||
var sensor = sensorComponent.CreateSensor(); |
|||
|
|||
var expectedShape = new[] { 3, 3, 2 }; |
|||
Assert.AreEqual(expectedShape, sensorComponent.GetObservationShape()); |
|||
Assert.AreEqual(expectedShape, sensor.GetObservationShape()); |
|||
|
|||
Assert.AreEqual(SensorCompressionType.PNG, sensor.GetCompressionType()); |
|||
|
|||
var pngData = sensor.GetCompressedObservation(); |
|||
if (WritePNGDataToFile) |
|||
{ |
|||
// Enable this if the format of the observation changes
|
|||
SavePNGs(pngData, "match3obs"); |
|||
} |
|||
|
|||
var expectedPng = LoadPNGs("match3obs", 1); |
|||
Assert.AreEqual(expectedPng, pngData); |
|||
} |
|||
|
|||
|
|||
|
|||
[Test] |
|||
public void TestCompressedVisualObservationsSpecial() |
|||
{ |
|||
var boardString = |
|||
@"000
|
|||
000 |
|||
010";
|
|||
var specialString = |
|||
@"010
|
|||
200 |
|||
000";
|
|||
|
|||
var gameObj = new GameObject("board"); |
|||
var board = gameObj.AddComponent<StringBoard>(); |
|||
board.SetBoard(boardString); |
|||
board.SetSpecial(specialString); |
|||
|
|||
var sensorComponent = gameObj.AddComponent<Match3SensorComponent>(); |
|||
sensorComponent.ObservationType = Match3ObservationType.CompressedVisual; |
|||
var sensor = sensorComponent.CreateSensor(); |
|||
|
|||
var expectedShape = new[] { 3, 3, 2 + 3 }; |
|||
Assert.AreEqual(expectedShape, sensorComponent.GetObservationShape()); |
|||
Assert.AreEqual(expectedShape, sensor.GetObservationShape()); |
|||
|
|||
Assert.AreEqual(SensorCompressionType.PNG, sensor.GetCompressionType()); |
|||
|
|||
var concatenatedPngData = sensor.GetCompressedObservation(); |
|||
var pathPrefix = "match3obs_special"; |
|||
if (WritePNGDataToFile) |
|||
{ |
|||
// Enable this if the format of the observation changes
|
|||
SavePNGs(concatenatedPngData, pathPrefix); |
|||
} |
|||
var expectedPng = LoadPNGs(pathPrefix, 2); |
|||
Assert.AreEqual(expectedPng, concatenatedPngData); |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Helper method for un-concatenating PNG observations.
|
|||
/// </summary>
|
|||
/// <param name="concatenated"></param>
|
|||
/// <returns></returns>
|
|||
List<byte[]> SplitPNGs(byte[] concatenated) |
|||
{ |
|||
var pngsOut = new List<byte[]>(); |
|||
var pngHeader = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }; |
|||
|
|||
var current = new List<byte>(); |
|||
for (var i = 0; i < concatenated.Length; i++) |
|||
{ |
|||
current.Add(concatenated[i]); |
|||
|
|||
// Check if the header starts at the next position
|
|||
// If so, we'll start a new output array.
|
|||
var headerIsNext = false; |
|||
if (i + 1 < concatenated.Length - pngHeader.Length) |
|||
{ |
|||
for (var j = 0; j < pngHeader.Length; j++) |
|||
{ |
|||
if (concatenated[i + 1 + j] != pngHeader[j]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (j == pngHeader.Length - 1) |
|||
{ |
|||
headerIsNext = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (headerIsNext) |
|||
{ |
|||
pngsOut.Add(current.ToArray()); |
|||
current = new List<byte>(); |
|||
} |
|||
} |
|||
pngsOut.Add(current.ToArray()); |
|||
|
|||
return pngsOut; |
|||
} |
|||
|
|||
void SavePNGs(byte[] concatenatedPngData, string pathPrefix) |
|||
{ |
|||
var splitPngs = SplitPNGs(concatenatedPngData); |
|||
|
|||
for (var i = 0; i < splitPngs.Count; i++) |
|||
{ |
|||
var pngData = splitPngs[i]; |
|||
var path = $"Packages/com.unity.ml-agents.extensions/Tests/Editor/Match3/{pathPrefix}{i}.png"; |
|||
using (var sw = File.Create(path)) |
|||
{ |
|||
foreach (var b in pngData) |
|||
{ |
|||
sw.WriteByte(b); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
byte[] LoadPNGs(string pathPrefix, int numExpected) |
|||
{ |
|||
var bytesOut = new List<byte>(); |
|||
for (var i = 0; i < numExpected; i++) |
|||
{ |
|||
var path = $"Packages/com.unity.ml-agents.extensions/Tests/Editor/Match3/{pathPrefix}{i}.png"; |
|||
var res = File.ReadAllBytes(path); |
|||
bytesOut.AddRange(res); |
|||
} |
|||
|
|||
return bytesOut.ToArray(); |
|||
|
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: dfe94a9d6e994f408cb97d07dd44c994 |
|||
timeCreated: 1603493723 |
|
|||
using System; |
|||
using NUnit.Framework; |
|||
using Unity.MLAgents.Extensions.Match3; |
|||
|
|||
namespace Unity.MLAgents.Extensions.Tests.Match3 |
|||
{ |
|||
public class MoveTests |
|||
{ |
|||
[Test] |
|||
public void TestMoveEquivalence() |
|||
{ |
|||
var moveUp = Move.FromPositionAndDirection(1, 1, Direction.Up, 10, 10); |
|||
var moveDown = Move.FromPositionAndDirection(2, 1, Direction.Down, 10, 10); |
|||
Assert.AreEqual(moveUp.MoveIndex, moveDown.MoveIndex); |
|||
|
|||
var moveRight = Move.FromPositionAndDirection(1, 1, Direction.Right, 10, 10); |
|||
var moveLeft = Move.FromPositionAndDirection(1, 2, Direction.Left, 10, 10); |
|||
Assert.AreEqual(moveRight.MoveIndex, moveLeft.MoveIndex); |
|||
} |
|||
|
|||
[Test] |
|||
public void TestNext() |
|||
{ |
|||
var maxRows = 8; |
|||
var maxCols = 13; |
|||
// make sure using Next agrees with FromMoveIndex.
|
|||
var advanceMove = Move.FromMoveIndex(0, maxRows, maxCols); |
|||
for (var moveIndex = 0; moveIndex < Move.NumPotentialMoves(maxRows, maxCols); moveIndex++) |
|||
{ |
|||
var moveFromIndex = Move.FromMoveIndex(moveIndex, maxRows, maxCols); |
|||
Assert.AreEqual(advanceMove.MoveIndex, moveFromIndex.MoveIndex); |
|||
Assert.AreEqual(advanceMove.Row, moveFromIndex.Row); |
|||
Assert.AreEqual(advanceMove.Column, moveFromIndex.Column); |
|||
Assert.AreEqual(advanceMove.Direction, moveFromIndex.Direction); |
|||
|
|||
advanceMove.Next(maxRows, maxCols); |
|||
} |
|||
} |
|||
|
|||
// These are off the board
|
|||
[TestCase(-1, 5, Direction.Up)] |
|||
[TestCase(10, 5, Direction.Up)] |
|||
[TestCase(5, -1, Direction.Up)] |
|||
[TestCase(5, 10, Direction.Up)] |
|||
// These are on the board but would move off
|
|||
[TestCase(0, 5, Direction.Down)] |
|||
[TestCase(9, 5, Direction.Up)] |
|||
[TestCase(5, 0, Direction.Left)] |
|||
[TestCase(5, 9, Direction.Right)] |
|||
public void TestInvalidMove(int row, int col, Direction dir) |
|||
{ |
|||
int numRows = 10, numCols = 10; |
|||
Assert.Throws<IndexOutOfRangeException>(() => |
|||
{ |
|||
Move.FromPositionAndDirection(row, col, dir, numRows, numCols); |
|||
}); |
|||
|
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 42981032af6f4241ae20fe24e898f60b |
|||
timeCreated: 1601336681 |
|
|||
fileFormatVersion: 2 |
|||
guid: 3e1767bf6c63e46b1a16404dc1afe508 |
|||
TextureImporter: |
|||
fileIDToRecycleName: {} |
|||
externalObjects: {} |
|||
serializedVersion: 9 |
|||
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 |
|||
isReadable: 0 |
|||
streamingMipmaps: 0 |
|||
streamingMipmapsPriority: 0 |
|||
grayScaleToAlpha: 0 |
|||
generateCubemap: 6 |
|||
cubemapConvolution: 0 |
|||
seamlessCubemap: 0 |
|||
textureFormat: 1 |
|||
maxTextureSize: 2048 |
|||
textureSettings: |
|||
serializedVersion: 2 |
|||
filterMode: -1 |
|||
aniso: -1 |
|||
mipBias: -100 |
|||
wrapU: -1 |
|||
wrapV: -1 |
|||
wrapW: -1 |
|||
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 |
|||
maxTextureSizeSet: 0 |
|||
compressionQualitySet: 0 |
|||
textureFormatSet: 0 |
|||
platformSettings: |
|||
- serializedVersion: 2 |
|||
buildTarget: DefaultTexturePlatform |
|||
maxTextureSize: 2048 |
|||
resizeAlgorithm: 0 |
|||
textureFormat: -1 |
|||
textureCompression: 1 |
|||
compressionQuality: 50 |
|||
crunchedCompression: 0 |
|||
allowsAlphaSplitting: 0 |
|||
overridden: 0 |
|||
androidETC2FallbackOverride: 0 |
|||
spriteSheet: |
|||
serializedVersion: 2 |
|||
sprites: [] |
|||
outline: [] |
|||
physicsShape: [] |
|||
bones: [] |
|||
spriteID: |
|||
vertices: [] |
|||
indices: |
|||
edges: [] |
|||
weights: [] |
|||
spritePackingTag: |
|||
pSDRemoveMatte: 0 |
|||
pSDShowRemoveMatteOption: 0 |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 2e4ca31cf9cff4505acbefe44b621d6f |
|||
TextureImporter: |
|||
fileIDToRecycleName: {} |
|||
externalObjects: {} |
|||
serializedVersion: 9 |
|||
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 |
|||
isReadable: 0 |
|||
streamingMipmaps: 0 |
|||
streamingMipmapsPriority: 0 |
|||
grayScaleToAlpha: 0 |
|||
generateCubemap: 6 |
|||
cubemapConvolution: 0 |
|||
seamlessCubemap: 0 |
|||
textureFormat: 1 |
|||
maxTextureSize: 2048 |
|||
textureSettings: |
|||
serializedVersion: 2 |
|||
filterMode: -1 |
|||
aniso: -1 |
|||
mipBias: -100 |
|||
wrapU: -1 |
|||
wrapV: -1 |
|||
wrapW: -1 |
|||
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 |
|||
maxTextureSizeSet: 0 |
|||
compressionQualitySet: 0 |
|||
textureFormatSet: 0 |
|||
platformSettings: |
|||
- serializedVersion: 2 |
|||
buildTarget: DefaultTexturePlatform |
|||
maxTextureSize: 2048 |
|||
resizeAlgorithm: 0 |
|||
textureFormat: -1 |
|||
textureCompression: 1 |
|||
compressionQuality: 50 |
|||
crunchedCompression: 0 |
|||
allowsAlphaSplitting: 0 |
|||
overridden: 0 |
|||
androidETC2FallbackOverride: 0 |
|||
spriteSheet: |
|||
serializedVersion: 2 |
|||
sprites: [] |
|||
outline: [] |
|||
physicsShape: [] |
|||
bones: [] |
|||
spriteID: |
|||
vertices: [] |
|||
indices: |
|||
edges: [] |
|||
weights: [] |
|||
spritePackingTag: |
|||
pSDRemoveMatte: 0 |
|||
pSDShowRemoveMatteOption: 0 |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: fceb584222ca149cd984dad123c8ae25 |
|||
TextureImporter: |
|||
fileIDToRecycleName: {} |
|||
externalObjects: {} |
|||
serializedVersion: 9 |
|||
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 |
|||
isReadable: 0 |
|||
streamingMipmaps: 0 |
|||
streamingMipmapsPriority: 0 |
|||
grayScaleToAlpha: 0 |
|||
generateCubemap: 6 |
|||
cubemapConvolution: 0 |
|||
seamlessCubemap: 0 |
|||
textureFormat: 1 |
|||
maxTextureSize: 2048 |
|||
textureSettings: |
|||
serializedVersion: 2 |
|||
filterMode: -1 |
|||
aniso: -1 |
|||
mipBias: -100 |
|||
wrapU: -1 |
|||
wrapV: -1 |
|||
wrapW: -1 |
|||
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 |
|||
maxTextureSizeSet: 0 |
|||
compressionQualitySet: 0 |
|||
textureFormatSet: 0 |
|||
platformSettings: |
|||
- serializedVersion: 2 |
|||
buildTarget: DefaultTexturePlatform |
|||
maxTextureSize: 2048 |
|||
resizeAlgorithm: 0 |
|||
textureFormat: -1 |
|||
textureCompression: 1 |
|||
compressionQuality: 50 |
|||
crunchedCompression: 0 |
|||
allowsAlphaSplitting: 0 |
|||
overridden: 0 |
|||
androidETC2FallbackOverride: 0 |
|||
spriteSheet: |
|||
serializedVersion: 2 |
|||
sprites: [] |
|||
outline: [] |
|||
physicsShape: [] |
|||
bones: [] |
|||
spriteID: |
|||
vertices: [] |
|||
indices: |
|||
edges: [] |
|||
weights: [] |
|||
spritePackingTag: |
|||
pSDRemoveMatte: 0 |
|||
pSDShowRemoveMatteOption: 0 |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
撰写
预览
正在加载...
取消
保存
Reference in new issue