diff --git a/Unity/AIForOceans/Assets/Audio/no/no_1.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_1.mp3
new file mode 100755
index 00000000..375e976d
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_1.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_10.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_10.mp3
new file mode 100755
index 00000000..8378b2d2
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_10.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_2.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_2.mp3
new file mode 100755
index 00000000..6977fd9a
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_2.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_3.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_3.mp3
new file mode 100755
index 00000000..356a17e2
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_3.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_4.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_4.mp3
new file mode 100755
index 00000000..e1a783a0
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_4.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_5.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_5.mp3
new file mode 100755
index 00000000..f4c2f5a8
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_5.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_6.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_6.mp3
new file mode 100755
index 00000000..de5a0232
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_6.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_7.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_7.mp3
new file mode 100755
index 00000000..535b0877
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_7.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_8.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_8.mp3
new file mode 100755
index 00000000..4ee4e973
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_8.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/no/no_9.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_9.mp3
new file mode 100755
index 00000000..1f4c2dbd
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_9.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/other/other_1.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_1.mp3
new file mode 100755
index 00000000..6a7740a3
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_1.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/other/other_2.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_2.mp3
new file mode 100755
index 00000000..e8f7a126
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_2.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/other/other_3.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_3.mp3
new file mode 100755
index 00000000..e71cb300
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_3.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/other/other_4.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_4.mp3
new file mode 100755
index 00000000..d134990b
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_4.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/sortno/sortno_1.mp3 b/Unity/AIForOceans/Assets/Audio/sortno/sortno_1.mp3
new file mode 100644
index 00000000..57ee4828
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortno/sortno_1.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_1.mp3 b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_1.mp3
new file mode 100644
index 00000000..78846689
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_1.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_2.mp3 b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_2.mp3
new file mode 100644
index 00000000..d09b48e9
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_2.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_3.mp3 b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_3.mp3
new file mode 100644
index 00000000..b3dd4864
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_3.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_1.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_1.mp3
new file mode 100755
index 00000000..4d4c2cc3
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_1.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_10.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_10.mp3
new file mode 100755
index 00000000..7f16f99c
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_10.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_2.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_2.mp3
new file mode 100755
index 00000000..139879bd
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_2.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_3.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_3.mp3
new file mode 100755
index 00000000..95d78457
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_3.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_4.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_4.mp3
new file mode 100755
index 00000000..32d6d5f1
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_4.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_5.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_5.mp3
new file mode 100755
index 00000000..3ff711ef
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_5.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_6.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_6.mp3
new file mode 100755
index 00000000..b66727d8
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_6.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_7.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_7.mp3
new file mode 100755
index 00000000..ae47edf3
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_7.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_8.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_8.mp3
new file mode 100755
index 00000000..41c6fead
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_8.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_9.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_9.mp3
new file mode 100755
index 00000000..7719a4e4
Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_9.mp3 differ
diff --git a/Unity/AIForOceans/Assets/Scripts/AIForOceans.asmdef b/Unity/AIForOceans/Assets/Scripts/AIForOceans.asmdef
new file mode 100644
index 00000000..58aa4992
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/AIForOceans.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "AIForOceans",
+ "rootNamespace": "AIForOceans",
+ "references": [
+ "Unity.TextMeshPro"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Animation/FishAnimator.cs b/Unity/AIForOceans/Assets/Scripts/Animation/FishAnimator.cs
new file mode 100644
index 00000000..90602146
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Animation/FishAnimator.cs
@@ -0,0 +1,165 @@
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Handles fish animation - bobbing, movement, scanning pause
+ /// Mirrors the original renderer.js animation logic
+ ///
+ public class FishAnimator : MonoBehaviour
+ {
+ [Header("Animation Settings")]
+ public float bobAmplitude = 10f;
+ public float bobFrequency = 2f;
+ public float moveSpeed = 100f;
+
+ [Header("Scan Settings")]
+ public bool isPaused = false;
+ public float scanPauseDuration = 1.5f;
+
+ // Internal state
+ private float startTime;
+ private float pauseStartTime;
+ private float totalPauseTime;
+ private Vector2 startPosition;
+ private float bobPhase;
+
+ // S-curve animation for scanning
+ private bool isScanning = false;
+ private float scanStartTime;
+ private float scanProgress;
+
+ private void Start()
+ {
+ startTime = Time.time;
+ startPosition = transform.position;
+ }
+
+ ///
+ /// Initialize with ocean object data
+ ///
+ public void Initialize(OceanObjectData data)
+ {
+ bobPhase = data.bobPhase;
+ moveSpeed = data.speed;
+ startPosition = data.position;
+ transform.position = startPosition;
+ }
+
+ private void Update()
+ {
+ if (!isPaused)
+ {
+ UpdateMovement();
+ }
+
+ UpdateBobbing();
+
+ if (isScanning)
+ {
+ UpdateScanAnimation();
+ }
+ }
+
+ ///
+ /// Update horizontal movement
+ ///
+ private void UpdateMovement()
+ {
+ float elapsed = Time.time - startTime - totalPauseTime;
+ float xOffset = elapsed * moveSpeed;
+
+ Vector2 pos = transform.position;
+ pos.x = startPosition.x + xOffset;
+ transform.position = pos;
+ }
+
+ ///
+ /// Update bobbing animation (sine wave)
+ ///
+ private void UpdateBobbing()
+ {
+ float time = Time.time;
+ float bobOffset = Mathf.Sin(time * bobFrequency + bobPhase) * bobAmplitude;
+
+ Vector2 pos = transform.position;
+ pos.y = startPosition.y + bobOffset;
+ transform.position = pos;
+ }
+
+ ///
+ /// Start pause for scanning
+ ///
+ public void StartScan()
+ {
+ isPaused = true;
+ pauseStartTime = Time.time;
+ isScanning = true;
+ scanStartTime = Time.time;
+ scanProgress = 0f;
+ }
+
+ ///
+ /// End scanning and resume movement
+ ///
+ public void EndScan()
+ {
+ isPaused = false;
+ totalPauseTime += Time.time - pauseStartTime;
+ isScanning = false;
+ }
+
+ ///
+ /// Update scanning S-curve animation
+ ///
+ private void UpdateScanAnimation()
+ {
+ float elapsed = Time.time - scanStartTime;
+ scanProgress = Mathf.Clamp01(elapsed / scanPauseDuration);
+
+ // S-curve easing: slow at start and end, fast in middle
+ float easedProgress = SCurve(scanProgress);
+
+ // Can be used to animate scan overlay or other effects
+ // The original uses this to pause fish in center while AI analyzes
+ }
+
+ ///
+ /// S-curve (sigmoid-like) easing function
+ ///
+ public static float SCurve(float t)
+ {
+ // Hermite interpolation (smooth step)
+ return t * t * (3f - 2f * t);
+ }
+
+ ///
+ /// More pronounced S-curve
+ ///
+ public static float SCurveSteep(float t)
+ {
+ // Quintic smooth step
+ return t * t * t * (t * (t * 6f - 15f) + 10f);
+ }
+
+ ///
+ /// Check if fish has moved off screen
+ ///
+ public bool IsOffScreen(float screenWidth)
+ {
+ return transform.position.x > screenWidth + 100f;
+ }
+
+ ///
+ /// Reset to start position
+ ///
+ public void Reset()
+ {
+ startTime = Time.time;
+ totalPauseTime = 0f;
+ transform.position = startPosition;
+ isPaused = false;
+ isScanning = false;
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Animation/FishRenderer.cs b/Unity/AIForOceans/Assets/Scripts/Animation/FishRenderer.cs
new file mode 100644
index 00000000..ff05bd33
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Animation/FishRenderer.cs
@@ -0,0 +1,160 @@
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Renders a procedurally generated fish from sprite components
+ ///
+ public class FishRenderer : MonoBehaviour
+ {
+ [Header("Sprite Renderers")]
+ public SpriteRenderer bodyRenderer;
+ public SpriteRenderer eyesRenderer;
+ public SpriteRenderer mouthRenderer;
+ public SpriteRenderer dorsalFinRenderer;
+ public SpriteRenderer pectoralFinFrontRenderer;
+ public SpriteRenderer pectoralFinBackRenderer;
+ public SpriteRenderer tailFinRenderer;
+ public SpriteRenderer scalesRenderer;
+
+ [Header("Sprite Arrays (set in inspector or load from Resources)")]
+ public Sprite[] bodySprites;
+ public Sprite[] eyesSprites;
+ public Sprite[] mouthSprites;
+ public Sprite[] dorsalFinSprites;
+ public Sprite[] pectoralFinSprites;
+ public Sprite[] tailFinSprites;
+ public Sprite[] scalesSprites;
+
+ // Current fish data
+ private OceanObjectData currentData;
+
+ ///
+ /// Set up the fish visual from data
+ ///
+ public void SetupFish(OceanObjectData data)
+ {
+ currentData = data;
+
+ if (data.objectType != GameConstants.OceanObjectType.Fish)
+ {
+ Debug.LogWarning("FishRenderer.SetupFish called with non-fish object");
+ return;
+ }
+
+ // Set sprites
+ SetSpriteIfValid(bodyRenderer, bodySprites, data.bodyIndex);
+ SetSpriteIfValid(eyesRenderer, eyesSprites, data.eyesIndex);
+ SetSpriteIfValid(mouthRenderer, mouthSprites, data.mouthIndex);
+ SetSpriteIfValid(dorsalFinRenderer, dorsalFinSprites, data.dorsalFinIndex);
+ SetSpriteIfValid(pectoralFinFrontRenderer, pectoralFinSprites, data.pectoralFinIndex);
+ SetSpriteIfValid(pectoralFinBackRenderer, pectoralFinSprites, data.pectoralFinIndex);
+ SetSpriteIfValid(tailFinRenderer, tailFinSprites, data.tailFinIndex);
+ SetSpriteIfValid(scalesRenderer, scalesSprites, data.scalesIndex);
+
+ // Apply colors
+ ApplyColors(data);
+ }
+
+ ///
+ /// Set sprite if index is valid
+ ///
+ private void SetSpriteIfValid(SpriteRenderer renderer, Sprite[] sprites, int index)
+ {
+ if (renderer == null || sprites == null || sprites.Length == 0)
+ return;
+
+ int safeIndex = Mathf.Clamp(index, 0, sprites.Length - 1);
+ renderer.sprite = sprites[safeIndex];
+ }
+
+ ///
+ /// Apply color tinting to fish parts
+ ///
+ private void ApplyColors(OceanObjectData data)
+ {
+ // Body uses primary color
+ if (bodyRenderer != null)
+ bodyRenderer.color = data.primaryColor;
+
+ // Scales use secondary color
+ if (scalesRenderer != null)
+ scalesRenderer.color = data.secondaryColor;
+
+ // Fins use fin color
+ Color finColor = data.finColor;
+ if (dorsalFinRenderer != null)
+ dorsalFinRenderer.color = finColor;
+ if (pectoralFinFrontRenderer != null)
+ pectoralFinFrontRenderer.color = finColor;
+ if (pectoralFinBackRenderer != null)
+ pectoralFinBackRenderer.color = finColor;
+ if (tailFinRenderer != null)
+ tailFinRenderer.color = finColor;
+
+ // Eyes and mouth stay neutral (white/original)
+ if (eyesRenderer != null)
+ eyesRenderer.color = Color.white;
+ if (mouthRenderer != null)
+ mouthRenderer.color = Color.white;
+ }
+
+ ///
+ /// Set sorting order for all parts
+ ///
+ public void SetSortingOrder(int baseOrder)
+ {
+ // Back to front ordering
+ if (tailFinRenderer != null)
+ tailFinRenderer.sortingOrder = baseOrder;
+ if (pectoralFinBackRenderer != null)
+ pectoralFinBackRenderer.sortingOrder = baseOrder + 1;
+ if (dorsalFinRenderer != null)
+ dorsalFinRenderer.sortingOrder = baseOrder + 2;
+ if (bodyRenderer != null)
+ bodyRenderer.sortingOrder = baseOrder + 3;
+ if (scalesRenderer != null)
+ scalesRenderer.sortingOrder = baseOrder + 4;
+ if (pectoralFinFrontRenderer != null)
+ pectoralFinFrontRenderer.sortingOrder = baseOrder + 5;
+ if (eyesRenderer != null)
+ eyesRenderer.sortingOrder = baseOrder + 6;
+ if (mouthRenderer != null)
+ mouthRenderer.sortingOrder = baseOrder + 7;
+ }
+
+ ///
+ /// Flip the fish horizontally
+ ///
+ public void SetFlipped(bool flipped)
+ {
+ Vector3 scale = transform.localScale;
+ scale.x = flipped ? -Mathf.Abs(scale.x) : Mathf.Abs(scale.x);
+ transform.localScale = scale;
+ }
+
+ ///
+ /// Show/hide the fish
+ ///
+ public void SetVisible(bool visible)
+ {
+ gameObject.SetActive(visible);
+ }
+
+ ///
+ /// Apply a highlight effect (for selection)
+ ///
+ public void SetHighlighted(bool highlighted)
+ {
+ float brightness = highlighted ? 1.2f : 1f;
+
+ // Brighten all parts slightly
+ if (bodyRenderer != null)
+ {
+ Color c = currentData.primaryColor * brightness;
+ c.a = 1f;
+ bodyRenderer.color = c;
+ }
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Audio/SoundManager.cs b/Unity/AIForOceans/Assets/Scripts/Audio/SoundManager.cs
new file mode 100644
index 00000000..1ee54196
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Audio/SoundManager.cs
@@ -0,0 +1,219 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Manages all game audio - sound effects and music
+ ///
+ public class SoundManager : MonoBehaviour
+ {
+ public static SoundManager Instance { get; private set; }
+
+ [Header("Audio Sources")]
+ public AudioSource sfxSource;
+ public AudioSource musicSource;
+
+ [Header("Sound Clips")]
+ public AudioClip[] yesClips;
+ public AudioClip[] noClips;
+ public AudioClip[] sortYesClips;
+ public AudioClip[] sortNoClips;
+ public AudioClip buttonClick;
+ public AudioClip scanSound;
+ public AudioClip resultSound;
+
+ [Header("Settings")]
+ [Range(0f, 1f)]
+ public float sfxVolume = 1f;
+ [Range(0f, 1f)]
+ public float musicVolume = 0.5f;
+
+ private Dictionary soundCategories;
+
+ private void Awake()
+ {
+ // Singleton
+ if (Instance != null && Instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+ Instance = this;
+ DontDestroyOnLoad(gameObject);
+
+ // Set up audio sources if not assigned
+ if (sfxSource == null)
+ {
+ sfxSource = gameObject.AddComponent();
+ sfxSource.playOnAwake = false;
+ }
+
+ if (musicSource == null)
+ {
+ musicSource = gameObject.AddComponent();
+ musicSource.playOnAwake = false;
+ musicSource.loop = true;
+ }
+
+ // Organize sounds into categories
+ soundCategories = new Dictionary
+ {
+ { "yes", yesClips },
+ { "no", noClips },
+ { "sortyes", sortYesClips },
+ { "sortno", sortNoClips }
+ };
+ }
+
+ ///
+ /// Play a random sound from a category
+ ///
+ public void PlayRandomFromCategory(string category)
+ {
+ if (soundCategories.TryGetValue(category.ToLower(), out AudioClip[] clips))
+ {
+ if (clips != null && clips.Length > 0)
+ {
+ int index = Random.Range(0, clips.Length);
+ PlaySFX(clips[index]);
+ }
+ }
+ }
+
+ ///
+ /// Play a specific sound effect
+ ///
+ public void PlaySFX(AudioClip clip)
+ {
+ if (clip != null && sfxSource != null)
+ {
+ sfxSource.PlayOneShot(clip, sfxVolume);
+ }
+ }
+
+ ///
+ /// Play button click sound
+ ///
+ public void PlayButtonClick()
+ {
+ PlaySFX(buttonClick);
+ }
+
+ ///
+ /// Play yes training sound
+ ///
+ public void PlayYes()
+ {
+ PlayRandomFromCategory("yes");
+ }
+
+ ///
+ /// Play no training sound
+ ///
+ public void PlayNo()
+ {
+ PlayRandomFromCategory("no");
+ }
+
+ ///
+ /// Play sort yes sound (during prediction)
+ ///
+ public void PlaySortYes()
+ {
+ PlayRandomFromCategory("sortyes");
+ }
+
+ ///
+ /// Play sort no sound (during prediction)
+ ///
+ public void PlaySortNo()
+ {
+ PlayRandomFromCategory("sortno");
+ }
+
+ ///
+ /// Play scan/analyze sound
+ ///
+ public void PlayScan()
+ {
+ PlaySFX(scanSound);
+ }
+
+ ///
+ /// Play result reveal sound
+ ///
+ public void PlayResult()
+ {
+ PlaySFX(resultSound);
+ }
+
+ ///
+ /// Set SFX volume
+ ///
+ public void SetSFXVolume(float volume)
+ {
+ sfxVolume = Mathf.Clamp01(volume);
+ }
+
+ ///
+ /// Set music volume
+ ///
+ public void SetMusicVolume(float volume)
+ {
+ musicVolume = Mathf.Clamp01(volume);
+ if (musicSource != null)
+ {
+ musicSource.volume = musicVolume;
+ }
+ }
+
+ ///
+ /// Play background music
+ ///
+ public void PlayMusic(AudioClip music)
+ {
+ if (musicSource != null && music != null)
+ {
+ musicSource.clip = music;
+ musicSource.volume = musicVolume;
+ musicSource.Play();
+ }
+ }
+
+ ///
+ /// Stop background music
+ ///
+ public void StopMusic()
+ {
+ if (musicSource != null)
+ {
+ musicSource.Stop();
+ }
+ }
+
+ ///
+ /// Fade out music over time
+ ///
+ public void FadeOutMusic(float duration)
+ {
+ StartCoroutine(FadeOutCoroutine(duration));
+ }
+
+ private System.Collections.IEnumerator FadeOutCoroutine(float duration)
+ {
+ float startVolume = musicSource.volume;
+ float elapsed = 0f;
+
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ musicSource.volume = Mathf.Lerp(startVolume, 0f, elapsed / duration);
+ yield return null;
+ }
+
+ musicSource.Stop();
+ musicSource.volume = startVolume;
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Classification/ITrainer.cs b/Unity/AIForOceans/Assets/Scripts/Classification/ITrainer.cs
new file mode 100644
index 00000000..61e37840
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Classification/ITrainer.cs
@@ -0,0 +1,33 @@
+namespace AIForOceans
+{
+ ///
+ /// Interface for all classifiers (naive implementations)
+ ///
+ public interface ITrainer
+ {
+ ///
+ /// Add a training example
+ ///
+ void AddExample(OceanObjectData data, GameConstants.ClassLabel label);
+
+ ///
+ /// Train the model (optional for some classifiers)
+ ///
+ void Train();
+
+ ///
+ /// Make a prediction
+ ///
+ (bool prediction, float confidence) Predict(OceanObjectData data);
+
+ ///
+ /// Get count of examples for a class
+ ///
+ int GetExampleCount(GameConstants.ClassLabel label);
+
+ ///
+ /// Clear all training data
+ ///
+ void ClearAll();
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Classification/NaiveKNNClassifier.cs b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveKNNClassifier.cs
new file mode 100644
index 00000000..2796e6f5
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveKNNClassifier.cs
@@ -0,0 +1,138 @@
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Naive K-Nearest Neighbors classifier using attribute matching
+ /// Replaces TensorFlow.js KNN without actual ML
+ ///
+ public class NaiveKNNClassifier : ITrainer
+ {
+ private List yesExamples = new List();
+ private List noExamples = new List();
+ private int k = 3; // Number of neighbors to consider
+
+ private struct TrainingExample
+ {
+ public OceanObjectData data;
+ public float[] features;
+ }
+
+ public void AddExample(OceanObjectData data, GameConstants.ClassLabel label)
+ {
+ var example = new TrainingExample
+ {
+ data = data,
+ features = data.ToFeatureVector()
+ };
+
+ if (label == GameConstants.ClassLabel.Yes)
+ {
+ yesExamples.Add(example);
+ }
+ else
+ {
+ noExamples.Add(example);
+ }
+ }
+
+ public void Train()
+ {
+ // KNN doesn't need explicit training - it's instance-based
+ }
+
+ public (bool prediction, float confidence) Predict(OceanObjectData data)
+ {
+ if (yesExamples.Count == 0 && noExamples.Count == 0)
+ {
+ return (true, 0.5f); // No training data, random guess
+ }
+
+ float[] queryFeatures = data.ToFeatureVector();
+
+ // Calculate distances to all examples
+ var distances = new List<(float distance, bool isYes)>();
+
+ foreach (var example in yesExamples)
+ {
+ float dist = CalculateDistance(queryFeatures, example.features);
+ distances.Add((dist, true));
+ }
+
+ foreach (var example in noExamples)
+ {
+ float dist = CalculateDistance(queryFeatures, example.features);
+ distances.Add((dist, false));
+ }
+
+ // Sort by distance and take k nearest
+ var nearest = distances
+ .OrderBy(d => d.distance)
+ .Take(Mathf.Min(k, distances.Count))
+ .ToList();
+
+ // Count votes
+ int yesVotes = nearest.Count(n => n.isYes);
+ int noVotes = nearest.Count - yesVotes;
+
+ bool prediction = yesVotes >= noVotes;
+ float confidence = Mathf.Max(yesVotes, noVotes) / (float)nearest.Count;
+
+ // Adjust confidence based on distance
+ if (nearest.Count > 0)
+ {
+ float avgDistance = nearest.Average(n => n.distance);
+ // Lower distance = higher confidence
+ confidence *= Mathf.Clamp01(1f - avgDistance);
+ }
+
+ return (prediction, confidence);
+ }
+
+ ///
+ /// Calculate Euclidean distance between feature vectors
+ ///
+ private float CalculateDistance(float[] a, float[] b)
+ {
+ if (a.Length != b.Length)
+ {
+ // Handle different vector lengths
+ int minLen = Mathf.Min(a.Length, b.Length);
+ float sum = 0;
+ for (int i = 0; i < minLen; i++)
+ {
+ sum += Mathf.Pow(a[i] - b[i], 2);
+ }
+ return Mathf.Sqrt(sum);
+ }
+
+ float distance = 0;
+ for (int i = 0; i < a.Length; i++)
+ {
+ distance += Mathf.Pow(a[i] - b[i], 2);
+ }
+ return Mathf.Sqrt(distance);
+ }
+
+ public int GetExampleCount(GameConstants.ClassLabel label)
+ {
+ return label == GameConstants.ClassLabel.Yes ? yesExamples.Count : noExamples.Count;
+ }
+
+ public void ClearAll()
+ {
+ yesExamples.Clear();
+ noExamples.Clear();
+ }
+
+ ///
+ /// Set the number of neighbors to consider
+ ///
+ public void SetK(int newK)
+ {
+ k = Mathf.Max(1, newK);
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Classification/NaiveSVMClassifier.cs b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveSVMClassifier.cs
new file mode 100644
index 00000000..d4fc8297
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveSVMClassifier.cs
@@ -0,0 +1,247 @@
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Naive SVM-like classifier using weighted attribute matching
+ /// Replaces TensorFlow SVM without actual ML
+ /// Used for word modes (short/long) where we classify by attributes
+ ///
+ public class NaiveSVMClassifier : ITrainer
+ {
+ private List yesExamples = new List();
+ private List noExamples = new List();
+
+ // Learned weights for each feature (after training)
+ private float[] featureWeights;
+ private float bias = 0f;
+ private bool isTrained = false;
+
+ private struct TrainingExample
+ {
+ public OceanObjectData data;
+ public float[] features;
+ }
+
+ public void AddExample(OceanObjectData data, GameConstants.ClassLabel label)
+ {
+ var example = new TrainingExample
+ {
+ data = data,
+ features = data.ToFeatureVector()
+ };
+
+ if (label == GameConstants.ClassLabel.Yes)
+ {
+ yesExamples.Add(example);
+ }
+ else
+ {
+ noExamples.Add(example);
+ }
+
+ isTrained = false; // Need to retrain
+ }
+
+ ///
+ /// Train the classifier by computing feature weights
+ /// This is a simplified version - real SVM uses optimization
+ ///
+ public void Train()
+ {
+ if (yesExamples.Count == 0 || noExamples.Count == 0)
+ {
+ // Can't train without both classes
+ isTrained = false;
+ return;
+ }
+
+ int featureCount = yesExamples[0].features.Length;
+ featureWeights = new float[featureCount];
+
+ // Calculate mean feature values for each class
+ float[] yesMean = CalculateMean(yesExamples);
+ float[] noMean = CalculateMean(noExamples);
+
+ // Weights are the difference between means
+ // Features that differ more between classes get higher weight
+ for (int i = 0; i < featureCount; i++)
+ {
+ featureWeights[i] = yesMean[i] - noMean[i];
+ }
+
+ // Normalize weights
+ float weightSum = featureWeights.Sum(w => Mathf.Abs(w));
+ if (weightSum > 0)
+ {
+ for (int i = 0; i < featureCount; i++)
+ {
+ featureWeights[i] /= weightSum;
+ }
+ }
+
+ // Calculate bias as the midpoint
+ float yesScore = CalculateScore(yesMean);
+ float noScore = CalculateScore(noMean);
+ bias = -(yesScore + noScore) / 2f;
+
+ isTrained = true;
+ }
+
+ public (bool prediction, float confidence) Predict(OceanObjectData data)
+ {
+ if (!isTrained)
+ {
+ // Fall back to simple similarity if not trained
+ return PredictBySimilarity(data);
+ }
+
+ float[] features = data.ToFeatureVector();
+ float score = CalculateScore(features) + bias;
+
+ // Positive score = Yes, Negative score = No
+ bool prediction = score >= 0;
+
+ // Confidence based on distance from decision boundary
+ float confidence = Mathf.Clamp01(Mathf.Abs(score) * 2f);
+
+ return (prediction, confidence);
+ }
+
+ ///
+ /// Calculate weighted score for features
+ ///
+ private float CalculateScore(float[] features)
+ {
+ float score = 0;
+ int len = Mathf.Min(features.Length, featureWeights.Length);
+
+ for (int i = 0; i < len; i++)
+ {
+ score += features[i] * featureWeights[i];
+ }
+
+ return score;
+ }
+
+ ///
+ /// Calculate mean feature vector
+ ///
+ private float[] CalculateMean(List examples)
+ {
+ if (examples.Count == 0)
+ return new float[0];
+
+ int featureCount = examples[0].features.Length;
+ float[] mean = new float[featureCount];
+
+ foreach (var example in examples)
+ {
+ for (int i = 0; i < featureCount; i++)
+ {
+ mean[i] += example.features[i];
+ }
+ }
+
+ for (int i = 0; i < featureCount; i++)
+ {
+ mean[i] /= examples.Count;
+ }
+
+ return mean;
+ }
+
+ ///
+ /// Fallback prediction using simple similarity
+ ///
+ private (bool prediction, float confidence) PredictBySimilarity(OceanObjectData data)
+ {
+ if (yesExamples.Count == 0 && noExamples.Count == 0)
+ {
+ return (true, 0.5f);
+ }
+
+ float[] queryFeatures = data.ToFeatureVector();
+
+ float yesAvgDist = yesExamples.Count > 0
+ ? yesExamples.Average(e => CalculateDistance(queryFeatures, e.features))
+ : float.MaxValue;
+
+ float noAvgDist = noExamples.Count > 0
+ ? noExamples.Average(e => CalculateDistance(queryFeatures, e.features))
+ : float.MaxValue;
+
+ bool prediction = yesAvgDist <= noAvgDist;
+ float totalDist = yesAvgDist + noAvgDist;
+ float confidence = totalDist > 0
+ ? Mathf.Abs(yesAvgDist - noAvgDist) / totalDist
+ : 0.5f;
+
+ return (prediction, confidence);
+ }
+
+ ///
+ /// Calculate Euclidean distance
+ ///
+ private float CalculateDistance(float[] a, float[] b)
+ {
+ int minLen = Mathf.Min(a.Length, b.Length);
+ float sum = 0;
+
+ for (int i = 0; i < minLen; i++)
+ {
+ sum += Mathf.Pow(a[i] - b[i], 2);
+ }
+
+ return Mathf.Sqrt(sum);
+ }
+
+ public int GetExampleCount(GameConstants.ClassLabel label)
+ {
+ return label == GameConstants.ClassLabel.Yes ? yesExamples.Count : noExamples.Count;
+ }
+
+ public void ClearAll()
+ {
+ yesExamples.Clear();
+ noExamples.Clear();
+ featureWeights = null;
+ bias = 0f;
+ isTrained = false;
+ }
+
+ ///
+ /// Get feature weights for visualization (like original SVM)
+ ///
+ public float[] GetFeatureWeights()
+ {
+ return featureWeights ?? new float[0];
+ }
+
+ ///
+ /// Get feature importance for a specific prediction
+ ///
+ public Dictionary GetFeatureImportance()
+ {
+ if (featureWeights == null)
+ return new Dictionary();
+
+ var importance = new Dictionary
+ {
+ { "body", Mathf.Abs(featureWeights[0]) },
+ { "eyes", Mathf.Abs(featureWeights[1]) },
+ { "mouth", Mathf.Abs(featureWeights[2]) },
+ { "dorsalFin", Mathf.Abs(featureWeights[3]) },
+ { "pectoralFin", Mathf.Abs(featureWeights[4]) },
+ { "tailFin", Mathf.Abs(featureWeights[5]) },
+ { "primaryColor", (Mathf.Abs(featureWeights[6]) + Mathf.Abs(featureWeights[7]) + Mathf.Abs(featureWeights[8])) / 3f },
+ { "secondaryColor", (Mathf.Abs(featureWeights[9]) + Mathf.Abs(featureWeights[10]) + Mathf.Abs(featureWeights[11])) / 3f },
+ { "finColor", (Mathf.Abs(featureWeights[12]) + Mathf.Abs(featureWeights[13]) + Mathf.Abs(featureWeights[14])) / 3f }
+ };
+
+ return importance;
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameConstants.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameConstants.cs
new file mode 100644
index 00000000..9a9082e7
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Core/GameConstants.cs
@@ -0,0 +1,109 @@
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Game-wide constants matching the original JavaScript implementation
+ ///
+ public static class GameConstants
+ {
+ // App Modes (Game Levels)
+ public enum AppMode
+ {
+ FishVTrash, // Basic fish vs trash classification
+ CreaturesVTrashDemo, // Demo showing AI bias
+ CreaturesVTrash, // Fish + creatures vs trash
+ Short, // 6 adjectives, SVM
+ Long // 15 adjectives, SVM
+ }
+
+ // Scene/Mode states
+ public enum GameScene
+ {
+ Loading,
+ Words,
+ Training,
+ Predicting,
+ Pond,
+ IntermediateLoading
+ }
+
+ // Object types in the ocean
+ public enum OceanObjectType
+ {
+ Fish,
+ Creature,
+ Trash
+ }
+
+ // Classification labels
+ public enum ClassLabel
+ {
+ Yes = 1,
+ No = 0
+ }
+
+ // Fish component counts (matching original)
+ public const int BODY_COUNT = 19;
+ public const int EYES_COUNT = 18;
+ public const int MOUTH_COUNT = 15;
+ public const int DORSAL_FIN_COUNT = 17;
+ public const int PECTORAL_FIN_COUNT = 17;
+ public const int TAIL_FIN_COUNT = 17;
+
+ // Creature types
+ public static readonly string[] CREATURE_TYPES = new string[]
+ {
+ "Crab", "Dolphin", "Jellyfish", "Octopus", "Otter",
+ "Seahorse", "Snail", "Starfish", "Turtle", "Whale"
+ };
+
+ // Trash types
+ public static readonly string[] TRASH_TYPES = new string[]
+ {
+ "6-pack-rings", "Apple", "Banana", "Battery", "Bottle",
+ "Bulb", "Can", "Coffee", "Fork", "Laundry", "Sock",
+ "Soda", "Tire", "Wing"
+ };
+
+ // Word choices for Short mode (6 adjectives)
+ public static readonly string[] SHORT_WORDS = new string[]
+ {
+ "blue", "orange", "striped", "spotted", "long", "round"
+ };
+
+ // Word choices for Long mode (15 adjectives)
+ public static readonly string[] LONG_WORDS = new string[]
+ {
+ "blue", "orange", "striped", "spotted", "long", "round",
+ "green", "purple", "scary", "friendly", "big", "small",
+ "fast", "slow", "colorful"
+ };
+
+ // Animation constants
+ public const float BOB_AMPLITUDE = 10f;
+ public const float BOB_FREQUENCY = 2f;
+ public const float FISH_SPEED = 100f;
+ public const float SCAN_PAUSE_DURATION = 1.5f;
+
+ // Training counts per mode
+ public const int FISH_V_TRASH_TRAINING_COUNT = 6;
+ public const int CREATURES_V_TRASH_TRAINING_COUNT = 8;
+ public const int SHORT_TRAINING_COUNT = 30;
+ public const int LONG_TRAINING_COUNT = 100;
+
+ // UI constants
+ public const float CANVAS_WIDTH = 400f;
+ public const float CANVAS_HEIGHT = 400f;
+ public const float UI_SCALE_FACTOR = 1f;
+
+ // Prediction confidence thresholds
+ public const float HIGH_CONFIDENCE_THRESHOLD = 0.7f;
+ public const float LOW_CONFIDENCE_THRESHOLD = 0.3f;
+
+ // Colors for prediction frames
+ public static readonly Color GREEN_FRAME = new Color(0.2f, 0.8f, 0.2f); // Correct/Yes
+ public static readonly Color RED_FRAME = new Color(0.8f, 0.2f, 0.2f); // Wrong/No
+ public static readonly Color BLUE_FRAME = new Color(0.2f, 0.4f, 0.8f); // Uncertain
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameInitializer.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameInitializer.cs
new file mode 100644
index 00000000..78a313f3
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Core/GameInitializer.cs
@@ -0,0 +1,52 @@
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Initializes the game on startup
+ ///
+ public class GameInitializer : MonoBehaviour
+ {
+ [Header("Managers")]
+ public GameObject gameManagerPrefab;
+ public GameObject soundManagerPrefab;
+ public GameObject localizationPrefab;
+
+ [Header("Initial Settings")]
+ public GameConstants.AppMode defaultMode = GameConstants.AppMode.FishVTrash;
+ public bool autoStartGame = false;
+
+ private void Awake()
+ {
+ // Ensure managers exist
+ EnsureManagerExists(gameManagerPrefab);
+ EnsureManagerExists(soundManagerPrefab);
+ EnsureManagerExists(localizationPrefab);
+ }
+
+ private void Start()
+ {
+ if (autoStartGame && GameManager.Instance != null)
+ {
+ GameManager.Instance.StartGame(defaultMode);
+ }
+ }
+
+ private void EnsureManagerExists(GameObject prefab) where T : MonoBehaviour
+ {
+ if (FindFirstObjectByType() == null)
+ {
+ if (prefab != null)
+ {
+ Instantiate(prefab);
+ }
+ else
+ {
+ // Create empty manager
+ var obj = new GameObject(typeof(T).Name);
+ obj.AddComponent();
+ }
+ }
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameManager.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameManager.cs
new file mode 100644
index 00000000..8e3929be
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Core/GameManager.cs
@@ -0,0 +1,256 @@
+using System;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Main game manager singleton - handles state and scene transitions
+ ///
+ public class GameManager : MonoBehaviour
+ {
+ public static GameManager Instance { get; private set; }
+
+ // Events for state changes
+ public event Action OnSceneChanged;
+ public event Action OnStateChanged;
+ public event Action OnTrainingExampleAdded;
+ public event Action OnPredictionMade;
+
+ // Current game state
+ public GameState State { get; private set; }
+
+ // Scene controller reference
+ private SceneController sceneController;
+
+ private void Awake()
+ {
+ // Singleton pattern
+ if (Instance != null && Instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+ Instance = this;
+ DontDestroyOnLoad(gameObject);
+
+ // Initialize state
+ State = new GameState();
+ }
+
+ private void Start()
+ {
+ sceneController = GetComponent();
+ if (sceneController == null)
+ {
+ sceneController = gameObject.AddComponent();
+ }
+ }
+
+ ///
+ /// Start a new game with the specified mode
+ ///
+ public void StartGame(GameConstants.AppMode mode)
+ {
+ State.Reset();
+ State.appMode = mode;
+ State.totalTrainingCount = State.GetTrainingCountForMode();
+
+ // Initialize appropriate trainer
+ InitializeTrainer();
+
+ // Generate initial fish data
+ GenerateFishData();
+
+ // Start with loading or words scene
+ if (mode == GameConstants.AppMode.Short || mode == GameConstants.AppMode.Long)
+ {
+ ChangeScene(GameConstants.GameScene.Words);
+ }
+ else if (mode == GameConstants.AppMode.CreaturesVTrashDemo)
+ {
+ // Demo skips training
+ ChangeScene(GameConstants.GameScene.Predicting);
+ }
+ else
+ {
+ ChangeScene(GameConstants.GameScene.Training);
+ }
+
+ OnStateChanged?.Invoke(State);
+ }
+
+ ///
+ /// Initialize the appropriate trainer for current mode
+ ///
+ private void InitializeTrainer()
+ {
+ if (State.appMode == GameConstants.AppMode.Short ||
+ State.appMode == GameConstants.AppMode.Long)
+ {
+ State.trainer = new NaiveSVMClassifier();
+ }
+ else
+ {
+ State.trainer = new NaiveKNNClassifier();
+ }
+ }
+
+ ///
+ /// Generate fish/objects for the current mode
+ ///
+ private void GenerateFishData()
+ {
+ State.fishData.Clear();
+
+ int count = State.totalTrainingCount + 20; // Training + prediction items
+
+ bool includeFish = true;
+ bool includeCreatures = State.appMode == GameConstants.AppMode.CreaturesVTrash ||
+ State.appMode == GameConstants.AppMode.CreaturesVTrashDemo;
+ bool includeTrash = State.appMode != GameConstants.AppMode.Short &&
+ State.appMode != GameConstants.AppMode.Long;
+
+ var generator = new OceanGenerator();
+ State.fishData = generator.GenerateOcean(count, includeFish, includeCreatures, includeTrash);
+ }
+
+ ///
+ /// Change to a new scene
+ ///
+ public void ChangeScene(GameConstants.GameScene newScene)
+ {
+ State.currentScene = newScene;
+ OnSceneChanged?.Invoke(newScene);
+ OnStateChanged?.Invoke(State);
+ }
+
+ ///
+ /// Add a training example
+ ///
+ public void AddTrainingExample(OceanObjectData data, bool isYes)
+ {
+ State.trainer.AddExample(data, isYes ? GameConstants.ClassLabel.Yes : GameConstants.ClassLabel.No);
+ State.trainingIndex++;
+
+ OnTrainingExampleAdded?.Invoke(data, isYes);
+ OnStateChanged?.Invoke(State);
+
+ // Check if training is complete
+ if (State.trainingIndex >= State.totalTrainingCount)
+ {
+ OnTrainingComplete();
+ }
+ }
+
+ ///
+ /// Called when training is complete
+ ///
+ private void OnTrainingComplete()
+ {
+ // Train the model (for SVM)
+ State.trainer.Train();
+
+ // Move to prediction phase
+ if (State.appMode == GameConstants.AppMode.Short ||
+ State.appMode == GameConstants.AppMode.Long)
+ {
+ ChangeScene(GameConstants.GameScene.IntermediateLoading);
+ }
+ else
+ {
+ ChangeScene(GameConstants.GameScene.Predicting);
+ }
+ }
+
+ ///
+ /// Make a prediction on an object
+ ///
+ public PredictionResult MakePrediction(OceanObjectData data)
+ {
+ var (prediction, confidence) = State.trainer.Predict(data);
+ var result = new PredictionResult(data, prediction, confidence);
+
+ // Determine if correct based on mode
+ result.isCorrect = DetermineCorrectness(data, prediction);
+
+ State.pondResults.Add(result);
+ OnPredictionMade?.Invoke(result);
+
+ return result;
+ }
+
+ ///
+ /// Determine if a prediction is correct
+ ///
+ private bool DetermineCorrectness(OceanObjectData data, bool prediction)
+ {
+ switch (State.appMode)
+ {
+ case GameConstants.AppMode.FishVTrash:
+ // Yes = Fish, No = Trash
+ bool isFishOrCreature = data.objectType != GameConstants.OceanObjectType.Trash;
+ return prediction == isFishOrCreature;
+
+ case GameConstants.AppMode.CreaturesVTrash:
+ case GameConstants.AppMode.CreaturesVTrashDemo:
+ // Yes = Fish/Creature, No = Trash
+ bool isNotTrash = data.objectType != GameConstants.OceanObjectType.Trash;
+ return prediction == isNotTrash;
+
+ case GameConstants.AppMode.Short:
+ case GameConstants.AppMode.Long:
+ // Subjective - based on selected word
+ // This would need attribute checking
+ return true; // Simplified for now
+
+ default:
+ return true;
+ }
+ }
+
+ ///
+ /// Select a word for Short/Long modes
+ ///
+ public void SelectWord(string word)
+ {
+ State.selectedWord = word;
+ ChangeScene(GameConstants.GameScene.Training);
+ OnStateChanged?.Invoke(State);
+ }
+
+ ///
+ /// Move to pond/results scene
+ ///
+ public void ShowResults()
+ {
+ ChangeScene(GameConstants.GameScene.Pond);
+ }
+
+ ///
+ /// Select a fish in the pond to view details
+ ///
+ public void SelectPondFish(OceanObjectData fish)
+ {
+ State.selectedPondFish = fish;
+ OnStateChanged?.Invoke(State);
+ }
+
+ ///
+ /// Toggle guide visibility
+ ///
+ public void ToggleGuide(bool show)
+ {
+ State.guideShowing = show;
+ OnStateChanged?.Invoke(State);
+ }
+
+ ///
+ /// Advance to next guide
+ ///
+ public void NextGuide()
+ {
+ State.guideIndex++;
+ OnStateChanged?.Invoke(State);
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameState.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameState.cs
new file mode 100644
index 00000000..8e2e1755
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Core/GameState.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Central game state - mirrors the original state.js
+ ///
+ [Serializable]
+ public class GameState
+ {
+ // Current mode and scene
+ public GameConstants.AppMode appMode = GameConstants.AppMode.FishVTrash;
+ public GameConstants.GameScene currentScene = GameConstants.GameScene.Loading;
+
+ // Training state
+ public int trainingIndex = 0;
+ public int totalTrainingCount = 0;
+ public string selectedWord = "";
+
+ // Fish/object data
+ public List fishData = new List();
+ public List pondResults = new List();
+
+ // Animation state
+ public bool isRunning = false;
+ public bool isPaused = false;
+ public float currentTime = 0f;
+
+ // UI state
+ public bool guideShowing = false;
+ public int guideIndex = 0;
+ public OceanObjectData selectedPondFish = null;
+
+ // Trainer reference (set at runtime)
+ [NonSerialized]
+ public ITrainer trainer;
+
+ ///
+ /// Reset state for a new game
+ ///
+ public void Reset()
+ {
+ trainingIndex = 0;
+ fishData.Clear();
+ pondResults.Clear();
+ isRunning = false;
+ isPaused = false;
+ currentTime = 0f;
+ guideShowing = false;
+ guideIndex = 0;
+ selectedPondFish = null;
+ trainer?.ClearAll();
+ }
+
+ ///
+ /// Get training count for current mode
+ ///
+ public int GetTrainingCountForMode()
+ {
+ return appMode switch
+ {
+ GameConstants.AppMode.FishVTrash => GameConstants.FISH_V_TRASH_TRAINING_COUNT,
+ GameConstants.AppMode.CreaturesVTrashDemo => 0, // No training in demo
+ GameConstants.AppMode.CreaturesVTrash => GameConstants.CREATURES_V_TRASH_TRAINING_COUNT,
+ GameConstants.AppMode.Short => GameConstants.SHORT_TRAINING_COUNT,
+ GameConstants.AppMode.Long => GameConstants.LONG_TRAINING_COUNT,
+ _ => 6
+ };
+ }
+ }
+
+ ///
+ /// Result of a prediction
+ ///
+ [Serializable]
+ public class PredictionResult
+ {
+ public OceanObjectData objectData;
+ public bool prediction;
+ public float confidence;
+ public bool isCorrect;
+
+ public PredictionResult(OceanObjectData data, bool pred, float conf)
+ {
+ objectData = data;
+ prediction = pred;
+ confidence = conf;
+ isCorrect = false; // Set based on context
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Core/SceneController.cs b/Unity/AIForOceans/Assets/Scripts/Core/SceneController.cs
new file mode 100644
index 00000000..d885dde6
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Core/SceneController.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Handles scene/mode transitions and UI management
+ ///
+ public class SceneController : MonoBehaviour
+ {
+ // References to scene UI containers (set in inspector or found at runtime)
+ public GameObject loadingUI;
+ public GameObject wordsUI;
+ public GameObject trainingUI;
+ public GameObject predictingUI;
+ public GameObject pondUI;
+ public GameObject intermediateLoadingUI;
+
+ // Current active scene
+ private GameConstants.GameScene currentScene;
+
+ private void Start()
+ {
+ // Subscribe to scene changes
+ if (GameManager.Instance != null)
+ {
+ GameManager.Instance.OnSceneChanged += HandleSceneChange;
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (GameManager.Instance != null)
+ {
+ GameManager.Instance.OnSceneChanged -= HandleSceneChange;
+ }
+ }
+
+ ///
+ /// Handle scene change events
+ ///
+ private void HandleSceneChange(GameConstants.GameScene newScene)
+ {
+ StartCoroutine(TransitionToScene(newScene));
+ }
+
+ ///
+ /// Transition to a new scene with optional fade
+ ///
+ private IEnumerator TransitionToScene(GameConstants.GameScene newScene)
+ {
+ // Hide current scene
+ HideAllScenes();
+
+ // Small delay for transition effect
+ yield return new WaitForSeconds(0.1f);
+
+ // Show new scene
+ currentScene = newScene;
+ ShowScene(newScene);
+
+ // Initialize scene-specific logic
+ InitializeScene(newScene);
+ }
+
+ ///
+ /// Hide all scene UIs
+ ///
+ private void HideAllScenes()
+ {
+ SetActiveIfNotNull(loadingUI, false);
+ SetActiveIfNotNull(wordsUI, false);
+ SetActiveIfNotNull(trainingUI, false);
+ SetActiveIfNotNull(predictingUI, false);
+ SetActiveIfNotNull(pondUI, false);
+ SetActiveIfNotNull(intermediateLoadingUI, false);
+ }
+
+ ///
+ /// Show specific scene UI
+ ///
+ private void ShowScene(GameConstants.GameScene scene)
+ {
+ switch (scene)
+ {
+ case GameConstants.GameScene.Loading:
+ SetActiveIfNotNull(loadingUI, true);
+ break;
+ case GameConstants.GameScene.Words:
+ SetActiveIfNotNull(wordsUI, true);
+ break;
+ case GameConstants.GameScene.Training:
+ SetActiveIfNotNull(trainingUI, true);
+ break;
+ case GameConstants.GameScene.Predicting:
+ SetActiveIfNotNull(predictingUI, true);
+ break;
+ case GameConstants.GameScene.Pond:
+ SetActiveIfNotNull(pondUI, true);
+ break;
+ case GameConstants.GameScene.IntermediateLoading:
+ SetActiveIfNotNull(intermediateLoadingUI, true);
+ break;
+ }
+ }
+
+ ///
+ /// Initialize scene-specific behavior
+ ///
+ private void InitializeScene(GameConstants.GameScene scene)
+ {
+ switch (scene)
+ {
+ case GameConstants.GameScene.Loading:
+ StartCoroutine(HandleLoading());
+ break;
+ case GameConstants.GameScene.IntermediateLoading:
+ StartCoroutine(HandleIntermediateLoading());
+ break;
+ case GameConstants.GameScene.Predicting:
+ // Start prediction animation
+ break;
+ }
+ }
+
+ ///
+ /// Handle initial loading
+ ///
+ private IEnumerator HandleLoading()
+ {
+ // Simulate asset loading
+ yield return new WaitForSeconds(1f);
+
+ // Move to next appropriate scene
+ var state = GameManager.Instance.State;
+ if (state.appMode == GameConstants.AppMode.Short ||
+ state.appMode == GameConstants.AppMode.Long)
+ {
+ GameManager.Instance.ChangeScene(GameConstants.GameScene.Words);
+ }
+ else
+ {
+ GameManager.Instance.ChangeScene(GameConstants.GameScene.Training);
+ }
+ }
+
+ ///
+ /// Handle intermediate loading (SVM training)
+ ///
+ private IEnumerator HandleIntermediateLoading()
+ {
+ // Show loading while "training" happens
+ yield return new WaitForSeconds(1.5f);
+
+ // Move to prediction
+ GameManager.Instance.ChangeScene(GameConstants.GameScene.Predicting);
+ }
+
+ private void SetActiveIfNotNull(GameObject obj, bool active)
+ {
+ if (obj != null)
+ {
+ obj.SetActive(active);
+ }
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Data/CreatureRenderer.cs b/Unity/AIForOceans/Assets/Scripts/Data/CreatureRenderer.cs
new file mode 100644
index 00000000..5bf8237e
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Data/CreatureRenderer.cs
@@ -0,0 +1,80 @@
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Renders sea creatures and trash items (single sprite)
+ ///
+ public class CreatureRenderer : MonoBehaviour
+ {
+ [Header("Sprite Renderer")]
+ public SpriteRenderer spriteRenderer;
+
+ [Header("Creature Sprites")]
+ public Sprite[] creatureSprites; // Index matches CREATURE_TYPES
+ public Sprite[] trashSprites; // Index matches TRASH_TYPES
+
+ private OceanObjectData currentData;
+
+ ///
+ /// Set up the visual from data
+ ///
+ public void Setup(OceanObjectData data)
+ {
+ currentData = data;
+
+ if (spriteRenderer == null)
+ {
+ spriteRenderer = GetComponent();
+ }
+
+ if (spriteRenderer == null) return;
+
+ if (data.objectType == GameConstants.OceanObjectType.Creature)
+ {
+ SetCreatureSprite(data.subType);
+ }
+ else if (data.objectType == GameConstants.OceanObjectType.Trash)
+ {
+ SetTrashSprite(data.subType);
+ }
+ }
+
+ private void SetCreatureSprite(string creatureType)
+ {
+ int index = System.Array.IndexOf(GameConstants.CREATURE_TYPES, creatureType);
+ if (index >= 0 && creatureSprites != null && index < creatureSprites.Length)
+ {
+ spriteRenderer.sprite = creatureSprites[index];
+ }
+ }
+
+ private void SetTrashSprite(string trashType)
+ {
+ int index = System.Array.IndexOf(GameConstants.TRASH_TYPES, trashType);
+ if (index >= 0 && trashSprites != null && index < trashSprites.Length)
+ {
+ spriteRenderer.sprite = trashSprites[index];
+ }
+ }
+
+ ///
+ /// Set visibility
+ ///
+ public void SetVisible(bool visible)
+ {
+ gameObject.SetActive(visible);
+ }
+
+ ///
+ /// Set sorting order
+ ///
+ public void SetSortingOrder(int order)
+ {
+ if (spriteRenderer != null)
+ {
+ spriteRenderer.sortingOrder = order;
+ }
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Data/OceanGenerator.cs b/Unity/AIForOceans/Assets/Scripts/Data/OceanGenerator.cs
new file mode 100644
index 00000000..b34b96cf
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Data/OceanGenerator.cs
@@ -0,0 +1,143 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Generates random ocean objects for the game
+ ///
+ public class OceanGenerator
+ {
+ ///
+ /// Generate a collection of ocean objects
+ ///
+ public List GenerateOcean(
+ int count,
+ bool includeFish = true,
+ bool includeCreatures = false,
+ bool includeTrash = true)
+ {
+ var objects = new List();
+
+ for (int i = 0; i < count; i++)
+ {
+ OceanObjectData obj = GenerateRandomObject(i, includeFish, includeCreatures, includeTrash);
+ objects.Add(obj);
+ }
+
+ // Shuffle the list
+ ShuffleList(objects);
+
+ return objects;
+ }
+
+ ///
+ /// Generate a single random object based on allowed types
+ ///
+ private OceanObjectData GenerateRandomObject(
+ int id,
+ bool includeFish,
+ bool includeCreatures,
+ bool includeTrash)
+ {
+ // Build list of possible types
+ var possibleTypes = new List();
+
+ if (includeFish)
+ possibleTypes.Add(GameConstants.OceanObjectType.Fish);
+ if (includeCreatures)
+ possibleTypes.Add(GameConstants.OceanObjectType.Creature);
+ if (includeTrash)
+ possibleTypes.Add(GameConstants.OceanObjectType.Trash);
+
+ if (possibleTypes.Count == 0)
+ {
+ // Default to fish if nothing selected
+ return OceanObjectData.CreateRandomFish(id);
+ }
+
+ // Weight fish higher for better game balance
+ if (includeFish && (includeCreatures || includeTrash))
+ {
+ // 60% fish, rest split between others
+ float roll = Random.value;
+ if (roll < 0.6f)
+ {
+ return OceanObjectData.CreateRandomFish(id);
+ }
+ else if (includeCreatures && includeTrash)
+ {
+ if (roll < 0.8f)
+ return OceanObjectData.CreateCreature(id);
+ else
+ return OceanObjectData.CreateTrash(id);
+ }
+ else if (includeCreatures)
+ {
+ return OceanObjectData.CreateCreature(id);
+ }
+ else
+ {
+ return OceanObjectData.CreateTrash(id);
+ }
+ }
+
+ // Equal distribution
+ var selectedType = possibleTypes[Random.Range(0, possibleTypes.Count)];
+
+ return selectedType switch
+ {
+ GameConstants.OceanObjectType.Fish => OceanObjectData.CreateRandomFish(id),
+ GameConstants.OceanObjectType.Creature => OceanObjectData.CreateCreature(id),
+ GameConstants.OceanObjectType.Trash => OceanObjectData.CreateTrash(id),
+ _ => OceanObjectData.CreateRandomFish(id)
+ };
+ }
+
+ ///
+ /// Generate objects specifically for training
+ ///
+ public List GenerateTrainingSet(
+ int count,
+ GameConstants.AppMode mode)
+ {
+ bool includeFish = true;
+ bool includeCreatures = mode == GameConstants.AppMode.CreaturesVTrash ||
+ mode == GameConstants.AppMode.CreaturesVTrashDemo;
+ bool includeTrash = mode != GameConstants.AppMode.Short &&
+ mode != GameConstants.AppMode.Long;
+
+ // For word modes, only fish
+ if (mode == GameConstants.AppMode.Short || mode == GameConstants.AppMode.Long)
+ {
+ includeCreatures = false;
+ includeTrash = false;
+ }
+
+ return GenerateOcean(count, includeFish, includeCreatures, includeTrash);
+ }
+
+ ///
+ /// Generate objects for prediction phase
+ ///
+ public List GeneratePredictionSet(
+ int count,
+ GameConstants.AppMode mode)
+ {
+ // Similar to training but may have different distribution
+ return GenerateTrainingSet(count, mode);
+ }
+
+ ///
+ /// Fisher-Yates shuffle
+ ///
+ private void ShuffleList(List list)
+ {
+ for (int i = list.Count - 1; i > 0; i--)
+ {
+ int j = Random.Range(0, i + 1);
+ (list[i], list[j]) = (list[j], list[i]);
+ }
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Data/OceanObjectData.cs b/Unity/AIForOceans/Assets/Scripts/Data/OceanObjectData.cs
new file mode 100644
index 00000000..1506b767
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Data/OceanObjectData.cs
@@ -0,0 +1,219 @@
+using System;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Data structure for any ocean object (fish, creature, trash)
+ ///
+ [Serializable]
+ public class OceanObjectData
+ {
+ // Basic info
+ public int id;
+ public GameConstants.OceanObjectType objectType;
+ public string name;
+
+ // Fish-specific components (indices into sprite arrays)
+ public int bodyIndex;
+ public int eyesIndex;
+ public int mouthIndex;
+ public int dorsalFinIndex;
+ public int pectoralFinIndex;
+ public int tailFinIndex;
+ public int scalesIndex;
+
+ // Colors
+ public Color primaryColor;
+ public Color secondaryColor;
+ public Color finColor;
+
+ // For creatures/trash - the specific type name
+ public string subType;
+
+ // Position and animation
+ public Vector2 position;
+ public float bobPhase;
+ public float speed;
+
+ // Classification label (set during training)
+ public GameConstants.ClassLabel? label;
+
+ ///
+ /// Create a random fish
+ ///
+ public static OceanObjectData CreateRandomFish(int id)
+ {
+ var fish = new OceanObjectData
+ {
+ id = id,
+ objectType = GameConstants.OceanObjectType.Fish,
+ name = $"Fish_{id}",
+
+ // Random component indices
+ bodyIndex = UnityEngine.Random.Range(0, GameConstants.BODY_COUNT),
+ eyesIndex = UnityEngine.Random.Range(0, GameConstants.EYES_COUNT),
+ mouthIndex = UnityEngine.Random.Range(0, GameConstants.MOUTH_COUNT),
+ dorsalFinIndex = UnityEngine.Random.Range(0, GameConstants.DORSAL_FIN_COUNT),
+ pectoralFinIndex = UnityEngine.Random.Range(0, GameConstants.PECTORAL_FIN_COUNT),
+ tailFinIndex = UnityEngine.Random.Range(0, GameConstants.TAIL_FIN_COUNT),
+ scalesIndex = UnityEngine.Random.Range(0, 3), // Fewer scale options
+
+ // Random colors
+ primaryColor = GetRandomFishColor(),
+ secondaryColor = GetRandomFishColor(),
+ finColor = GetRandomFishColor(),
+
+ // Animation
+ bobPhase = UnityEngine.Random.Range(0f, Mathf.PI * 2f),
+ speed = GameConstants.FISH_SPEED * UnityEngine.Random.Range(0.8f, 1.2f)
+ };
+
+ return fish;
+ }
+
+ ///
+ /// Create a creature
+ ///
+ public static OceanObjectData CreateCreature(int id, string creatureType = null)
+ {
+ if (string.IsNullOrEmpty(creatureType))
+ {
+ int index = UnityEngine.Random.Range(0, GameConstants.CREATURE_TYPES.Length);
+ creatureType = GameConstants.CREATURE_TYPES[index];
+ }
+
+ return new OceanObjectData
+ {
+ id = id,
+ objectType = GameConstants.OceanObjectType.Creature,
+ name = creatureType,
+ subType = creatureType,
+ bobPhase = UnityEngine.Random.Range(0f, Mathf.PI * 2f),
+ speed = GameConstants.FISH_SPEED * UnityEngine.Random.Range(0.6f, 1.0f)
+ };
+ }
+
+ ///
+ /// Create trash
+ ///
+ public static OceanObjectData CreateTrash(int id, string trashType = null)
+ {
+ if (string.IsNullOrEmpty(trashType))
+ {
+ int index = UnityEngine.Random.Range(0, GameConstants.TRASH_TYPES.Length);
+ trashType = GameConstants.TRASH_TYPES[index];
+ }
+
+ return new OceanObjectData
+ {
+ id = id,
+ objectType = GameConstants.OceanObjectType.Trash,
+ name = trashType,
+ subType = trashType,
+ bobPhase = UnityEngine.Random.Range(0f, Mathf.PI * 2f),
+ speed = GameConstants.FISH_SPEED * UnityEngine.Random.Range(0.4f, 0.8f)
+ };
+ }
+
+ ///
+ /// Get a random fish-appropriate color
+ ///
+ private static Color GetRandomFishColor()
+ {
+ // Predefined fish color palettes
+ Color[] fishColors = new Color[]
+ {
+ new Color(0.2f, 0.6f, 1.0f), // Blue
+ new Color(1.0f, 0.6f, 0.2f), // Orange
+ new Color(0.2f, 0.8f, 0.4f), // Green
+ new Color(0.8f, 0.2f, 0.8f), // Purple
+ new Color(1.0f, 1.0f, 0.2f), // Yellow
+ new Color(1.0f, 0.4f, 0.4f), // Red/Pink
+ new Color(0.4f, 0.8f, 0.8f), // Cyan
+ new Color(0.6f, 0.4f, 0.2f), // Brown
+ };
+
+ return fishColors[UnityEngine.Random.Range(0, fishColors.Length)];
+ }
+
+ ///
+ /// Convert to feature vector for classification
+ ///
+ public float[] ToFeatureVector()
+ {
+ if (objectType == GameConstants.OceanObjectType.Fish)
+ {
+ // Feature vector includes all fish attributes
+ return new float[]
+ {
+ bodyIndex / (float)GameConstants.BODY_COUNT,
+ eyesIndex / (float)GameConstants.EYES_COUNT,
+ mouthIndex / (float)GameConstants.MOUTH_COUNT,
+ dorsalFinIndex / (float)GameConstants.DORSAL_FIN_COUNT,
+ pectoralFinIndex / (float)GameConstants.PECTORAL_FIN_COUNT,
+ tailFinIndex / (float)GameConstants.TAIL_FIN_COUNT,
+ primaryColor.r,
+ primaryColor.g,
+ primaryColor.b,
+ secondaryColor.r,
+ secondaryColor.g,
+ secondaryColor.b,
+ finColor.r,
+ finColor.g,
+ finColor.b
+ };
+ }
+ else
+ {
+ // For creatures/trash, use a simpler encoding
+ float typeValue = objectType == GameConstants.OceanObjectType.Creature ? 0.5f : 1.0f;
+ return new float[] { typeValue, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+ }
+ }
+
+ ///
+ /// Check if fish has a specific attribute (for word modes)
+ ///
+ public bool HasAttribute(string attribute)
+ {
+ if (objectType != GameConstants.OceanObjectType.Fish)
+ return false;
+
+ attribute = attribute.ToLower();
+
+ return attribute switch
+ {
+ "blue" => IsColorDominant(Color.blue),
+ "orange" => IsColorDominant(new Color(1f, 0.5f, 0f)),
+ "green" => IsColorDominant(Color.green),
+ "purple" => IsColorDominant(new Color(0.5f, 0f, 0.5f)),
+ "yellow" => IsColorDominant(Color.yellow),
+ "red" => IsColorDominant(Color.red),
+ "striped" => scalesIndex == 1,
+ "spotted" => scalesIndex == 2,
+ "long" => bodyIndex >= GameConstants.BODY_COUNT / 2,
+ "round" => bodyIndex < GameConstants.BODY_COUNT / 2,
+ "big" => bodyIndex >= GameConstants.BODY_COUNT * 2 / 3,
+ "small" => bodyIndex < GameConstants.BODY_COUNT / 3,
+ _ => false
+ };
+ }
+
+ private bool IsColorDominant(Color targetColor)
+ {
+ float threshold = 0.4f;
+ return ColorDistance(primaryColor, targetColor) < threshold ||
+ ColorDistance(secondaryColor, targetColor) < threshold;
+ }
+
+ private float ColorDistance(Color a, Color b)
+ {
+ return Mathf.Sqrt(
+ Mathf.Pow(a.r - b.r, 2) +
+ Mathf.Pow(a.g - b.g, 2) +
+ Mathf.Pow(a.b - b.b, 2)
+ );
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Scenes/PredictionSceneController.cs b/Unity/AIForOceans/Assets/Scripts/Scenes/PredictionSceneController.cs
new file mode 100644
index 00000000..bda47818
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Scenes/PredictionSceneController.cs
@@ -0,0 +1,199 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Controls the prediction scene flow
+ ///
+ public class PredictionSceneController : MonoBehaviour
+ {
+ [Header("References")]
+ public FishRenderer fishRenderer;
+ public FishAnimator fishAnimator;
+ public AIBot aiBot;
+ public PredictionUI predictionUI;
+
+ [Header("Animation Settings")]
+ public Vector2 spawnPosition = new Vector2(-200f, 0f);
+ public Vector2 scanPosition = new Vector2(0f, 0f);
+ public Vector2 exitPosition = new Vector2(200f, 0f);
+ public float swimSpeed = 100f;
+ public float scanDuration = 1.5f;
+ public float delayBetweenFish = 0.5f;
+
+ [Header("Prediction Settings")]
+ public int fishToPredictCount = 20;
+
+ private Queue fishQueue;
+ private bool isRunning = false;
+ private int predictedCount = 0;
+
+ private void OnEnable()
+ {
+ StartPredictions();
+ }
+
+ private void OnDisable()
+ {
+ StopPredictions();
+ }
+
+ ///
+ /// Start the prediction sequence
+ ///
+ public void StartPredictions()
+ {
+ if (isRunning) return;
+
+ var state = GameManager.Instance.State;
+
+ // Get fish for prediction (skip training fish)
+ fishQueue = new Queue();
+ int startIndex = state.totalTrainingCount;
+
+ for (int i = startIndex; i < state.fishData.Count && i < startIndex + fishToPredictCount; i++)
+ {
+ fishQueue.Enqueue(state.fishData[i]);
+ }
+
+ predictedCount = 0;
+ isRunning = true;
+
+ StartCoroutine(RunPredictionLoop());
+ }
+
+ ///
+ /// Stop predictions
+ ///
+ public void StopPredictions()
+ {
+ isRunning = false;
+ StopAllCoroutines();
+ }
+
+ ///
+ /// Main prediction loop
+ ///
+ private IEnumerator RunPredictionLoop()
+ {
+ while (isRunning && fishQueue.Count > 0)
+ {
+ OceanObjectData fish = fishQueue.Dequeue();
+ yield return StartCoroutine(ProcessFish(fish));
+ yield return new WaitForSeconds(delayBetweenFish);
+ }
+
+ // All predictions complete
+ isRunning = false;
+ OnAllPredictionsComplete();
+ }
+
+ ///
+ /// Process a single fish through prediction
+ ///
+ private IEnumerator ProcessFish(OceanObjectData fish)
+ {
+ // Set up fish visual
+ if (fishRenderer != null && fish.objectType == GameConstants.OceanObjectType.Fish)
+ {
+ fishRenderer.SetupFish(fish);
+ }
+
+ // Swim in from left
+ yield return StartCoroutine(SwimTo(spawnPosition, scanPosition, 1f));
+
+ // AI scans the fish
+ if (aiBot != null)
+ {
+ aiBot.SetExpression(AIBot.Expression.Scanning);
+ }
+
+ if (predictionUI != null)
+ {
+ predictionUI.ShowScanning();
+ }
+
+ // Play scan sound
+ if (SoundManager.Instance != null)
+ {
+ SoundManager.Instance.PlayScan();
+ }
+
+ // Wait for scan
+ yield return new WaitForSeconds(scanDuration);
+
+ // Make prediction
+ PredictionResult result = GameManager.Instance.MakePrediction(fish);
+ predictedCount++;
+
+ // AI reacts
+ if (aiBot != null)
+ {
+ aiBot.SetExpression(result.prediction ? AIBot.Expression.Yes : AIBot.Expression.No);
+ }
+
+ // Brief pause to show result
+ yield return new WaitForSeconds(0.5f);
+
+ // Swim out
+ yield return StartCoroutine(SwimTo(scanPosition, exitPosition, 0.5f));
+ }
+
+ ///
+ /// Animate swimming from one position to another
+ ///
+ private IEnumerator SwimTo(Vector2 from, Vector2 to, float duration)
+ {
+ if (fishRenderer == null) yield break;
+
+ Transform fishTransform = fishRenderer.transform;
+ fishTransform.position = from;
+
+ float elapsed = 0f;
+
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ float t = FishAnimator.SCurve(elapsed / duration);
+ fishTransform.position = Vector2.Lerp(from, to, t);
+ yield return null;
+ }
+
+ fishTransform.position = to;
+ }
+
+ ///
+ /// Called when all predictions are complete
+ ///
+ private void OnAllPredictionsComplete()
+ {
+ // Hide prediction UI
+ if (predictionUI != null)
+ {
+ predictionUI.Hide();
+ }
+
+ // Move to pond scene
+ GameManager.Instance.ShowResults();
+ }
+
+ ///
+ /// Skip to results immediately
+ ///
+ public void SkipToResults()
+ {
+ StopPredictions();
+
+ // Make remaining predictions instantly
+ while (fishQueue != null && fishQueue.Count > 0)
+ {
+ OceanObjectData fish = fishQueue.Dequeue();
+ GameManager.Instance.MakePrediction(fish);
+ }
+
+ OnAllPredictionsComplete();
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/Scenes/TrainingSceneController.cs b/Unity/AIForOceans/Assets/Scripts/Scenes/TrainingSceneController.cs
new file mode 100644
index 00000000..547e2f41
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/Scenes/TrainingSceneController.cs
@@ -0,0 +1,186 @@
+using System.Collections;
+using UnityEngine;
+
+namespace AIForOceans
+{
+ ///
+ /// Controls the training scene flow
+ ///
+ public class TrainingSceneController : MonoBehaviour
+ {
+ [Header("References")]
+ public FishRenderer currentFishRenderer;
+ public FishAnimator currentFishAnimator;
+ public AIBot aiBot;
+ public TrainingUI trainingUI;
+
+ [Header("Spawn Settings")]
+ public Vector2 spawnPosition = new Vector2(-200f, 0f);
+ public Vector2 centerPosition = new Vector2(0f, 0f);
+ public float fishEnterDuration = 1f;
+
+ private OceanObjectData currentFish;
+ private bool isAnimating = false;
+
+ private void OnEnable()
+ {
+ // Subscribe to events
+ if (GameManager.Instance != null)
+ {
+ GameManager.Instance.OnTrainingExampleAdded += OnExampleAdded;
+ }
+
+ // Show first fish
+ ShowNextFish();
+ }
+
+ private void OnDisable()
+ {
+ if (GameManager.Instance != null)
+ {
+ GameManager.Instance.OnTrainingExampleAdded -= OnExampleAdded;
+ }
+ }
+
+ ///
+ /// Show the next fish in the training sequence
+ ///
+ private void ShowNextFish()
+ {
+ var state = GameManager.Instance.State;
+
+ if (state.trainingIndex >= state.fishData.Count)
+ {
+ Debug.LogWarning("No more fish to show");
+ return;
+ }
+
+ currentFish = state.fishData[state.trainingIndex];
+
+ // Set up the fish renderer
+ if (currentFishRenderer != null)
+ {
+ if (currentFish.objectType == GameConstants.OceanObjectType.Fish)
+ {
+ currentFishRenderer.SetupFish(currentFish);
+ }
+ // For creatures/trash, would need different rendering
+ }
+
+ // Animate fish entering
+ StartCoroutine(AnimateFishEnter());
+ }
+
+ ///
+ /// Animate fish swimming in from left
+ ///
+ private IEnumerator AnimateFishEnter()
+ {
+ isAnimating = true;
+
+ if (currentFishRenderer == null)
+ {
+ isAnimating = false;
+ yield break;
+ }
+
+ Transform fishTransform = currentFishRenderer.transform;
+ fishTransform.position = spawnPosition;
+
+ float elapsed = 0f;
+
+ while (elapsed < fishEnterDuration)
+ {
+ elapsed += Time.deltaTime;
+ float t = elapsed / fishEnterDuration;
+
+ // Ease in-out
+ float easedT = FishAnimator.SCurve(t);
+
+ fishTransform.position = Vector2.Lerp(spawnPosition, centerPosition, easedT);
+ yield return null;
+ }
+
+ fishTransform.position = centerPosition;
+ isAnimating = false;
+
+ // AI bot looks at fish
+ if (aiBot != null)
+ {
+ aiBot.SetExpression(AIBot.Expression.Thinking);
+ }
+ }
+
+ ///
+ /// Called when user labels current fish
+ ///
+ private void OnExampleAdded(OceanObjectData data, bool isYes)
+ {
+ // AI bot reacts
+ if (aiBot != null)
+ {
+ aiBot.SetExpression(isYes ? AIBot.Expression.Yes : AIBot.Expression.No);
+ }
+
+ // Animate fish exit and show next
+ StartCoroutine(AnimateFishExitAndShowNext(isYes));
+ }
+
+ ///
+ /// Animate fish exiting and bring in next
+ ///
+ private IEnumerator AnimateFishExitAndShowNext(bool wasYes)
+ {
+ if (currentFishRenderer == null)
+ yield break;
+
+ isAnimating = true;
+
+ Transform fishTransform = currentFishRenderer.transform;
+ Vector2 exitPosition = new Vector2(200f, 0f);
+
+ float elapsed = 0f;
+ float exitDuration = 0.5f;
+
+ while (elapsed < exitDuration)
+ {
+ elapsed += Time.deltaTime;
+ float t = elapsed / exitDuration;
+ float easedT = FishAnimator.SCurve(t);
+
+ fishTransform.position = Vector2.Lerp(centerPosition, exitPosition, easedT);
+ yield return null;
+ }
+
+ // Brief pause
+ yield return new WaitForSeconds(0.2f);
+
+ // Check if more fish to show
+ var state = GameManager.Instance.State;
+ if (state.trainingIndex < state.totalTrainingCount &&
+ state.trainingIndex < state.fishData.Count)
+ {
+ ShowNextFish();
+ }
+
+ isAnimating = false;
+ }
+
+ ///
+ /// Skip current animation (for fast users)
+ ///
+ public void SkipAnimation()
+ {
+ if (isAnimating)
+ {
+ StopAllCoroutines();
+ isAnimating = false;
+
+ if (currentFishRenderer != null)
+ {
+ currentFishRenderer.transform.position = centerPosition;
+ }
+ }
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/UI/AIBot.cs b/Unity/AIForOceans/Assets/Scripts/UI/AIBot.cs
new file mode 100644
index 00000000..60cdfe78
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/UI/AIBot.cs
@@ -0,0 +1,153 @@
+using System.Collections;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace AIForOceans
+{
+ ///
+ /// AI Bot character with different expression states
+ ///
+ public class AIBot : MonoBehaviour
+ {
+ public enum Expression
+ {
+ Neutral,
+ Thinking,
+ Yes,
+ No,
+ Scanning,
+ Closed
+ }
+
+ [Header("Sprite References")]
+ public Image bodyImage;
+ public Image headImage;
+ public Image expressionImage;
+ public Image scanOverlayImage;
+
+ [Header("Expression Sprites")]
+ public Sprite neutralSprite;
+ public Sprite thinkingSprite;
+ public Sprite yesSprite;
+ public Sprite noSprite;
+ public Sprite scanningSprite;
+ public Sprite closedSprite;
+
+ [Header("Animation")]
+ public float expressionDuration = 1f;
+ public float bobAmplitude = 5f;
+ public float bobFrequency = 1f;
+
+ private Expression currentExpression = Expression.Neutral;
+ private Coroutine expressionCoroutine;
+ private Vector3 basePosition;
+
+ private void Start()
+ {
+ basePosition = transform.position;
+ SetExpression(Expression.Neutral);
+
+ // Hide scan overlay initially
+ if (scanOverlayImage != null)
+ {
+ scanOverlayImage.enabled = false;
+ }
+ }
+
+ private void Update()
+ {
+ // Gentle bobbing animation
+ float bobOffset = Mathf.Sin(Time.time * bobFrequency) * bobAmplitude;
+ transform.position = basePosition + new Vector3(0f, bobOffset, 0f);
+ }
+
+ ///
+ /// Set the bot's expression
+ ///
+ public void SetExpression(Expression expression)
+ {
+ currentExpression = expression;
+
+ // Update expression sprite
+ if (expressionImage != null)
+ {
+ expressionImage.sprite = expression switch
+ {
+ Expression.Neutral => neutralSprite,
+ Expression.Thinking => thinkingSprite,
+ Expression.Yes => yesSprite,
+ Expression.No => noSprite,
+ Expression.Scanning => scanningSprite,
+ Expression.Closed => closedSprite,
+ _ => neutralSprite
+ };
+ }
+
+ // Show/hide scan overlay
+ if (scanOverlayImage != null)
+ {
+ scanOverlayImage.enabled = expression == Expression.Scanning;
+ }
+
+ // Auto-return to neutral for reactions
+ if (expression == Expression.Yes || expression == Expression.No)
+ {
+ if (expressionCoroutine != null)
+ {
+ StopCoroutine(expressionCoroutine);
+ }
+ expressionCoroutine = StartCoroutine(ReturnToNeutral());
+ }
+ }
+
+ ///
+ /// Return to neutral after showing expression
+ ///
+ private IEnumerator ReturnToNeutral()
+ {
+ yield return new WaitForSeconds(expressionDuration);
+
+ if (currentExpression == Expression.Yes || currentExpression == Expression.No)
+ {
+ SetExpression(Expression.Neutral);
+ }
+ }
+
+ ///
+ /// Play a celebration animation
+ ///
+ public void Celebrate()
+ {
+ StartCoroutine(CelebrationAnimation());
+ }
+
+ private IEnumerator CelebrationAnimation()
+ {
+ SetExpression(Expression.Yes);
+
+ // Bounce animation
+ float duration = 0.5f;
+ float elapsed = 0f;
+ Vector3 startPos = basePosition;
+
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ float t = elapsed / duration;
+ float bounce = Mathf.Sin(t * Mathf.PI * 3) * 10f * (1f - t);
+ transform.position = startPos + new Vector3(0f, bounce, 0f);
+ yield return null;
+ }
+
+ transform.position = startPos;
+ }
+
+ ///
+ /// Get current expression
+ ///
+ public Expression GetExpression()
+ {
+ return currentExpression;
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/UI/ConfirmationDialog.cs b/Unity/AIForOceans/Assets/Scripts/UI/ConfirmationDialog.cs
new file mode 100644
index 00000000..65e72637
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/UI/ConfirmationDialog.cs
@@ -0,0 +1,133 @@
+using System;
+using UnityEngine;
+using UnityEngine.UI;
+using TMPro;
+
+namespace AIForOceans
+{
+ ///
+ /// Reusable confirmation dialog
+ ///
+ public class ConfirmationDialog : MonoBehaviour
+ {
+ [Header("UI Elements")]
+ public GameObject dialogPanel;
+ public TextMeshProUGUI titleText;
+ public TextMeshProUGUI messageText;
+ public Button confirmButton;
+ public Button cancelButton;
+ public TextMeshProUGUI confirmButtonText;
+ public TextMeshProUGUI cancelButtonText;
+
+ // Callbacks
+ private Action onConfirm;
+ private Action onCancel;
+
+ private void Start()
+ {
+ // Set up button listeners
+ if (confirmButton != null)
+ {
+ confirmButton.onClick.AddListener(OnConfirmClicked);
+ }
+
+ if (cancelButton != null)
+ {
+ cancelButton.onClick.AddListener(OnCancelClicked);
+ }
+
+ // Hide by default
+ Hide();
+ }
+
+ ///
+ /// Show the dialog with custom content
+ ///
+ public void Show(
+ string title,
+ string message,
+ string confirmText = "OK",
+ string cancelText = "Cancel",
+ Action onConfirmCallback = null,
+ Action onCancelCallback = null)
+ {
+ if (dialogPanel != null)
+ {
+ dialogPanel.SetActive(true);
+ }
+
+ if (titleText != null)
+ {
+ titleText.text = title;
+ }
+
+ if (messageText != null)
+ {
+ messageText.text = message;
+ }
+
+ if (confirmButtonText != null)
+ {
+ confirmButtonText.text = confirmText;
+ }
+
+ if (cancelButtonText != null)
+ {
+ cancelButtonText.text = cancelText;
+ }
+
+ onConfirm = onConfirmCallback;
+ onCancel = onCancelCallback;
+
+ // Show/hide cancel button based on callback
+ if (cancelButton != null)
+ {
+ cancelButton.gameObject.SetActive(onCancel != null || !string.IsNullOrEmpty(cancelText));
+ }
+ }
+
+ ///
+ /// Show a simple alert (OK only)
+ ///
+ public void ShowAlert(string title, string message, Action onOk = null)
+ {
+ Show(title, message, "OK", "", onOk, null);
+ }
+
+ ///
+ /// Hide the dialog
+ ///
+ public void Hide()
+ {
+ if (dialogPanel != null)
+ {
+ dialogPanel.SetActive(false);
+ }
+
+ onConfirm = null;
+ onCancel = null;
+ }
+
+ private void OnConfirmClicked()
+ {
+ if (SoundManager.Instance != null)
+ {
+ SoundManager.Instance.PlayButtonClick();
+ }
+
+ onConfirm?.Invoke();
+ Hide();
+ }
+
+ private void OnCancelClicked()
+ {
+ if (SoundManager.Instance != null)
+ {
+ SoundManager.Instance.PlayButtonClick();
+ }
+
+ onCancel?.Invoke();
+ Hide();
+ }
+ }
+}
diff --git a/Unity/AIForOceans/Assets/Scripts/UI/GameButton.cs b/Unity/AIForOceans/Assets/Scripts/UI/GameButton.cs
new file mode 100644
index 00000000..b16ea7b5
--- /dev/null
+++ b/Unity/AIForOceans/Assets/Scripts/UI/GameButton.cs
@@ -0,0 +1,166 @@
+using System;
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.EventSystems;
+
+namespace AIForOceans
+{
+ ///
+ /// Custom button with states and feedback
+ ///
+ public class GameButton : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
+ {
+ [Header("Button Settings")]
+ public string buttonId;
+ public bool playSound = true;
+
+ [Header("Visual Settings")]
+ public Color normalColor = Color.white;
+ public Color hoverColor = new Color(0.9f, 0.9f, 0.9f);
+ public Color pressedColor = new Color(0.7f, 0.7f, 0.7f);
+ public Color disabledColor = new Color(0.5f, 0.5f, 0.5f, 0.5f);
+
+ [Header("Animation")]
+ public float scaleOnPress = 0.95f;
+ public float animationSpeed = 10f;
+
+ // Events
+ public event Action OnClicked;
+ public event Action OnPressed;
+ public event Action OnReleased;
+
+ // Components
+ private Image image;
+ private Button button;
+ private RectTransform rectTransform;
+
+ // State
+ private bool isHovered = false;
+ private bool isPressed = false;
+ private bool isInteractable = true;
+ private Vector3 originalScale;
+ private Vector3 targetScale;
+
+ private void Awake()
+ {
+ image = GetComponent();
+ button = GetComponent