This commit is contained in:
2022-12-15 23:29:02 +01:00
parent 95220bec08
commit 4f57b57a00
24 changed files with 1695 additions and 81 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,332 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &1023969388561388979
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 2655555272253868329, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Name
value: Puzzle Plan Entry
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Pivot.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Pivot.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_RootOrder
value: -1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMax.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMin.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_SizeDelta.x
value: 320
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_SizeDelta.y
value: 54
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868335, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Text
value: Puzzle name
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects:
- targetCorrespondingSourceObject: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
insertIndex: -1
addedObject: {fileID: 8821617704014446116}
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 2655555272253868329, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
insertIndex: -1
addedObject: {fileID: 286020777690709323}
m_SourcePrefab: {fileID: 100100000, guid: fa44f6047bc35a141a84d1b4e0919ff9, type: 3}
--- !u!1 &3093889143253032090 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 2655555272253868329, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
m_PrefabInstance: {fileID: 1023969388561388979}
m_PrefabAsset: {fileID: 0}
--- !u!114 &286020777690709323
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3093889143253032090}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8ef80fc9016c4a46a190769f3b771bfa, type: 3}
m_Name:
m_EditorClassIdentifier:
puzzleName: {fileID: 3093889143253032092}
estimatedTime: {fileID: 8821617704014446117}
--- !u!114 &3093889143253032092 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2655555272253868335, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
m_PrefabInstance: {fileID: 1023969388561388979}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3093889143253032090}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &3093889143253032093 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
m_PrefabInstance: {fileID: 1023969388561388979}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &6824857666461398794
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 3093889143253032093}
m_Modifications:
- target: {fileID: 2655555272253868329, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Name
value: Estimated Time
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Pivot.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Pivot.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_RootOrder
value: -1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMax.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchorMin.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_SizeDelta.x
value: 320
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_SizeDelta.y
value: 24
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_AnchoredPosition.y
value: -30
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868335, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_Text
value: 'Time Estimate: '
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868335, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_FontData.m_MinSize
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2655555272253868335, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
propertyPath: m_FontData.m_FontSize
value: 16
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: fa44f6047bc35a141a84d1b4e0919ff9, type: 3}
--- !u!224 &8821617704014446116 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 2655555272253868334, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
m_PrefabInstance: {fileID: 6824857666461398794}
m_PrefabAsset: {fileID: 0}
--- !u!114 &8821617704014446117 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2655555272253868335, guid: fa44f6047bc35a141a84d1b4e0919ff9,
type: 3}
m_PrefabInstance: {fileID: 6824857666461398794}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cad05994b2fb37746988912bce5a31f5
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -79,4 +79,4 @@ MonoBehaviour:
m_HorizontalOverflow: 0 m_HorizontalOverflow: 0
m_VerticalOverflow: 0 m_VerticalOverflow: 0
m_LineSpacing: 1 m_LineSpacing: 1
m_Text: 'Time:' m_Text: Text

View File

@@ -22,7 +22,7 @@ namespace EscapeRoomEngine.Engine.Runtime
[InfoBox("If a space was generated without any puzzles in it, the engine will try generating another new space. To prevent infinite loops, the amount of retries is bound.")] [InfoBox("If a space was generated without any puzzles in it, the engine will try generating another new space. To prevent infinite loops, the amount of retries is bound.")]
public int maxSpaceGenerationTries = 1000; public int maxSpaceGenerationTries = 1000;
[Tooltip("The engine will try to generate a room that takes approximately this many seconds to complete.")] [Tooltip("The engine will try to generate a room that takes approximately this many seconds to complete.")]
public float initialTargetTime = 5 * 60; public float initialTargetTime = 10 * 60;
public Vector3 roomOffset = new(0, 1000, 0); public Vector3 roomOffset = new(0, 1000, 0);
[Required] public EngineTheme theme; [Required] public EngineTheme theme;
@@ -83,7 +83,11 @@ namespace EscapeRoomEngine.Engine.Runtime
var tries = 0; var tries = 0;
Space space; Space space;
Passage exit = null; Passage exit = null;
var puzzle = PlanPuzzles();
// choose the next puzzle
// PlanPuzzles();
var puzzle = ChoosePuzzle();
GameControl.Instance.CurrentPuzzle = puzzle;
do do
{ {
@@ -119,21 +123,41 @@ namespace EscapeRoomEngine.Engine.Runtime
room.AddSpace(space, exit); room.AddSpace(space, exit);
} }
/// <summary> private PuzzleModuleDescription ChoosePuzzle()
/// Updates the list of puzzles planned for this run and returns the puzzle to put in the next room.
/// </summary>
private PuzzleModuleDescription PlanPuzzles()
{ {
// choose a puzzle from the plan and remove it from both the plan and later available puzzles (so it is only chosen once)
var nextPuzzle = _plannedPuzzles.PopRandomElement(); var puzzle = _plannedPuzzles.PopRandomElement();
_availablePuzzles.Remove(puzzle);
EstimatedTimeRemaining = Measure.AverageTime(_plannedPuzzles); return puzzle;
return nextPuzzle;
} }
#endregion #endregion
/// <summary>
/// Updates the list of puzzles planned for this run.
/// </summary>
public void PlanPuzzles()
{
var n = _availablePuzzles.Count;
var estimates = new int[n];
var indices = new Range(0, n).ToArray();
var target = Mathf.RoundToInt(GameControl.Instance.TargetTime - GameControl.Instance.EstimatedTimeRoom);
_plannedPuzzles = new List<PuzzleModuleDescription>();
for (var i = 0; i < n; i++)
{
estimates[i] = Mathf.RoundToInt(Measure.EstimateTime(_availablePuzzles[i]));
}
var chosen = Backtrack.Closest(indices, estimates, target);
foreach (var i in chosen)
{
_plannedPuzzles.Add(_availablePuzzles[i]);
}
EstimatedTimeRemaining = Measure.EstimateTime(_plannedPuzzles);
GameControl.Instance.PlannedPuzzles = _plannedPuzzles;
}
public void HidePreviousRoom(bool destroy = true) public void HidePreviousRoom(bool destroy = true)
{ {
if (NumberOfRooms > 2) if (NumberOfRooms > 2)

View File

@@ -18,13 +18,20 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
private static Dictionary<int, PuzzleMeasurement> _runningMeasurements; private static Dictionary<int, PuzzleMeasurement> _runningMeasurements;
private static Session _currentSession; private static Session _currentSession;
public static float AverageTime(IEnumerable<PuzzleModuleDescription> puzzles) =>
puzzles.Sum(puzzle => PuzzleStorage.Instance.Load(puzzle).AverageTimeToSolve);
public static float AverageTime(PuzzleModuleDescription puzzle) => public static float AverageTime(PuzzleModuleDescription puzzle) =>
PuzzleStorage.Instance.Load(puzzle).AverageTimeToSolve; PuzzleStorage.Instance.Load(puzzle).AverageTimeToSolve;
public static float AverageTime(IEnumerable<PuzzleModuleDescription> puzzles) => puzzles.Sum(AverageTime);
public static float AverageTime(Room room) => public static float AverageTime(Room room) =>
room.puzzles.Sum(puzzle => AverageTime((PuzzleModuleDescription)puzzle.description)); room.puzzles.Sum(puzzle => AverageTime((PuzzleModuleDescription)puzzle.description));
public static float EstimateTime(PuzzleModuleDescription puzzle) =>
PuzzleStorage.Instance.Load(puzzle).EstimateTimeToSolve(SessionPercentile());
public static float EstimateTime(IEnumerable<PuzzleModuleDescription> puzzles) => puzzles.Sum(EstimateTime);
public static float EstimateTime(Room room) =>
room.puzzles.Sum(puzzle => EstimateTime((PuzzleModuleDescription)puzzle.description));
public static float SessionPercentile() => _currentSession?.MeanPercentile ?? 0.5f;
public static void StartMeasuring(PuzzleModuleDescription puzzle) public static void StartMeasuring(PuzzleModuleDescription puzzle)
{ {
_runningMeasurements[puzzle.Id] = new PuzzleMeasurement _runningMeasurements[puzzle.Id] = new PuzzleMeasurement

View File

@@ -17,6 +17,7 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
public float TotalTimeSpentOnPuzzle => Measurements.Sum(measurement => measurement.Time); public float TotalTimeSpentOnPuzzle => Measurements.Sum(measurement => measurement.Time);
public float AverageTimeToSolve => Measurements.Count > 0 ? TotalTimeSpentOnPuzzle / Measurements.Count : 0f; public float AverageTimeToSolve => Measurements.Count > 0 ? TotalTimeSpentOnPuzzle / Measurements.Count : 0f;
public NormalDistribution Distribution => new(TimesAsSamples());
[UsedImplicitly] [UsedImplicitly]
public Puzzle() {} public Puzzle() {}
@@ -25,6 +26,23 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
ID = puzzle.Id; ID = puzzle.Id;
} }
/// <summary>
/// Estimate how long a player in the given percentile will take to solve this puzzle.
/// </summary>
public float EstimateTimeToSolve(float percentile) => Distribution.InverseCumulative(percentile);
private float[] TimesAsSamples()
{
var samples = new float[Measurements.Count];
for (var i = 0; i < Measurements.Count; i++)
{
samples[i] = Measurements[i].Time;
}
return samples;
}
public override string ToString() public override string ToString()
{ {
return $"{Engine.Theme.GetPuzzle(ID)}: avg. {AverageTimeToSolve.ToTimeSpan():m':'ss} ({string.Join(", ", Measurements)})"; return $"{Engine.Theme.GetPuzzle(ID)}: avg. {AverageTimeToSolve.ToTimeSpan():m':'ss} ({string.Join(", ", Measurements)})";

View File

@@ -8,6 +8,8 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
{ {
public class PuzzleStorage : MonoBehaviour public class PuzzleStorage : MonoBehaviour
{ {
private const int SchemaVersion = 1;
public static PuzzleStorage Instance { get; private set; } public static PuzzleStorage Instance { get; private set; }
[SerializeField] [SerializeField]
@@ -17,7 +19,20 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
private void OnEnable() private void OnEnable()
{ {
_realm = Realm.GetInstance(databasePath); var config = new RealmConfiguration
{
SchemaVersion = SchemaVersion,
MigrationCallback = (migration, oldSchemaVersion) =>
{
if (oldSchemaVersion < 1)
{
// migration from version 0 to 1
}
Logger.Log($"Migrated database to version {SchemaVersion}", LogType.Measuring);
}
};
_realm = Realm.GetInstance(config.ConfigWithPath(databasePath));
} }
private void Awake() private void Awake()
@@ -41,10 +56,7 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
{ {
session.Time = time; session.Time = time;
_realm.Write(() => _realm.Write(() => _realm.Add(session));
{
_realm.Add(session);
});
} }
#endregion #endregion
@@ -84,6 +96,9 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
// add solved puzzle to session // add solved puzzle to session
session.PuzzlesSolved.Add(found); session.PuzzlesSolved.Add(found);
// add time percentile to session
session.Percentiles.Add(found.Distribution.Cumulative(measurement.Time));
}); });
} }

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using EscapeRoomEngine.Engine.Runtime.Utilities; using EscapeRoomEngine.Engine.Runtime.Utilities;
using JetBrains.Annotations; using JetBrains.Annotations;
using MongoDB.Bson; using MongoDB.Bson;
@@ -13,8 +14,11 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
[PrimaryKey] [PrimaryKey]
public ObjectId ID { get; set; } public ObjectId ID { get; set; }
public float Time { get; set; } public float Time { get; set; }
public IList<float> Percentiles { get; }
public IList<Puzzle> PuzzlesSolved { get; } public IList<Puzzle> PuzzlesSolved { get; }
public float MeanPercentile => Percentiles.Count == 0 ? .5f : Probability.Mean(Percentiles.ToArray());
[UsedImplicitly] [UsedImplicitly]
public Session() public Session()
{ {

View File

@@ -1,4 +1,6 @@
using EscapeRoomEngine.Engine.Runtime.Measurements; using System.Collections.Generic;
using EscapeRoomEngine.Engine.Runtime.Measurements;
using EscapeRoomEngine.Engine.Runtime.Modules;
using EscapeRoomEngine.Engine.Runtime.Utilities; using EscapeRoomEngine.Engine.Runtime.Utilities;
using NaughtyAttributes; using NaughtyAttributes;
using UnityEngine; using UnityEngine;
@@ -15,20 +17,31 @@ namespace EscapeRoomEngine.Engine.Runtime.UI
{ {
public static GameControl Instance { get; private set; } public static GameControl Instance { get; private set; }
[SerializeField]
private float uiUpdateInterval = 1, planUpdateInterval = 1;
[BoxGroup("Internal")] [SerializeField] [BoxGroup("Internal")] [SerializeField]
private Button startButton, stopButton, pauseButton, addTimeButton, removeTimeButton; private Button startButton, stopButton, pauseButton, addTimeButton, removeTimeButton;
[BoxGroup("Internal")] [SerializeField] [BoxGroup("Internal")] [SerializeField]
private Text timeText, roomTimeText, estimateTimeText, targetTimeText; private Text timeText, roomTimeText, estimateTimeText, targetTimeText, percentileText;
[BoxGroup("Internal")] [SerializeField] [BoxGroup("Internal")] [SerializeField]
private float uiUpdateInterval = 1; private PuzzlePlan puzzlePlan;
[HideInInspector] public GameState gameState = GameState.Stopped; [HideInInspector] public GameState gameState = GameState.Stopped;
public PuzzleModuleDescription CurrentPuzzle
{
set => puzzlePlan.CurrentPuzzle = value;
}
public List<PuzzleModuleDescription> PlannedPuzzles
{
set => puzzlePlan.Puzzles = value;
}
public float TimeElapsed { get; private set; } public float TimeElapsed { get; private set; }
public float TimeInRoom { get; set; } public float TimeInRoom { get; set; }
public float TargetTime { get; private set; } public float TargetTime { get; private set; }
public float EstimatedTimeRoom { get; private set; }
private float _previousUIUpdate; private float _previousUIUpdate, _previousPlanUpdate;
private void Awake() private void Awake()
{ {
@@ -57,6 +70,15 @@ namespace EscapeRoomEngine.Engine.Runtime.UI
_previousUIUpdate = Time.time; _previousUIUpdate = Time.time;
SetTimeText(); SetTimeText();
UpdateStats();
}
// update plan
if (Time.time > _previousPlanUpdate + planUpdateInterval)
{
_previousPlanUpdate = Time.time;
Engine.Instance.PlanPuzzles();
} }
// enable or disable buttons // enable or disable buttons
@@ -129,15 +151,21 @@ namespace EscapeRoomEngine.Engine.Runtime.UI
Engine.Instance.CurrentRoom.Match(some: room => Engine.Instance.CurrentRoom.Match(some: room =>
{ {
estimateTimeText.text = TimeToText( EstimatedTimeRoom =
TimeElapsed - TimeInRoom TimeElapsed - TimeInRoom + Mathf.Max(TimeInRoom, Measure.EstimateTime(room));
+ Mathf.Max(TimeInRoom, Measure.AverageTime(room)) estimateTimeText.text = TimeToText(EstimatedTimeRoom + Engine.Instance.EstimatedTimeRemaining);
+ Engine.Instance.EstimatedTimeRemaining);
}); });
} }
private void UpdateStats()
{
percentileText.text = PercentageToText(Measure.SessionPercentile());
}
private static string TimeToText(float time) => $"{time.ToTimeSpan():mm':'ss}"; private static string TimeToText(float time) => $"{time.ToTimeSpan():mm':'ss}";
private static string PercentageToText(float percentage) => $"{percentage:P1}";
#endregion #endregion
#region Measurements #region Measurements

View File

@@ -0,0 +1,56 @@
using System.Collections.Generic;
using EscapeRoomEngine.Engine.Runtime.Modules;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.UI;
namespace EscapeRoomEngine.Engine.Runtime.UI
{
public class PuzzlePlan : MonoBehaviour
{
[SerializeField]
private Vector2 entryOffset;
[BoxGroup("Internal")] [SerializeField]
private RectTransform plan;
[BoxGroup("Internal")] [SerializeField]
private PuzzlePlanEntry currentPuzzle, entryPrefab;
[BoxGroup("Internal")] [SerializeField]
private GameObject currentPuzzleTitle, planTitle;
public PuzzleModuleDescription CurrentPuzzle
{
set
{
currentPuzzleTitle.SetActive(true);
currentPuzzle.gameObject.SetActive(true);
// set the current puzzle
currentPuzzle.Puzzle = value;
}
}
public List<PuzzleModuleDescription> Puzzles
{
set
{
planTitle.SetActive(true);
plan.gameObject.SetActive(true);
// remove the old children
foreach (RectTransform child in plan)
{
Destroy(child.gameObject);
}
// add the new children
var offset = Vector2.zero;
value.ForEach(puzzle =>
{
var entry = Instantiate(entryPrefab, plan, false);
entry.Position = offset;
entry.Puzzle = puzzle;
offset += entryOffset;
});
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ad95894771b8478591e5f5abbe1d0244
timeCreated: 1671133031

View File

@@ -0,0 +1,32 @@
using System;
using EscapeRoomEngine.Engine.Runtime.Measurements;
using EscapeRoomEngine.Engine.Runtime.Modules;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.UI;
namespace EscapeRoomEngine.Engine.Runtime.UI
{
public class PuzzlePlanEntry : MonoBehaviour
{
[BoxGroup("Internal")] [Required] [SerializeField]
private Text puzzleName, estimatedTime;
public Vector2 Position
{
set
{
puzzleName.rectTransform.anchoredPosition = value;
}
}
public PuzzleModuleDescription Puzzle
{
set
{
puzzleName.text = value.puzzleName;
estimatedTime.text = $"Time Estimate: {Measure.EstimateTime(value).ToTimeSpan():mm':'ss}";
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8ef80fc9016c4a46a190769f3b771bfa
timeCreated: 1671136597

View File

@@ -0,0 +1,127 @@
using System;
// ReSharper disable ReturnTypeCanBeEnumerable.Global
// ReSharper disable ParameterTypeCanBeEnumerable.Local
namespace EscapeRoomEngine.Engine.Runtime.Utilities
{
public struct Backtrack
{
private int[] indices;
private int[] values;
private int target;
public Backtrack(int[] indices, int[] values, int target)
{
this.indices = indices;
this.values = values;
this.target = target;
}
/// <summary>
/// Find any number of elements from the given set of indices and values with the maximum sum that doesn't exceed the target sum.
/// </summary>
/// <remarks>This function uses a backtracking approach to find the sum.</remarks>
public int[] BruteForceLower(int[] chosen = null, int pos = 0)
{
chosen ??= Array.Empty<int>();
if (Sum(chosen) > target)
{
// if the sum of the chosen values exceeds the target, return nothing
return Array.Empty<int>();
}
if (pos >= indices.Length)
{
// if we cannot add any more elements, return all chosen
return chosen;
}
// find the best indices when skipping the one at the current position
var leave = BruteForceLower(chosen, pos + 1);
// find the best indices when including the one at the current position
var next = new int[chosen.Length + 1];
for (var i = 0; i < chosen.Length; i++)
{
next[i] = chosen[i];
}
next[^1] = indices[pos];
var pick = BruteForceLower(next, pos + 1);
// return the best result
return Sum(leave) > Sum(pick) ? leave : pick;
}
/// <summary>
/// Find any number of elements from the given set of indices and values with the minimum sum that is higher than the target sum.
/// </summary>
/// <remarks>This function uses a backtracking approach to find the sum.</remarks>
public int[] BruteForceHigher(int[] chosen = null, int pos = int.MaxValue)
{
chosen ??= indices;
if (pos == int.MaxValue)
{
pos = indices.Length - 1;
}
if (Sum(chosen) < target)
{
// if the sum of the chosen values is lower than the target, return all indices (the maximum choice)
return indices;
}
if (pos < 0)
{
// if we cannot remove any more elements, return all chosen
return chosen;
}
// find the best indices when not removing the one at the current position
var leave = BruteForceHigher(chosen, pos - 1);
// find the best indices when removing the one at the current position
var next = new int[chosen.Length - 1];
for (var i = 0; i < chosen.Length; i++)
{
if (i < pos)
{
next[i] = chosen[i];
} else if (i > pos)
{
next[i - 1] = chosen[i];
}
}
var pick = BruteForceHigher(next, pos - 1);
// return the best result
return Sum(leave) < Sum(pick) ? leave : pick;
}
public int[] BruteForce()
{
var lower = BruteForceLower();
var higher = BruteForceHigher();
var errLower = Math.Abs(target - Sum(lower));
var errHigher = Math.Abs(target - Sum(higher));
return errLower < errHigher ? lower : higher;
}
/// <summary>
/// Get a subset of indices where the sum of their corresponding values is closest to a target sum.
/// </summary>
public static int[] Closest(int[] indices, int[] values, int target) =>
new Backtrack(indices, values, target).BruteForce();
private int Sum(int[] chosen)
{
var sum = 0;
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var i in chosen)
{
sum += values[i];
}
return sum;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 313a79a04104480f931206ff7f57d0c5
timeCreated: 1671126182

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using MathNet.Numerics.Distributions; using MathNet.Numerics.Distributions;
using UnityEngine;
using Random = System.Random; using Random = System.Random;
namespace EscapeRoomEngine.Engine.Runtime.Utilities namespace EscapeRoomEngine.Engine.Runtime.Utilities
@@ -8,19 +9,21 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
[Serializable] [Serializable]
public struct NormalDistribution public struct NormalDistribution
{ {
public double mean, σ; public float μ, σ;
public static NormalDistribution Standard => new NormalDistribution { mean = 0, σ = 1 }; public static NormalDistribution Standard => new NormalDistribution { μ = 0, σ = 1 };
public NormalDistribution(double[] samples) : this() public NormalDistribution(float[] samples) : this()
{ {
mean = Probability.Mean(samples); μ = Probability.Mean(samples);
σ = Probability.StandardDeviation(samples, mean); σ = Probability.StandardDeviation(samples, μ);
} }
public double Sample() => σ * Probability.Normal() + mean; public float Sample() => σ * Probability.Normal() + μ;
public double Cumulative(double x) => new Normal(mean, σ).CumulativeDistribution(x); public float Cumulative(float x) => (float)new Normal(μ, σ).CumulativeDistribution(x);
public float InverseCumulative(float x) => (float)new Normal(μ, σ).InverseCumulativeDistribution(x);
} }
public static class Probability public static class Probability
@@ -32,22 +35,22 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
/// For simplicity, the result is clamped between -3 and 3. This is accurate for 99.7% of all samples, by the three-σ rule. /// For simplicity, the result is clamped between -3 and 3. This is accurate for 99.7% of all samples, by the three-σ rule.
/// </summary> /// </summary>
/// <remarks>The calculation of the random variable is done by a Box-Muller transform.</remarks> /// <remarks>The calculation of the random variable is done by a Box-Muller transform.</remarks>
public static double Normal() public static float Normal()
{ {
double u1, u2, square; float u1, u2, square;
// get two random points inside the unit circle // get two random points inside the unit circle
do do
{ {
u1 = 2 * _random.NextDouble() - 1; u1 = 2 * (float)_random.NextDouble() - 1;
u2 = 2 * _random.NextDouble() - 1; u2 = 2 * (float)_random.NextDouble() - 1;
square = u1 * u1 + u2 * u2; square = u1 * u1 + u2 * u2;
} while (square >= 1f); } while (square >= 1f);
return u1 * Math.Sqrt(-2 * Math.Log(square) / square); return u1 * Mathf.Sqrt(-2 * Mathf.Log(square) / square);
} }
public static double Mean(double[] samples) public static float Mean(float[] samples)
{ {
if (samples.Length == 0) if (samples.Length == 0)
{ {
@@ -57,10 +60,10 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
return samples.Sum() / samples.Length; return samples.Sum() / samples.Length;
} }
public static double StandardDeviation(double[] samples) => StandardDeviation(samples, Mean(samples)); public static float StandardDeviation(float[] samples) => StandardDeviation(samples, Mean(samples));
public static double StandardDeviation(double[] samples, double mean) public static float StandardDeviation(float[] samples, float mean)
{ {
var deviations = new double[samples.Length]; var deviations = new float[samples.Length];
for (var i = 0; i < samples.Length; i++) for (var i = 0; i < samples.Length; i++)
{ {
@@ -68,7 +71,7 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
deviations[i] = d * d; deviations[i] = d * d;
} }
return Math.Sqrt(Mean(deviations)); return Mathf.Sqrt(Mean(deviations));
} }
} }
} }

View File

@@ -12,6 +12,19 @@
this.max = max; this.max = max;
} }
public int[] ToArray(bool includeMax = false)
{
var count = includeMax ? Length + 1 : Length;
var array = new int[count];
for (var i = 0; i < count; i++)
{
array[i] = min + i;
}
return array;
}
public override string ToString() => $"{{{min}, ..., {max}}}"; public override string ToString() => $"{{{min}, ..., {max}}}";
} }
} }

View File

@@ -25,8 +25,14 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
public static int RandomInclusive(int from, int to) => Random.Range(from, to + 1); public static int RandomInclusive(int from, int to) => Random.Range(from, to + 1);
public static T RandomElement<T>(this List<T> list) => list[Random.Range(0, list.Count)]; #endregion
}
public static class ListExtensions
{
/// <summary>
/// remove a random element from a list and return it.
/// </summary>
public static T PopRandomElement<T>(this List<T> list) public static T PopRandomElement<T>(this List<T> list)
{ {
var index = Random.Range(0, list.Count); var index = Random.Range(0, list.Count);
@@ -35,7 +41,20 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
return element; return element;
} }
#endregion public static T RandomElement<T>(this List<T> list) => list[Random.Range(0, list.Count)];
/// <summary>
/// Perform a Fisher-Yates shuffle on a given list, leaving it in a random order.
/// </summary>
public static void Shuffle<T>(this List<T> list)
{
for (var n = list.Count - 1; n > 0; n--)
{
var i = Utilities.RandomInclusive(0, n);
var j = Utilities.RandomInclusive(0, n);
(list[i], list[j]) = (list[j], list[i]);
}
}
} }
public static class Vector2IntExtensions public static class Vector2IntExtensions

View File

@@ -25,7 +25,7 @@ PluginImporter:
- first: - first:
Windows Store Apps: WindowsStoreApps Windows Store Apps: WindowsStoreApps
second: second:
enabled: 1 enabled: 0
settings: {} settings: {}
userData: userData:
assetBundleName: assetBundleName:

View File

@@ -547,6 +547,7 @@ GameObject:
- component: {fileID: 1568048334} - component: {fileID: 1568048334}
- component: {fileID: 1568048339} - component: {fileID: 1568048339}
- component: {fileID: 1568048336} - component: {fileID: 1568048336}
- component: {fileID: 1568048340}
m_Layer: 0 m_Layer: 0
m_Name: Engine m_Name: Engine
m_TagString: Untagged m_TagString: Untagged
@@ -567,7 +568,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
maxSpaceGenerationTries: 1000 maxSpaceGenerationTries: 1000
initialTargetTime: 300 initialTargetTime: 600
roomOffset: {x: 0, y: 1000, z: 0} roomOffset: {x: 0, y: 1000, z: 0}
theme: {fileID: 11400000, guid: 568d9a7d70f3edb4cb6db66a0010f105, type: 2} theme: {fileID: 11400000, guid: 568d9a7d70f3edb4cb6db66a0010f105, type: 2}
--- !u!4 &1568048335 --- !u!4 &1568048335
@@ -598,7 +599,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
loggingEnabled: 1 loggingEnabled: 1
typeFilter: 00000000 typeFilter: 0000000009000000
--- !u!114 &1568048339 --- !u!114 &1568048339
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -612,3 +613,17 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
databasePath: measurements.realm databasePath: measurements.realm
--- !u!114 &1568048340
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568048333}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 599a288d4210437698dc391e5cc84a8a, type: 3}
m_Name:
m_EditorClassIdentifier:
values: a3000000110100007c0000002801000029000000
target: 600

View File

@@ -0,0 +1,47 @@
using EscapeRoomEngine.Engine.Runtime.Utilities;
using JetBrains.Annotations;
using NaughtyAttributes;
using UnityEngine;
namespace Test_Assets
{
public class BacktrackingTest : MonoBehaviour
{
public int[] values;
public int target;
[Button]
[UsedImplicitly]
public void Closest()
{
PrintResult(Backtrack.Closest(new Range(0, values.Length).ToArray(), values, target));
}
[Button]
[UsedImplicitly]
public void ClosestLower()
{
var backtrack = new Backtrack(new Range(0, values.Length).ToArray(), values, target);
PrintResult(backtrack.BruteForceLower());
}
[Button]
[UsedImplicitly]
public void ClosestHigher()
{
var backtrack = new Backtrack(new Range(0, values.Length).ToArray(), values, target);
PrintResult(backtrack.BruteForceHigher());
}
private void PrintResult(int[] indices)
{
var sum = 0;
foreach (var i in indices)
{
Debug.Log(values[i]);
sum += values[i];
}
Debug.Log($"sum: {sum}");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 599a288d4210437698dc391e5cc84a8a
timeCreated: 1671127266

View File

@@ -1,6 +1,5 @@
using EscapeRoomEngine.Engine.Runtime.Utilities; using EscapeRoomEngine.Engine.Runtime.Utilities;
using JetBrains.Annotations; using JetBrains.Annotations;
using MathNet.Numerics.Distributions;
using NaughtyAttributes; using NaughtyAttributes;
using UnityEngine; using UnityEngine;
@@ -11,7 +10,8 @@ namespace Test_Assets
public NormalDistribution distribution = NormalDistribution.Standard; public NormalDistribution distribution = NormalDistribution.Standard;
public int steps = 24; public int steps = 24;
public int n = 1000000; public int n = 1000000;
public double sample; public float sample;
public float percentile = 0.5f;
[Button] [Button]
[UsedImplicitly] [UsedImplicitly]
@@ -62,9 +62,16 @@ namespace Test_Assets
Debug.Log(distribution.Cumulative(sample)); Debug.Log(distribution.Cumulative(sample));
} }
private double[] Samples() [Button]
[UsedImplicitly]
public void InversePercentile()
{ {
var samples = new double[n]; Debug.Log(distribution.InverseCumulative(percentile));
}
private float[] Samples()
{
var samples = new float[n];
for (var i = 0; i < n; i++) for (var i = 0; i < n; i++)
{ {
samples[i] = distribution.Sample(); samples[i] = distribution.Sample();