Skip to content

Commit

Permalink
Add TimeBombAgent
Browse files Browse the repository at this point in the history
  • Loading branch information
nowsprinting committed Mar 17, 2024
1 parent af3e0e0 commit d3c7c0a
Show file tree
Hide file tree
Showing 14 changed files with 1,910 additions and 3 deletions.
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

0 comments on commit d3c7c0a

Please sign in to comment.