From f6a46df9107bd71c7a7a26abb176f2d8789ff5de Mon Sep 17 00:00:00 2001 From: YIFN Date: Tue, 18 Aug 2026 16:50:30 +0900 Subject: [PATCH 1/3] chore: establish OpenAI Game Builders submission lane --- AGENTS.md | 19 +++++++++-- Docs/Submission/OpenAIGame2026/baseline.md | 37 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 Docs/Submission/OpenAIGame2026/baseline.md diff --git a/AGENTS.md b/AGENTS.md index f20894b..5cc3971 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# Home Protector agent contract +# Home Protector OpenAI Game Builders contract -This repository is a Unity 2022.3.60f1 project completed from the existing `isometric scene` gameplay. Keep changes incremental and preserve working legacy systems while moving ownership to `Assets/_Project`. +This worktree is the WebGL-first OpenAI Game Builders submission lane for the Unity 2022.3.60f1 project. Keep changes incremental and preserve the verified `isometric scene` gameplay while adapting it for browser play. ## Required boundaries @@ -8,7 +8,19 @@ This repository is a Unity 2022.3.60f1 project completed from the existing `isom - Keep `WaveSystem`, `EnemySpawner`, `TowerSpawner`, `TargetManager`, and `ResourceManager` as the combat engine; adapt them through bridges instead of rewriting them. - Keep CommonSoldier and Monkey. Treat the old PostBox prefab as the refrigerator placeholder and preserve its draggable, target, and resource behavior while migrating it. - Keep microphone activation and a keyboard fallback on the same activation path. -- Do not add WebGL, GitHub Pages, NAN submission automation, or an in-game AI director. +- Add WebGL and browser compatibility only on `codex/openaigame2026`; keep platform-neutral fixes easy to cherry-pick. +- Do not add NAN submission automation or an in-game AI director. + +## Contest delivery + +- The required deliverable is a public browser link that starts without installation, approval, or login. +- Browser completion must never depend on microphone permission. Keyboard fallback uses the same player-activation path. +- Request microphone permission only after an explicit user gesture and only on HTTPS. Denial, timeout, or missing devices must fall back cleanly. +- Use this contest worktree as the only Unity writer while WebGL work is active. Treat the original Windows worktree as read-only for contest changes. +- Write local builds only to ignored `Builds/OpenAIGame2026-WebGL`. Never commit generated WebGL output to the source branch. +- If deployment needs a generated-output branch or hosting project, keep it separate from source history. +- Record the pre-challenge baseline and every challenge-period feature in `Docs/Submission/OpenAIGame2026`. +- Internal submission freeze is 2026-08-26 18:00 KST; prioritize a completable hosted build over additional content. ## Unity ownership @@ -28,6 +40,7 @@ This repository is a Unity 2022.3.60f1 project completed from the existing `isom - Let Unity own compile, serialization, reference, EditMode, PlayMode, and build verification. - Use `Tools/Unity/Invoke-HomeProtectorUnity.ps1` for concise summaries. Read full Unity logs only after a failure. +- WebGL readiness requires a Unity WebGL build plus an HTTPS browser smoke test of Loading to final result, including keyboard fallback. - Use `Tools/Git/Validate-UnityRepo.ps1` for repository hygiene; do not duplicate Unity semantic checks in shell scripts or skills. - A missing Unity license is an explicit blocked check, never a passing result. diff --git a/Docs/Submission/OpenAIGame2026/baseline.md b/Docs/Submission/OpenAIGame2026/baseline.md new file mode 100644 index 0000000..25c19ad --- /dev/null +++ b/Docs/Submission/OpenAIGame2026/baseline.md @@ -0,0 +1,37 @@ +# OpenAI Game Builders 2026 baseline + +## Submission constraints + +- Official source: https://openaigame2026.com/ (checked 2026-08-18 KST). +- Online qualifier submission closes on 2026-08-26. +- A publicly accessible game that runs directly in a browser is required. +- The core loop must be playable throughout judging without approval or installation. +- A 16:9 PNG or JPG thumbnail is required. +- A gameplay video of up to three minutes and a Codex collaboration explanation are optional scoring opportunities. +- Existing projects are allowed, but challenge-period additions must be identified. + +## Pre-existing project baseline + +- Baseline commit: `0e594273b5cda13ce8226dc187e2b6ea05c62e81`. +- Baseline date: 2026-08-18 KST. +- Engine: Unity 2022.3.60f1. +- Playable scene: `Assets/Scenes/isometric scene.unity`. +- Existing core: preparation, placement, wave combat, protected resources, legacy microphone activation, and keyboard-capable loading input. +- Preserved content identities include CommonSoldier, Monkey, Bear, Cockroach, Rice, Dryer, and CoolDryer. + +## Challenge-period scope + +Track additions here as they land on `codex/openaigame2026`: + +| Date | Commit | New work | Codex contribution | Human decision | +|---|---|---|---|---| +| 2026-08-18 | pending | Contest worktree, WebGL contract, and reproducible baseline | Repository audit, isolation plan, and verification harness | WebGL-first schedule and use of the existing game concept | + +## Release gate + +- Loading screen reaches gameplay in a supported desktop browser. +- One complete preparation-to-result loop works from the hosted HTTPS URL. +- Keyboard fallback can activate the player with microphone permission denied. +- WebGL console has no uncaught exceptions or missing-content errors. +- Controls are visible before play starts. +- Public link works in a fresh browser session without authentication. From 29bc436d9ce3031db40af9a495ecdf5c9eca1072 Mon Sep 17 00:00:00 2001 From: YIFN Date: Tue, 25 Aug 2026 01:29:25 +0900 Subject: [PATCH 2/3] feat: add OpenAI Game Builders WebGL build --- .../Scripts/Editor/WebGLBuildPipeline.meta | 8 + .../OpenAIGame2026WebGLBuild.cs | 164 ++++++++++++++++++ .../OpenAIGame2026WebGLBuild.cs.meta | 11 ++ .../Scripts/Legacy/MicrophoneSystem.cs | 56 +++++- .../Legacy/unused/MicroPhoneVolumeTest.cs | 2 + .../Editor/OpenAIGame2026WebGLBuildTests.cs | 146 ++++++++++++++++ .../OpenAIGame2026WebGLBuildTests.cs.meta | 11 ++ .../Editor/WebGLInputFallbackTests.cs | 31 ++++ .../Editor/WebGLInputFallbackTests.cs.meta | 11 ++ Tools/Unity/Invoke-HomeProtectorUnity.ps1 | 10 +- 10 files changed, 444 insertions(+), 6 deletions(-) create mode 100644 Assets/_Project/Scripts/Editor/WebGLBuildPipeline.meta create mode 100644 Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs create mode 100644 Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs.meta create mode 100644 Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs create mode 100644 Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs.meta create mode 100644 Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs create mode 100644 Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs.meta diff --git a/Assets/_Project/Scripts/Editor/WebGLBuildPipeline.meta b/Assets/_Project/Scripts/Editor/WebGLBuildPipeline.meta new file mode 100644 index 0000000..c5b5b9e --- /dev/null +++ b/Assets/_Project/Scripts/Editor/WebGLBuildPipeline.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 89e728165c93dc540b7ec7eca988b467 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs b/Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs new file mode 100644 index 0000000..c99cb79 --- /dev/null +++ b/Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using System.Linq; +using HomeProtector.Editor.AssetPipeline; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace HomeProtector.Editor.Build +{ + public static class OpenAIGame2026WebGLBuild + { + private const string LoadingScenePath = "Assets/Scenes/StartGame_Loading.unity"; + private const string PlayableScenePath = "Assets/Scenes/isometric scene.unity"; + private const string OutputFolderName = "OpenAIGame2026-WebGL"; + private const string OutputRelativePath = "Builds/" + OutputFolderName; + + [MenuItem("Home Protector/OpenAI Game Builders/Build WebGL")] + public static void Build() + { + HomeProtectorAutomation.ValidateProject(); + + if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.WebGL, BuildTarget.WebGL)) + { + throw new BuildFailedException("Unity WebGL build support is not installed."); + } + + string[] scenes = EditorBuildSettings.scenes + .Where(scene => scene.enabled) + .Select(scene => scene.path) + .ToArray(); + ValidateScenes(scenes); + + string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + string outputDirectory = GetSafeOutputDirectory(projectRoot); + ConfigurePlayerSettings(); + RecreateOutputDirectory(outputDirectory); + + BuildPlayerOptions options = new() + { + scenes = scenes, + locationPathName = outputDirectory, + target = BuildTarget.WebGL, + options = BuildOptions.StrictMode, + }; + + BuildReport report = BuildPipeline.BuildPlayer(options); + if (report.summary.result != BuildResult.Succeeded) + { + throw new BuildFailedException( + $"WebGL build failed with {report.summary.totalErrors} error(s). " + + $"See the Unity build log for details."); + } + + string indexPath = Path.Combine(outputDirectory, "index.html"); + if (!File.Exists(indexPath)) + { + throw new BuildFailedException($"WebGL build succeeded without '{indexPath}'."); + } + + Debug.Log( + $"HOME_PROTECTOR_WEBGL_BUILD_OK output={outputDirectory} " + + $"scenes={scenes.Length} bytes={report.summary.totalSize}"); + } + + private static void ValidateScenes(string[] scenes) + { + if (scenes.Length < 2 || scenes[0] != LoadingScenePath) + { + throw new BuildFailedException( + $"WebGL build requires '{LoadingScenePath}' as the first enabled scene."); + } + + if (!scenes.Contains(PlayableScenePath, StringComparer.Ordinal)) + { + throw new BuildFailedException( + $"WebGL build requires enabled playable scene '{PlayableScenePath}'."); + } + } + + private static string GetSafeOutputDirectory(string projectRoot) + { + string buildsRoot = Path.GetFullPath(Path.Combine(projectRoot, "Builds")); + string outputDirectory = Path.GetFullPath(Path.Combine(projectRoot, OutputRelativePath)); + string requiredPrefix = buildsRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + + Path.DirectorySeparatorChar; + + if (!outputDirectory.StartsWith(requiredPrefix, StringComparison.OrdinalIgnoreCase) || + !string.Equals( + Path.GetFileName(outputDirectory), + OutputFolderName, + StringComparison.Ordinal)) + { + throw new BuildFailedException($"Unsafe WebGL output path '{outputDirectory}'."); + } + + return outputDirectory; + } + + private static void ConfigurePlayerSettings() + { + PlayerSettings.defaultScreenWidth = 1280; + PlayerSettings.defaultScreenHeight = 720; + PlayerSettings.defaultWebScreenWidth = 1280; + PlayerSettings.defaultWebScreenHeight = 720; + PlayerSettings.runInBackground = false; + PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Disabled; + PlayerSettings.WebGL.decompressionFallback = false; + PlayerSettings.WebGL.dataCaching = true; + PlayerSettings.WebGL.nameFilesAsHashes = true; + AssetDatabase.SaveAssets(); + } + + private static void RecreateOutputDirectory(string outputDirectory) + { + string buildsRoot = Directory.GetParent(outputDirectory)?.FullName + ?? throw new BuildFailedException( + $"WebGL output has no parent directory: '{outputDirectory}'."); + ThrowIfReparsePoint(buildsRoot); + + if (Directory.Exists(outputDirectory)) + { + ThrowIfTreeContainsReparsePoint(outputDirectory); + Directory.Delete(outputDirectory, true); + } + + Directory.CreateDirectory(outputDirectory); + } + + private static void ThrowIfTreeContainsReparsePoint(string directory) + { + ThrowIfReparsePoint(directory); + + foreach (string entry in Directory.EnumerateFileSystemEntries( + directory, + "*", + SearchOption.TopDirectoryOnly)) + { + ThrowIfReparsePoint(entry); + + if (Directory.Exists(entry)) + { + ThrowIfTreeContainsReparsePoint(entry); + } + } + } + + private static void ThrowIfReparsePoint(string path) + { + if (!File.Exists(path) && !Directory.Exists(path)) + { + return; + } + + FileAttributes attributes = File.GetAttributes(path); + if ((attributes & FileAttributes.ReparsePoint) != 0) + { + throw new BuildFailedException( + $"Refusing to clean WebGL output through reparse point '{path}'."); + } + } + } +} diff --git a/Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs.meta b/Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs.meta new file mode 100644 index 0000000..a9bee16 --- /dev/null +++ b/Assets/_Project/Scripts/Editor/WebGLBuildPipeline/OpenAIGame2026WebGLBuild.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c588424b302131c419f07c2e8e535840 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Legacy/MicrophoneSystem.cs b/Assets/_Project/Scripts/Legacy/MicrophoneSystem.cs index 14f7e3c..5a2b3d5 100644 --- a/Assets/_Project/Scripts/Legacy/MicrophoneSystem.cs +++ b/Assets/_Project/Scripts/Legacy/MicrophoneSystem.cs @@ -36,6 +36,8 @@ public class MicrophoneSystem : MonoBehaviour [SerializeField] private bool playerActivationEnabled = true; // Ç÷¹À̾î Ȱ¼ºÈ­ °¡´É ¿©ºÎ [SerializeField] private bool oneTimeUseOnly = true; // ÇÑ ¿þÀ̺ê´ç ÇÑ ¹ø¸¸ »ç¿ë °¡´É ¿©ºÎ [SerializeField] private float playerActiveTime = 50f; // Ç÷¹À̾î Ȱ¼ºÈ­ À¯Áö ½Ã°£ (Àú³á ¸ðµå¿¡¼­) + [SerializeField] private bool keyboardFallbackEnabled = true; + [SerializeField] private KeyCode keyboardActivationKey = KeyCode.F; private bool hasActivatedPlayer = false; // ÀÌ¹Ì Ç÷¹À̾ Ȱ¼ºÈ­Çß´ÂÁö ¿©ºÎ private Vector3 defaultPlayerPosition = Vector3.zero; // ±âº» Ç÷¹À̾î À§Ä¡ (0,0,0) @@ -211,9 +213,15 @@ private void Update() } else if (timeSystem != null && timeSystem.CurrentTime == TimeOfDay.Evening) { + if (keyboardFallbackEnabled && Input.GetKeyDown(keyboardActivationKey)) + { + RequestPlayerActivation(); + } + +#if !UNITY_WEBGL || UNITY_EDITOR // Àú³á ¸ðµå¿¡¼­¸¸ ¸¶ÀÌÅ© °¨Áö // ¸¶ÀÌÅ© º¼·ý È®ÀÎ ¹× ¹èÄ¡ ¸ðµå Àüȯ - if (micClip != null && micName != null && Microphone.IsRecording(micName)) + if (!isPlacementMode && micClip != null && micName != null && Microphone.IsRecording(micName)) { float volume = GetMaxVolume(); scaledVolume = ScaleVolume(volume); @@ -231,10 +239,10 @@ private void Update() if (scaledVolume >= currentActivationThreshold) { if (verbose) Debug.Log($"º¼·ý({scaledVolume})ÀÌ ÀÓ°è°ª({currentActivationThreshold})À» ³Ñ¾î ¹èÄ¡ ¸ðµå ÁøÀÔ"); - EnterPlacementMode(); + RequestPlayerActivation(); } } - else + else if (!isPlacementMode) { // ¸¶ÀÌÅ©°¡ ³ìÀ½ ÁßÀÌ ¾Æ´Ñ °æ¿ì if (verbose && Time.frameCount % 300 == 0) @@ -243,6 +251,7 @@ private void Update() StartCoroutine(InitializeAndTestMicrophone()); } } +#endif } // µð¹ö±× Á¤º¸ ¾÷µ¥ÀÌÆ® @@ -251,12 +260,14 @@ private void Update() private void OnDisable() { +#if !UNITY_WEBGL || UNITY_EDITOR // ¸¶ÀÌÅ© Á¤Áö if (micName != null && Microphone.IsRecording(micName)) { Microphone.End(micName); if (verbose) Debug.Log("¸¶ÀÌÅ© ³ìÀ½ ÁßÁöµÊ"); } +#endif // °ÔÀÓÀÌ ÀϽÃÁ¤ÁöµÈ »óÅ·ΠÁ¾·áµÇÁö ¾Êµµ·Ï ÇÔ if (wasGamePaused) @@ -285,6 +296,14 @@ private void OnDestroy() #region Initialization Methods private IEnumerator InitializeAndTestMicrophone() { +#if UNITY_WEBGL && !UNITY_EDITOR + if (verbose) + { + Debug.Log("WebGL microphone input is unavailable. Use the keyboard fallback."); + } + + yield break; +#else // ¾ÈÁ¤ÀûÀÎ ÃʱâÈ­¸¦ À§ÇØ Âª°Ô ´ë±â yield return new WaitForSeconds(0.2f); @@ -334,6 +353,7 @@ private IEnumerator InitializeAndTestMicrophone() { Debug.LogError("»ç¿ë °¡´ÉÇÑ ¸¶ÀÌÅ©°¡ ¾ø½À´Ï´Ù!"); } +#endif } private void InitializeFatigueCurve() @@ -399,6 +419,9 @@ private Sprite GetCircleSprite() #region Microphone Volume Processing private void CheckMicrophoneVolumeForActivation() { +#if UNITY_WEBGL && !UNITY_EDITOR + return; +#else if (micName != null && Microphone.IsRecording(micName)) { float volume = GetMaxVolume(); @@ -410,18 +433,22 @@ private void CheckMicrophoneVolumeForActivation() // º¼·ýÀÌ ÀÓ°è°ªÀ» ³ÑÀ¸¸é ¹èÄ¡ ¸ðµå Àüȯ if (scaledVolume >= currentActivationThreshold) { - EnterPlacementMode(); + RequestPlayerActivation(); } } else if (verbose) { Debug.LogWarning("¸¶ÀÌÅ©°¡ ³ìÀ½ ÁßÀÌ ¾Æ´Ï¾î¼­ º¼·ý È®ÀÎ ºÒ°¡"); } +#endif } private float GetMaxVolume() { if (micClip == null) return 0; +#if UNITY_WEBGL && !UNITY_EDITOR + return 0f; +#else float[] samples = new float[sampleWindow]; int micPosition = Microphone.GetPosition(micName); @@ -452,6 +479,7 @@ private float GetMaxVolume() } return maxVolume; +#endif } private int ScaleVolume(float volume) @@ -481,6 +509,26 @@ private void UpdateActivationThreshold() #endregion #region Player Placement Mode + public void RequestPlayerActivation() + { + if (!playerActivationEnabled || isPlacementMode) + { + return; + } + + if (timeSystem != null && timeSystem.CurrentTime != TimeOfDay.Evening) + { + return; + } + + if (oneTimeUseOnly && hasActivatedPlayer) + { + return; + } + + EnterPlacementMode(); + } + private void EnterPlacementMode() { isPlacementMode = true; diff --git a/Assets/_Project/Scripts/Legacy/unused/MicroPhoneVolumeTest.cs b/Assets/_Project/Scripts/Legacy/unused/MicroPhoneVolumeTest.cs index 514abc1..91b211c 100644 --- a/Assets/_Project/Scripts/Legacy/unused/MicroPhoneVolumeTest.cs +++ b/Assets/_Project/Scripts/Legacy/unused/MicroPhoneVolumeTest.cs @@ -2,6 +2,7 @@ public class MicroPhoneVolumeTest : MonoBehaviour { +#if !UNITY_WEBGL || UNITY_EDITOR private AudioClip micClip; private string micName; @@ -60,5 +61,6 @@ float GetMaxVolume() return maxVolume; } +#endif } diff --git a/Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs b/Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs new file mode 100644 index 0000000..8824317 --- /dev/null +++ b/Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using HomeProtector.Editor.Build; +using NUnit.Framework; +using UnityEditor; +using UnityEditor.Build; + +namespace HomeProtector.Tests.LegacyEditMode +{ + public sealed class OpenAIGame2026WebGLBuildTests + { + private string tempRoot; + private string junctionPath; + + [SetUp] + public void SetUp() + { + tempRoot = Path.Combine( + Path.GetTempPath(), + "HomeProtectorWebGLBuildTests", + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempRoot); + } + + [TearDown] + public void TearDown() + { + if (!string.IsNullOrEmpty(junctionPath) && Directory.Exists(junctionPath)) + { + Directory.Delete(junctionPath); + } + + if (!string.IsNullOrEmpty(tempRoot) && Directory.Exists(tempRoot)) + { + Directory.Delete(tempRoot, true); + } + } + + [Test] + public void RecreateOutputDirectoryRejectsJunctionWithoutTouchingTarget() + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + Assert.Ignore("Junction coverage is Windows-only."); + } + + string buildsRoot = Path.Combine(tempRoot, "Builds"); + string externalTarget = Path.Combine(tempRoot, "OutsideTarget"); + junctionPath = Path.Combine(buildsRoot, "OpenAIGame2026-WebGL"); + string markerPath = Path.Combine(externalTarget, "marker.txt"); + + Directory.CreateDirectory(buildsRoot); + Directory.CreateDirectory(externalTarget); + File.WriteAllText(markerPath, "preserve"); + CreateJunction(junctionPath, externalTarget); + + MethodInfo recreate = typeof(OpenAIGame2026WebGLBuild).GetMethod( + "RecreateOutputDirectory", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.That(recreate, Is.Not.Null); + + TargetInvocationException exception = Assert.Throws( + () => recreate.Invoke(null, new object[] { junctionPath })); + + Assert.That(exception.InnerException, Is.TypeOf()); + Assert.That(File.Exists(markerPath), Is.True); + } + + [Test] + public void ConfigurePlayerSettingsSetsWebCanvasDimensions() + { + WithConfiguredPlayerSettings(() => + { + Assert.That(PlayerSettings.defaultWebScreenWidth, Is.EqualTo(1280)); + Assert.That(PlayerSettings.defaultWebScreenHeight, Is.EqualTo(720)); + }); + } + + [Test] + public void ConfigurePlayerSettingsUsesHashedWebGLFiles() + { + WithConfiguredPlayerSettings(() => + Assert.That(PlayerSettings.WebGL.nameFilesAsHashes, Is.True)); + } + + private static void WithConfiguredPlayerSettings(Action assertions) + { + int standaloneWidth = PlayerSettings.defaultScreenWidth; + int standaloneHeight = PlayerSettings.defaultScreenHeight; + int webWidth = PlayerSettings.defaultWebScreenWidth; + int webHeight = PlayerSettings.defaultWebScreenHeight; + bool runInBackground = PlayerSettings.runInBackground; + WebGLCompressionFormat compression = PlayerSettings.WebGL.compressionFormat; + bool decompressionFallback = PlayerSettings.WebGL.decompressionFallback; + bool dataCaching = PlayerSettings.WebGL.dataCaching; + bool nameFilesAsHashes = PlayerSettings.WebGL.nameFilesAsHashes; + + try + { + MethodInfo configure = typeof(OpenAIGame2026WebGLBuild).GetMethod( + "ConfigurePlayerSettings", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.That(configure, Is.Not.Null); + configure.Invoke(null, null); + assertions(); + } + finally + { + PlayerSettings.defaultScreenWidth = standaloneWidth; + PlayerSettings.defaultScreenHeight = standaloneHeight; + PlayerSettings.defaultWebScreenWidth = webWidth; + PlayerSettings.defaultWebScreenHeight = webHeight; + PlayerSettings.runInBackground = runInBackground; + PlayerSettings.WebGL.compressionFormat = compression; + PlayerSettings.WebGL.decompressionFallback = decompressionFallback; + PlayerSettings.WebGL.dataCaching = dataCaching; + PlayerSettings.WebGL.nameFilesAsHashes = nameFilesAsHashes; + AssetDatabase.SaveAssets(); + } + } + + private static void CreateJunction(string junction, string target) + { + ProcessStartInfo startInfo = new() + { + FileName = "cmd.exe", + Arguments = $"/d /c mklink /J \"{junction}\" \"{target}\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + + using Process process = Process.Start(startInfo); + process.WaitForExit(); + + Assert.That( + process.ExitCode, + Is.EqualTo(0), + process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd()); + } + } +} diff --git a/Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs.meta b/Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs.meta new file mode 100644 index 0000000..03e834c --- /dev/null +++ b/Assets/_Project/Tests/LegacyEditMode/Editor/OpenAIGame2026WebGLBuildTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b63425861a704141965113647b6aa97 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs b/Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs new file mode 100644 index 0000000..4c06016 --- /dev/null +++ b/Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using NUnit.Framework; +using UnityEngine; + +namespace HomeProtector.Tests.LegacyEditMode +{ + public sealed class WebGLInputFallbackTests + { + [Test] + public void MicrophoneSystemDefinesKeyboardFallbackAndSharedActivationRequest() + { + const BindingFlags privateInstance = BindingFlags.Instance | BindingFlags.NonPublic; + FieldInfo fallbackEnabled = typeof(MicrophoneSystem).GetField( + "keyboardFallbackEnabled", + privateInstance); + FieldInfo activationKey = typeof(MicrophoneSystem).GetField( + "keyboardActivationKey", + privateInstance); + MethodInfo activationRequest = typeof(MicrophoneSystem).GetMethod( + "RequestPlayerActivation", + BindingFlags.Instance | BindingFlags.Public); + + Assert.That(fallbackEnabled, Is.Not.Null); + Assert.That(fallbackEnabled.FieldType, Is.EqualTo(typeof(bool))); + Assert.That(activationKey, Is.Not.Null); + Assert.That(activationKey.FieldType, Is.EqualTo(typeof(KeyCode))); + Assert.That(activationRequest, Is.Not.Null); + Assert.That(activationRequest.ReturnType, Is.EqualTo(typeof(void))); + } + } +} diff --git a/Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs.meta b/Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs.meta new file mode 100644 index 0000000..c73f397 --- /dev/null +++ b/Assets/_Project/Tests/LegacyEditMode/Editor/WebGLInputFallbackTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc0c4e72687413c4dbb6b7ce96200463 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tools/Unity/Invoke-HomeProtectorUnity.ps1 b/Tools/Unity/Invoke-HomeProtectorUnity.ps1 index 4f31bff..1130466 100644 --- a/Tools/Unity/Invoke-HomeProtectorUnity.ps1 +++ b/Tools/Unity/Invoke-HomeProtectorUnity.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [ValidateSet('EditMode', 'PlayMode', 'Validate')] + [ValidateSet('EditMode', 'PlayMode', 'Validate', 'BuildWebGL')] [string]$Mode = 'EditMode', [string]$ProjectRoot, [string]$UnityPath = $env:UNITY_EDITOR_PATH @@ -42,7 +42,7 @@ $arguments = @( '-logFile', $logPath ) -if ($Mode -eq 'Validate') { +if ($Mode -in @('Validate', 'BuildWebGL')) { $arguments += '-quit' } @@ -56,6 +56,12 @@ switch ($Mode) { 'Validate' { $arguments += @('-executeMethod', 'HomeProtector.Editor.AssetPipeline.HomeProtectorAutomation.ValidateProject') } + 'BuildWebGL' { + $arguments += @( + '-buildTarget', 'WebGL', + '-executeMethod', 'HomeProtector.Editor.Build.OpenAIGame2026WebGLBuild.Build' + ) + } } $processArguments = @($arguments | ForEach-Object { From 01947de0c9e5f86855066c2c7f3fdbba70234ae8 Mon Sep 17 00:00:00 2001 From: YIFN Date: Tue, 25 Aug 2026 01:31:49 +0900 Subject: [PATCH 3/3] docs: record OpenAI Game Builders submission status --- Docs/Submission/OpenAIGame2026/baseline.md | 5 +-- .../OpenAIGame2026/submission-status.md | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Docs/Submission/OpenAIGame2026/submission-status.md diff --git a/Docs/Submission/OpenAIGame2026/baseline.md b/Docs/Submission/OpenAIGame2026/baseline.md index 25c19ad..26ca4fc 100644 --- a/Docs/Submission/OpenAIGame2026/baseline.md +++ b/Docs/Submission/OpenAIGame2026/baseline.md @@ -6,7 +6,7 @@ - Online qualifier submission closes on 2026-08-26. - A publicly accessible game that runs directly in a browser is required. - The core loop must be playable throughout judging without approval or installation. -- A 16:9 PNG or JPG thumbnail is required. +- A thumbnail is required; 16:9 PNG or JPG up to 10 MB is recommended. - A gameplay video of up to three minutes and a Codex collaboration explanation are optional scoring opportunities. - Existing projects are allowed, but challenge-period additions must be identified. @@ -25,7 +25,8 @@ Track additions here as they land on `codex/openaigame2026`: | Date | Commit | New work | Codex contribution | Human decision | |---|---|---|---|---| -| 2026-08-18 | pending | Contest worktree, WebGL contract, and reproducible baseline | Repository audit, isolation plan, and verification harness | WebGL-first schedule and use of the existing game concept | +| 2026-08-18 | `f6a46df9` | Contest worktree, WebGL contract, and reproducible baseline | Repository audit, isolation plan, and verification harness | WebGL-first schedule and use of the existing game concept | +| 2026-08-25 | `29bc436d` | Reproducible WebGL build, browser-safe keyboard activation fallback, and regression tests | WebGL compatibility diagnosis, implementation, TDD, deployment, and verification | Preserve native microphone play while making browser completion independent of microphone permission | ## Release gate diff --git a/Docs/Submission/OpenAIGame2026/submission-status.md b/Docs/Submission/OpenAIGame2026/submission-status.md new file mode 100644 index 0000000..aff819b --- /dev/null +++ b/Docs/Submission/OpenAIGame2026/submission-status.md @@ -0,0 +1,32 @@ +# OpenAI Game Builders 2026 submission status + +## Candidate + +- Game: Home Protector +- Public browser build: https://home-protector-openaigame2026.vercel.app +- Source branch: `codex/openaigame2026` +- Draft pull request: https://github.com/YIFNEN/HomeProtector/pull/2 +- Rebuild command: `Tools/Unity/Invoke-HomeProtectorUnity.ps1 -Mode BuildWebGL` + +## Verified on 2026-08-25 KST + +- Unity EditMode: 23/23 passed. +- Unity project validation: passed. +- Unity PlayMode: 3/3 passed. +- Unity WebGL build: passed with two enabled scenes. +- Generated output: 17 files, 44,955,417 bytes (42.87 MiB). +- Repository hygiene: passed; generated WebGL output is ignored. +- Hosted index and loader, data, framework, and Wasm resources returned HTTP 200. + +## Required before submission + +- Complete a fresh-browser manual play from Loading through a final result. +- Make `Evening: F, then click a position` visible before or during play. +- Capture and upload the required thumbnail. +- Approve a final title and introduction of no more than 200 characters. +- Recheck the public URL without an authenticated hosting session. + +## Optional scoring material + +- Record up to three minutes of unmodified actual gameplay. +- Summarize Codex collaboration using the challenge-period table in `baseline.md`.