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_VerticalOverflow: 0
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.")]
public int maxSpaceGenerationTries = 1000;
[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);
[Required] public EngineTheme theme;
@@ -83,7 +83,11 @@ namespace EscapeRoomEngine.Engine.Runtime
var tries = 0;
Space space;
Passage exit = null;
var puzzle = PlanPuzzles();
// choose the next puzzle
// PlanPuzzles();
var puzzle = ChoosePuzzle();
GameControl.Instance.CurrentPuzzle = puzzle;
do
{
@@ -119,21 +123,41 @@ namespace EscapeRoomEngine.Engine.Runtime
room.AddSpace(space, exit);
}
/// <summary>
/// Updates the list of puzzles planned for this run and returns the puzzle to put in the next room.
/// </summary>
private PuzzleModuleDescription PlanPuzzles()
private PuzzleModuleDescription ChoosePuzzle()
{
var nextPuzzle = _plannedPuzzles.PopRandomElement();
EstimatedTimeRemaining = Measure.AverageTime(_plannedPuzzles);
return nextPuzzle;
// choose a puzzle from the plan and remove it from both the plan and later available puzzles (so it is only chosen once)
var puzzle = _plannedPuzzles.PopRandomElement();
_availablePuzzles.Remove(puzzle);
return puzzle;
}
#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)
{
if (NumberOfRooms > 2)

View File

@@ -18,13 +18,20 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
private static Dictionary<int, PuzzleMeasurement> _runningMeasurements;
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) =>
PuzzleStorage.Instance.Load(puzzle).AverageTimeToSolve;
public static float AverageTime(IEnumerable<PuzzleModuleDescription> puzzles) => puzzles.Sum(AverageTime);
public static float AverageTime(Room room) =>
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)
{
_runningMeasurements[puzzle.Id] = new PuzzleMeasurement

View File

@@ -17,7 +17,8 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
public float TotalTimeSpentOnPuzzle => Measurements.Sum(measurement => measurement.Time);
public float AverageTimeToSolve => Measurements.Count > 0 ? TotalTimeSpentOnPuzzle / Measurements.Count : 0f;
public NormalDistribution Distribution => new(TimesAsSamples());
[UsedImplicitly]
public Puzzle() {}
public Puzzle(PuzzleModuleDescription puzzle)
@@ -25,6 +26,23 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
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()
{
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
{
private const int SchemaVersion = 1;
public static PuzzleStorage Instance { get; private set; }
[SerializeField]
@@ -17,7 +19,20 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
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()
@@ -41,10 +56,7 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
{
session.Time = time;
_realm.Write(() =>
{
_realm.Add(session);
});
_realm.Write(() => _realm.Add(session));
}
#endregion
@@ -84,6 +96,9 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
// add solved puzzle to session
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.Diagnostics.CodeAnalysis;
using System.Linq;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using JetBrains.Annotations;
using MongoDB.Bson;
@@ -13,8 +14,11 @@ namespace EscapeRoomEngine.Engine.Runtime.Measurements
[PrimaryKey]
public ObjectId ID { get; set; }
public float Time { get; set; }
public IList<float> Percentiles { get; }
public IList<Puzzle> PuzzlesSolved { get; }
public float MeanPercentile => Percentiles.Count == 0 ? .5f : Probability.Mean(Percentiles.ToArray());
[UsedImplicitly]
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 NaughtyAttributes;
using UnityEngine;
@@ -15,20 +17,31 @@ namespace EscapeRoomEngine.Engine.Runtime.UI
{
public static GameControl Instance { get; private set; }
[SerializeField]
private float uiUpdateInterval = 1, planUpdateInterval = 1;
[BoxGroup("Internal")] [SerializeField]
private Button startButton, stopButton, pauseButton, addTimeButton, removeTimeButton;
[BoxGroup("Internal")] [SerializeField]
private Text timeText, roomTimeText, estimateTimeText, targetTimeText;
private Text timeText, roomTimeText, estimateTimeText, targetTimeText, percentileText;
[BoxGroup("Internal")] [SerializeField]
private float uiUpdateInterval = 1;
private PuzzlePlan puzzlePlan;
[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 TimeInRoom { get; set; }
public float TargetTime { get; private set; }
public float EstimatedTimeRoom { get; private set; }
private float _previousUIUpdate;
private float _previousUIUpdate, _previousPlanUpdate;
private void Awake()
{
@@ -57,6 +70,15 @@ namespace EscapeRoomEngine.Engine.Runtime.UI
_previousUIUpdate = Time.time;
SetTimeText();
UpdateStats();
}
// update plan
if (Time.time > _previousPlanUpdate + planUpdateInterval)
{
_previousPlanUpdate = Time.time;
Engine.Instance.PlanPuzzles();
}
// enable or disable buttons
@@ -129,15 +151,21 @@ namespace EscapeRoomEngine.Engine.Runtime.UI
Engine.Instance.CurrentRoom.Match(some: room =>
{
estimateTimeText.text = TimeToText(
TimeElapsed - TimeInRoom
+ Mathf.Max(TimeInRoom, Measure.AverageTime(room))
+ Engine.Instance.EstimatedTimeRemaining);
EstimatedTimeRoom =
TimeElapsed - TimeInRoom + Mathf.Max(TimeInRoom, Measure.EstimateTime(room));
estimateTimeText.text = TimeToText(EstimatedTimeRoom + Engine.Instance.EstimatedTimeRemaining);
});
}
private void UpdateStats()
{
percentileText.text = PercentageToText(Measure.SessionPercentile());
}
private static string TimeToText(float time) => $"{time.ToTimeSpan():mm':'ss}";
private static string PercentageToText(float percentage) => $"{percentage:P1}";
#endregion
#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.Linq;
using MathNet.Numerics.Distributions;
using UnityEngine;
using Random = System.Random;
namespace EscapeRoomEngine.Engine.Runtime.Utilities
@@ -8,19 +9,21 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
[Serializable]
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.StandardDeviation(samples, mean);
μ = Probability.Mean(samples);
σ = 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
@@ -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.
/// </summary>
/// <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
do
{
u1 = 2 * _random.NextDouble() - 1;
u2 = 2 * _random.NextDouble() - 1;
u1 = 2 * (float)_random.NextDouble() - 1;
u2 = 2 * (float)_random.NextDouble() - 1;
square = u1 * u1 + u2 * u2;
} 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)
{
@@ -57,10 +60,10 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
return samples.Sum() / samples.Length;
}
public static double StandardDeviation(double[] samples) => StandardDeviation(samples, Mean(samples));
public static double StandardDeviation(double[] samples, double mean)
public static float StandardDeviation(float[] samples) => StandardDeviation(samples, Mean(samples));
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++)
{
@@ -68,7 +71,7 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
deviations[i] = d * d;
}
return Math.Sqrt(Mean(deviations));
return Mathf.Sqrt(Mean(deviations));
}
}
}

View File

@@ -12,6 +12,19 @@
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}}}";
}
}

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 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)
{
var index = Random.Range(0, list.Count);
@@ -35,7 +41,20 @@ namespace EscapeRoomEngine.Engine.Runtime.Utilities
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