Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TimeBombAgent helpful pass the out-game tutorial using MonkeyAgent #37

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion Editor/Localization/ja.po
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2023 DeNA Co., Ltd.
# Copyright (c) 2023-2024 DeNA Co., Ltd.
# This software is released under the MIT License.
#
# Package UI Localization is available in 2020.2
Expand Down Expand Up @@ -239,6 +239,25 @@ msgid "Agent to repeat execution"
msgstr "リピート実行するAgentを指定します"


#: Editor/UI/Agents/TimeBombAgentEditor.cs

# agent (same as OneTimeAgentEditor.cs)
msgid "Agent"
msgstr "Agent"

# agent tooltip
msgid "Working Agent. If this Agent exits first, the TimeBombAgent will fail."
msgstr "実際に動作するAgent。このAgentが先に終了すると、TimeBombAgentは失敗します"

# defuse message
msgid "Defuse Message"
msgstr "解除メッセージ"

# defuse message tooltip
msgid "Defuse the time bomb when this message comes to the log first. Can specify regex."
msgstr "このメッセージが先にログに出力されたら、TimeBombAgentは正常終了します。正規表現を指定できます。"


#: Editor/UI/Agents/ParallelCompositeAgentEditor.cs

# agents
Expand Down
43 changes: 43 additions & 0 deletions Editor/UI/Agents/TimeBombAgentEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2023-2024 DeNA Co., Ltd.
// This software is released under the MIT License.

using DeNA.Anjin.Agents;
using UnityEditor;
using UnityEngine;

namespace DeNA.Anjin.Editor.UI.Agents
{
/// <summary>
/// Editor GUI for TimeBombAgent
/// </summary>
[CustomEditor(typeof(TimeBombAgent))]
public class TimeBombAgentEditor : UnityEditor.Editor
{
private static readonly string s_description = L10n.Tr("Description");
private static readonly string s_descriptionTooltip = L10n.Tr("Description about this agent instance");
private static readonly string s_agent = L10n.Tr("Agent");

private static readonly string s_agentTooltip =
L10n.Tr("Working Agent. If this Agent exits first, the TimeBombAgent will fail.");

private static readonly string s_defuseMessage = L10n.Tr("Defuse Message");

private static readonly string s_defuseMessageTooltip =
L10n.Tr("Defuse the time bomb when this message comes to the log first. Can specify regex.");

/// <inheritdoc/>
public override void OnInspectorGUI()
{
serializedObject.Update();

EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(TimeBombAgent.description)),
new GUIContent(s_description, s_descriptionTooltip));
EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(TimeBombAgent.agent)),
new GUIContent(s_agent, s_agentTooltip));
EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(TimeBombAgent.defuseMessage)),
new GUIContent(s_defuseMessage, s_defuseMessageTooltip));

serializedObject.ApplyModifiedProperties();
}
}
}
3 changes: 3 additions & 0 deletions Editor/UI/Agents/TimeBombAgentEditor.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ An Agent that executes multiple Agents in parallel.

The instance of this Agent (.asset file) can have the following settings.

<dl><dt>Agents
<dl>
<dt>Agents</dt><dd>A list of Agents to be executed in parallel, which can be nested by specifying a CompositeAgent</dd>
</dl>

Expand All @@ -294,7 +294,7 @@ For example, in a game where the title scene leads to a different path only for

The Agent instance (.asset file) can contain the following settings.

<dl> <dt>Agent
<dl>
<dt>Agent</dt><dd>Agent that can be executed only once, which can be nested by specifying a CompositeAgent</dd>
</dl>

Expand All @@ -313,6 +313,23 @@ This Agent instance (.asset file) can contain the following.
</dl>


### TimeBombAgent

An Agent that will fail if a defuse message is not received before the inner agent exits.

For example, pass the out-game tutorial (Things that can be completed with tap operations on uGUI, such as smartphone games).

1. Set `UGUIMonkeyAgent` as the `Agent`. It should not be able to operate except for the advancing button. Set the `Lifespan Sec` with a little margin.
2. Set the log message to be output when the tutorial is completed as the `Defuse Message`.

This Agent instance (.asset file) can contain the following.

<dl>
<dt>Agent</dt><dd>Working Agent. If this Agent exits first, the TimeBombAgent will fail.</dd>
<dt>Defuse Message</dt><dd>Defuse the time bomb when this message comes to the log first. Can specify regex.</dd>
</dl>


### EmergencyExitAgent

An Agent that monitors the appearance of the `EmergencyExit` component in the `DeNA.Anjin.Annotations` assembly and clicks on it as soon as it appears.
Expand Down
17 changes: 17 additions & 0 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,23 @@ SerialCompositeAgentと組み合わせることで、シナリオを何周もし
</dl>


### TimeBombAgent

内包するAgentが終了する前に解除メッセージを受信しないと失敗するAgent。

例えば、アウトゲームのチュートリアル(スマホゲームなどuGUIのタップ操作で完遂できるもの)を突破するには、次のように設定します。

1. `UGUIMonkeyAgent` を `Agent` に設定します。進行するボタン以外は操作できないはずです。`実行時間` は少し余裕をもって設定してください
2. チュートリアル完遂時にログに出力されるメッセージを `解除メッセージ` に設定します

このAgentのインスタンス(.assetファイル)には以下を設定できます。

<dl>
<dt>Agent</dt><dd>実際に動作するAgent。このAgentが先に終了すると、TimeBombAgentは失敗します。</dd>
<dt>解除メッセージ</dt><dd>このメッセージが先にログに出力されたら、TimeBombAgentは正常終了します。正規表現でも指定できます。</dd>
</dl>


### EmergencyExitAgent

`DeNA.Anjin.Annotations` アセンブリに含まれる `EmergencyExit` コンポーネントの出現を監視し、表示されたら即クリックするAgentです。
Expand Down
96 changes: 96 additions & 0 deletions Runtime/Agents/TimeBombAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) 2023-2024 DeNA Co., Ltd.
// This software is released under the MIT License.

using System;
using System.Text.RegularExpressions;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace DeNA.Anjin.Agents
{
/// <summary>
/// An Agent that will fail if a defuse message is not received before the inner agent exits.
///
/// For example, pass the out-game tutorial (Things that can be completed with tap operations on uGUI, such as smartphone games).
/// 1. Set `UGUIMonkeyAgent` as the `Agent`. It should not be able to operate except for the advancing button. Set the `Lifespan Sec` with a little margin.
/// 2. Set the log message to be output when the tutorial is completed as the `Defuse Message`.
/// </summary>
[CreateAssetMenu(fileName = "New TimeBombAgent", menuName = "Anjin/Time Bomb Agent", order = 17)]
public class TimeBombAgent : AbstractAgent
{
/// <summary>
/// Working Agent. If this Agent exits first, the TimeBombAgent will fail.
/// </summary>
public AbstractAgent agent;

/// <summary>
/// Defuse the time bomb when this message comes to the log first.
/// Can specify regex.
/// </summary>
public string defuseMessage;

private Regex DefuseMessageRegex => new Regex(defuseMessage);
private CancellationTokenSource _cts;

private void OnEnable()
{
Application.logMessageReceived += HandleDefuseMessage;
}

private void OnDisable()
{
Application.logMessageReceived -= HandleDefuseMessage;
}

private void HandleDefuseMessage(string logString, string stackTrace, LogType type)
{
if (DefuseMessageRegex.IsMatch(logString))
{
try
{
_cts?.Cancel();
}
catch (ObjectDisposedException e)
{
// ignored
}
}
}

/// <inheritdoc />
public override async UniTask Run(CancellationToken token)
{
Logger.Log($"Enter {this.name}.Run()");

using (var agentCts = new CancellationTokenSource()) // To cancel only the Working Agent.
{
_cts = agentCts;

using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, agentCts.Token))
{
try
{
agent.Logger = Logger;
agent.Random = Random; // This Agent does not consume pseudo-random numbers, so passed on as is.
await agent.Run(linkedCts.Token);

throw new TimeoutException(
$"Could not receive defuse message `{defuseMessage}` before the agent terminated.");
}
catch (OperationCanceledException e)
{
if (token.IsCancellationRequested) // The parent was cancelled.
{
throw;
}

Logger.Log($"Working agent {agent.name} was cancelled.");
}
}
}

Logger.Log($"Exit {this.name}.Run()");
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Agents/TimeBombAgent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions Tests/Runtime/Agents/TimeBombAgentTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) 2023-2024 DeNA Co., Ltd.
// This software is released under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using DeNA.Anjin.Utilities;
using NUnit.Framework;
using TestHelper.Attributes;
using UnityEngine;
using UnityEngine.TestTools;

namespace DeNA.Anjin.Agents
{
[TestFixture]
[UnityPlatform(RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor, RuntimePlatform.LinuxEditor)]
public class TimeBombAgentTest
{
private static UGUIMonkeyAgent CreateMonkeyAgent(long lifespanSec)
{
var agent = ScriptableObject.CreateInstance<UGUIMonkeyAgent>();
agent.name = nameof(UGUIMonkeyAgent);
agent.lifespanSec = lifespanSec;
agent.delayMillis = 100;
agent.secondsToErrorForNoInteractiveComponent = 0; // Disable check
agent.touchAndHoldDelayMillis = 200;
return agent;
}

private static TimeBombAgent CreateTimeBombAgent(AbstractAgent workingAgent, string defuseMessage)
{
var agent = ScriptableObject.CreateInstance<TimeBombAgent>();
agent.name = nameof(TimeBombAgent);
agent.agent = workingAgent;
agent.defuseMessage = defuseMessage;
return agent;
}

[Test]
public async Task Run_CancelTask_StopAgent()
{
var monkeyAgent = CreateMonkeyAgent(5);
var agent = CreateTimeBombAgent(monkeyAgent, "^Never match!$");
agent.Logger = new ConsoleLogger(Debug.unityLogger.logHandler);
agent.Random = new RandomFactory(0).CreateRandom();

using (var cts = new CancellationTokenSource())
{
var task = agent.Run(cts.Token);
await UniTask.NextFrame();

cts.Cancel();
await UniTask.NextFrame();

Assert.That(task.Status, Is.EqualTo(UniTaskStatus.Canceled));
}
}

[Test]
[LoadScene("Packages/com.dena.anjin/Tests/TestScenes/OutGameTutorial.unity")]
public async Task Run_Defuse_StopAgent()
{
var monkeyAgent = CreateMonkeyAgent(5);
var agent = CreateTimeBombAgent(monkeyAgent, "^Tutorial Completed!$");
agent.Logger = new ConsoleLogger(Debug.unityLogger.logHandler);
agent.Random = new RandomFactory(0).CreateRandom();

LogAssert.Expect(LogType.Log, "Working agent UGUIMonkeyAgent was cancelled.");
LogAssert.Expect(LogType.Log, "Exit TimeBombAgent.Run()");

using (var cts = new CancellationTokenSource())
{
await agent.Run(cts.Token);
}
}

[Test]
[LoadScene("Packages/com.dena.anjin/Tests/TestScenes/OutGameTutorial.unity")]
public async Task Run_NotDefuse_ThrowsTimeoutException()
{
var monkeyAgent = CreateMonkeyAgent(1);
var agent = CreateTimeBombAgent(monkeyAgent, "^Never match!$");
agent.Logger = new ConsoleLogger(Debug.unityLogger.logHandler);
agent.Random = new RandomFactory(0).CreateRandom();

using (var cts = new CancellationTokenSource())
{
try
{
await agent.Run(cts.Token);
Assert.Fail("Should throw TimeoutException");
}
catch (TimeoutException e)
{
Assert.That(e.Message, Is.EqualTo(
"Could not receive defuse message `^Never match!$` before the agent terminated."));
}
}
}
}
}
3 changes: 3 additions & 0 deletions Tests/Runtime/Agents/TimeBombAgentTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Tests/Runtime/TestComponents.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading