<|start_filename|>src/PacMan.GameComponents/ISoundLoader.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.JSInterop;
using PacMan.GameComponents.Audio;

namespace PacMan.GameComponents
{
    public interface ISoundLoader
    {
        SoundEffect GetSoundEffect(SoundName name);

        IEnumerable<SoundEffect> AllSounds { get; }

        ValueTask LoadAll(IJSRuntime runtime);
    }
}

<|start_filename|>src/PacMan.GameComponents/DotCounter.cs<|end_filename|>
﻿using System;
using System.Diagnostics.CodeAnalysis;

namespace PacMan.GameComponents
{
    public class DotCounter
    {
        readonly string _name;
        readonly int _limit;

        bool _timedOut;

        public DotCounter(int limit, string name)
        {
            _name = name;
            _limit = limit;
            _timedOut = false;

            IsActive = false;
            Counter = 0;
        }

        public bool IsActive { get; private set; }

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public void Increment()
        {
            if (!IsActive)
            {
                throw new InvalidOperationException($"Cannot increment a non active counter for {_name}");
            }

            if (Counter >= _limit && _limit > 0)
            {
                throw new InvalidOperationException($"Cannot increment counter, already at limit for {_name}.");
            }

            ++Counter;
        }

        public bool LimitReached => Counter == _limit || _timedOut;

        public int Counter { get; protected set; }

        public virtual void SetTimedOut() => _timedOut = true;

        public virtual bool IsFinished => false;

        public void Deactivate() => IsActive = false;

        public void Activate() => IsActive = true;
    }
}

<|start_filename|>src/PacMan.GameComponents/Tweening/TweenCreationSettings.cs<|end_filename|>
﻿using System;

namespace PacMan.GameComponents.Tweening
{
    public class TweenCreationSettings
    {
        public TweenCreationSettings(Type type, Easing easing, TimeSpan duration)
        {
            Type = type;
            Easing = easing;
            Duration = duration;
        }

        public Type Type
        {
            get;
            private set;
        }

        public Easing Easing
        {
            get;
            private set;
        }

        public TimeSpan Duration
        {
            get;
            private set;
        }

        public TweenCreationSettings(Type type, Easing easing)
        {
            Type = type;
            Easing = easing;
        }

        public static TweenCreationSettings Create<T>()
        {
            return Create<T>(Easing.EaseInOut);
        }

        public static TweenCreationSettings Create<T>(Easing easing)
        {
            return new(typeof(T), easing);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/LoopingTimer.cs<|end_filename|>
﻿using System;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class LoopingTimer
    {
        TimeSpan _currentTime;

        readonly TimeSpan _firesEvery;
        readonly Action _callback;

        public static LoopingTimer DoNothing => new(
            TimeSpan.MaxValue,
            static() => {
            });

        public LoopingTimer(TimeSpan firesEvery, Action callback)
        {
            _firesEvery = firesEvery;
            _currentTime = firesEvery;
            _callback = callback;
        }

        public void Run(CanvasTimingInformation timing)
        {
            _currentTime -= timing.ElapsedTime;

            if (_currentTime < TimeSpan.Zero)
            {
                _currentTime += _firesEvery;

                _callback();
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/GhostStateChangedEvent.cs<|end_filename|>
﻿using MediatR;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents.Events
{
    public readonly struct GhostStateChangedEvent : INotification
    {
        public IGhost Ghost { get; }

        public GhostState State { get; }

        public GhostStateChangedEvent(IGhost ghost, GhostState state)
        {
            Ghost = ghost;
            State = state;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/PlayerHasNoLivesEvent.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using MediatR;
using PacMan.GameComponents.Requests;

namespace PacMan.GameComponents.Events
{
    /// <summary>
    /// When pacman has been caught, eaten, and the player has no more lives.
    /// </summary>
    public readonly struct PlayerHasNoLivesEvent : INotification
    {
        [UsedImplicitly]
        public class Handler : INotificationHandler<PlayerHasNoLivesEvent>
        {
            readonly IMediator _mediator;
            readonly IGame _game;

            public Handler(IGame game, IMediator mediator) => (_game, _mediator) = (game, mediator);

            public async Task Handle(PlayerHasNoLivesEvent notification, CancellationToken cancellationToken)
            {
                // ReSharper disable once HeapView.BoxingAllocation
                var act = await _mediator.Send(new GetActRequest("PlayerGameOverAct"), cancellationToken);
                await act.Reset();

                _game.SetAct(act);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Tweening/Tweener.cs<|end_filename|>
using System;
using System.Diagnostics.CodeAnalysis;
using PacMan.GameComponents.Canvas;
// ReSharper disable CompareOfFloatsByEqualityOperator

namespace PacMan.GameComponents.Tweening
{
    public delegate float TweeningFunction(float timeElapsed, float start, float change, float duration);

    public delegate void TweenEndHandler();

    public class Tweener
    {
        readonly TweeningFunction _tweeningFunction;
        float _from;
        readonly float _duration;

        bool _hasEnded;

        public event TweenEndHandler? Ended;

        public static TweeningFunction CreateTweeningFunction<T>(Easing easing)
        {
            return CreateTweeningFunction(typeof(T), easing);
        }

        public static TweeningFunction CreateTweeningFunction(Type type, Easing easing)
        {
            // ReSharper disable once HeapView.BoxingAllocation
            return (TweeningFunction)Delegate.CreateDelegate(typeof(TweeningFunction), type, easing.ToString());
        }

        public Tweener(float from, float to, float duration, TweeningFunction tweeningFunction)
        {
            _tweeningFunction = tweeningFunction;

            Running = true;
            _elapsed = 0.0f;
            _from = @from;
            _to = to;
            Position = @from;
            _change = to - @from;
            _duration = duration;
        }

        public Tweener(float from, float to, TimeSpan duration, TweeningFunction tweeningFunction)
            : this(from, to, (float)duration.TotalSeconds, tweeningFunction)
        {
        }

        public float Position
        {
            get;
            private set;
        }

        float _change;

        float _elapsed;
        float _to;

        public bool Running
        {
            get;
        }

        public void Update(CanvasTimingInformation gameTime)
        {
            if (!Running || (_elapsed == _duration))
            {
                return;
            }

            Position = _tweeningFunction(_elapsed, _from, _change, _duration);
            _elapsed += (float)gameTime.ElapsedTime.TotalSeconds;

            if (_elapsed >= _duration)
            {
                _elapsed = _duration;
                Position = _from + _change;
                onEnd();
            }
        }

        void onEnd()
        {
            _hasEnded = true;

            Ended?.Invoke();
        }

        public bool HasEnded => _hasEnded;

        public void Reset()
        {
            _hasEnded = false;
            _elapsed = 0.0f;
            _change = _to - _from;
            Position = _from;
        }

        public void Reverse()
        {
            var f = _from;
            _from = _to;
            _to = f;
            Position = f;
            _change = _to - _from;
            _elapsed = 0.0f;
        }

        [SuppressMessage("ReSharper", "HeapView.BoxingAllocation")]
        public override string ToString() =>
            $@"{_tweeningFunction.Method.DeclaringType?.Name}.{_tweeningFunction.Method.Name}. Tween {_from} -> {_from + _change} in {_duration}s. Elapsed {_elapsed:##0.##}s";
    }
}


<|start_filename|>src/PacMan.GameComponents/Tweening/Easing.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Tweening
{
    public enum Easing
    {
        EaseNone,
        EaseIn,
        EaseOut,
        EaseInOut
    }
}

<|start_filename|>src/PacMan.GameComponents/MazeBounds.cs<|end_filename|>
﻿using System.Drawing;
using System.Numerics;

namespace PacMan.GameComponents
{
    public static class MazeBounds
    {
        public static Vector2 TopLeft => Vector2.Zero;

        public static Size Dimensions => new(28, 30);
    }
}

<|start_filename|>src/PacMan.GameComponents/IHumanInterfaceParser.cs<|end_filename|>
﻿using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public interface IHumanInterfaceParser
    {
        bool IsLeftKeyDown { get; }

        bool IsUpKeyDown { get; }

        bool IsDownKeyDown { get; }

        bool IsRightKeyDown { get; }

        bool WasTapped { get; }

        bool WasLongPress { get; }

        void Update(CanvasTimingInformation timing);

        bool IsKeyCurrentlyDown(byte key);

        /// <summary>
        /// A 'press' is pressing and releasing, not just 'pushed'
        /// To see if a key is 'down', use 'IsKeyCurrentlyDown'
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        bool WasKeyPressedAndReleased(byte key);

        void KeyDown(byte key);

        void KeyUp(byte key);

        bool IsPanning(byte key);

        void Swiped(byte key);

        void TapHappened();

        void LongPressHappened();
    }
}

<|start_filename|>src/PacMan.GameComponents/MathHelper.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public static class MathHelper
    {
        public static float Lerp(float value1, float value2, float amount) => value1 + ((value2 - value1) * amount);
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/GhostScatterMover.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Ghosts
{
    /// <summary>
    /// Moves the ghost to their 'scatter cell' (provided by the ghost)
    /// </summary>
    public class GhostScatterMover : GhostMover
    {
        public GhostScatterMover(Ghost ghost, IMaze maze) : base(
            ghost,
            GhostMovementMode.Scatter,
            maze,
            ghost.GetScatterTarget)
        {
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/EyesSpritesheetInfo.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Numerics;

namespace PacMan.GameComponents.Ghosts
{
    public class EyesSpritesheetInfo
    {
        readonly Dictionary<Direction, Vector2> _positions;

        const int _width = 16;

        public EyesSpritesheetInfo(Vector2 position)
        {
            _positions = new();

            var toMove = new Vector2(_width, 0);

            _positions[Direction.Right] = position;

            var marker = position;
            marker = marker + toMove;

            _positions[Direction.Left] = marker;
            marker = marker + toMove;

            _positions[Direction.Up] = marker;
            marker = marker + toMove;

            _positions[Direction.Down] = marker;
        }

        public Vector2 GetSourcePosition(Direction direction) => _positions[direction];
    }
}

<|start_filename|>src/PacMan.GameComponents/Audio/IGameSoundPlayer.cs<|end_filename|>
﻿using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace PacMan.GameComponents.Audio
{
    public interface IGameSoundPlayer
    {
        ValueTask LoadAll(IJSRuntime runtime);

        ValueTask Reset();

        ValueTask Update();

        ValueTask Disable();

        ValueTask Enable();

        ValueTask PowerPillEaten();

        ValueTask FruitEaten();

        ValueTask GhostEaten();

        ValueTask GotExtraLife();

        ValueTask CutScene();

        ValueTask PacManDying();

        ValueTask PlayerStart();

        // ValueTask CoinInserted();
        ValueTask Munch1();

        ValueTask Munch2();

        void MarkAsFinished(string name);

        ValueTask CoinInserted();
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/GhostMovementModeChangedEvent.cs<|end_filename|>
﻿using MediatR;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents.Events
{
    public readonly struct GhostMovementModeChangedEvent : INotification
    {
        public IGhost Ghost { get; }

        public GhostMovementMode Mode { get; }

        public GhostMovementModeChangedEvent(IGhost ghost, GhostMovementMode mode)
        {
            Ghost = ghost;
            Mode = mode;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/GhostMovementConductor.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.Ghosts
{
    public class GhostMovementConductor
    {
        int _index;

        readonly List<ModeAndDuration> _items;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public GhostMovementConductor(GhostsLevelPatternProperties properties)
        {
            _index = -1;

            _items = new() {
                new(GhostMovementMode.Scatter, properties.Scatter1.Seconds()),
                new(GhostMovementMode.Chase, properties.Chase1.Seconds()),
                new(GhostMovementMode.Scatter, properties.Scatter2.Seconds()),
                new(GhostMovementMode.Chase, properties.Chase2.Seconds()),
                new(GhostMovementMode.Scatter, properties.Scatter3.Seconds()),
                new(GhostMovementMode.Chase, properties.Chase3.Seconds()),
                new(GhostMovementMode.Scatter, properties.Scatter4.Seconds()),
                new(GhostMovementMode.Chase, properties.Chase4.Seconds())
            };

            incrementIndex();
        }

        public void Update(CanvasTimingInformation context)
        {
            ModeAndDuration item = _items[_index];

            item.DecreaseDurationBy(context.ElapsedTime);

            if (item.Duration < TimeSpan.Zero)
            {
                incrementIndex();
            }
        }

        void incrementIndex()
        {
            _index += 1;

            if (_index >= _items.Count)
            {
                throw new InvalidOperationException("No more move patterns!?");
            }
        }

        public GhostMovementMode CurrentMode => _items[_index].Mode;
    }
}


<|start_filename|>src/PacMan.GameComponents/IGameModel.cs<|end_filename|>
﻿using System.ComponentModel;

namespace PacMan.GameComponents
{
    public interface IGameModel
    {
        bool OnePlayerGamePossible { get; }

        bool TwoPlayerGamePossible { get; }

        bool ShouldShowCoinGrid { get; }

        string CanContinueIn { get; }

        string PauseResumeImage { get; }

        bool ShouldShowDirectionGrid { get; }

        bool Paused { get; set; }

        string PurchaseLicenseResult { get; set; }

        bool CanContinueFromLicenseScreen { get; }

        event PropertyChangedEventHandler PropertyChanged;
    }
}

<|start_filename|>src/PacMan.GameComponents/Audio/SoundUtilities.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Audio
{
    public static class SoundUtilities
    {
        public static void Mute()
        {
        }

        public static void Unmute()
        {
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/GhostInsideHouseMover.cs<|end_filename|>
﻿using System;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.Ghosts
{
    /// Moves the ghosts while they are inside of the house
    public class GhostInsideHouseMover : GhostMover
    {
        readonly GhostHouseDoor _door;
        readonly Vector2[] _routeOut;
        readonly Vector2 _topPos;
        readonly Vector2 _bottomPos;

        Vector2 _cellToMoveFrom;
        Vector2 _cellToMoveTo;

        bool _readyToExit;
        int _indexInRouteOut;
        bool _finished;

        public GhostInsideHouseMover(
            Ghost ghost,
            IMaze maze,
            GhostHouseDoor ghostHouseDoor) : base(ghost, GhostMovementMode.InHouse, maze, () => new(CellIndex.Zero))
        {
            _door = ghostHouseDoor;

            Vector2 center = Maze.PixelCenterOfHouse;

            float x = center.X + (ghost.OffsetInHouse * 16);

            _topPos = new(x, (float)((13.5 * 8) + 4));
            _bottomPos = new(x, (float)((15.5 * 8) - 4));

            var centerOfUpDown = new Vector2(_topPos.X, Maze.PixelCenterOfHouse.Y);

            ghost.Position = _topPos + new Vector2(0, (_bottomPos.Y - _topPos.Y) / 2);

            _cellToMoveFrom = ghost.Position;

            if (ghost.Direction.Current == Direction.Down)
            {
                _cellToMoveTo = _bottomPos;
            }
            else if (ghost.Direction.Current == Direction.Up)
            {
                _cellToMoveTo = _topPos;
            }
            else
            {
                throw new InvalidOperationException("Ghost must be pointing up or down at start.");
            }

            _routeOut = new[] { centerOfUpDown, Maze.PixelCenterOfHouse, Maze.PixelHouseEntrancePoint };
        }

        void whenAtTargetCell()
        {
            _cellToMoveFrom = _cellToMoveTo;

            if (!_readyToExit)
            {
                if (_cellToMoveTo == _topPos)
                {
                    _cellToMoveTo = _bottomPos;
                }
                else
                {
                    _cellToMoveTo = _topPos;
                }

                return;
            }

            if (_indexInRouteOut == _routeOut.Length)
            {
                _finished = true;
            }
            else
            {
                _cellToMoveTo = _routeOut[_indexInRouteOut++];
            }
        }

        public override ValueTask<MovementResult> Update(CanvasTimingInformation context)
        {
            if (!_readyToExit && _door.CanGhostLeave(Ghost))
            {
                _readyToExit = true;
                return new(MovementResult.NotFinished);
            }

            if (_finished)
            {
                Ghost.Direction = new(Direction.Left, Direction.Left);
                Ghost.SetMovementMode(GhostMovementMode.Undecided);
                return new(MovementResult.Finished);
            }

            Vector2 diff = _cellToMoveTo - _cellToMoveFrom;

            if (diff != Vector2.Zero)
            {
                diff = diff.Normalize();

                Ghost.Position = Ghost.Position + (diff / Vector2s.Two);

                var dir = DirectionToIndexLookup.GetDirectionFromVector(diff);

                Ghost.Direction = new(dir, dir);
            }

            if (Ghost.Position.Floor() == _cellToMoveTo.Floor())
            {
                Ghost.Position = _cellToMoveTo;
                whenAtTargetCell();
            }

            return new(MovementResult.NotFinished);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/SceneUpdateResult.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public enum SceneUpdateResult
    {
        Running,
        Finished
    }
}

<|start_filename|>src/PacMan.GameComponents/LifeStatus.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public enum LifeStatus
    {
        Alive,
        BeingDigested,
        Dying,
        Dead
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/DirectionInfo.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Ghosts
{
    public class DirectionInfo
    {
        public DirectionInfo(Direction current, Direction next) => (Current, Next) = (current, next);

        public Direction Current { get; private set; }

        public Direction Next { get; private set; }

        public void Update(Direction nextDirection)
        {
            Current = Next;
            Next = nextDirection;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/DemoPlayerStats.cs<|end_filename|>
﻿using System.Threading.Tasks;
using MediatR;

namespace PacMan.GameComponents
{
    public class DemoPlayerStats : PlayerStats
    {
        public DemoPlayerStats(IMediator mediator) : base(0, mediator)
        {
            LivesRemaining = 1;
        }

        protected override ValueTask IncreaseScoreBy(int amount)
        {
            return default;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/TimedSpriteList.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class TimedSpriteList
    {
        readonly List<TimedSprite> _sprites;

        public TimedSpriteList()
        {
            _sprites = new();
        }

        public void Add(TimedSprite sprite) => _sprites.Add(sprite);

        public void Update(CanvasTimingInformation timing)
        {
            foreach (var s in _sprites)
            {
                s.Update(timing);
            }

            for (int i = _sprites.Count - 1; i >= 0; i--)
            {
                if (_sprites[i].Expired)
                {
                    _sprites.RemoveAt(i);
                    break;
                }
            }
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            foreach (var s in _sprites)
            {
                await s.Draw(session);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/SimpleFruit.cs<|end_filename|>
﻿using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents
{
    public class SimpleFruit : ISprite
    {
        public SimpleFruit()
        {
            Visible = true;
            Position = Vector2.Zero;

            SetFruitItem(FruitItem.Apple);
        }

        public Vector2 Position { get; set; }

        public ValueTask Draw(CanvasWrapper session)
        {
            return session.DrawSprite(this, Spritesheet.Reference);
        }

        public Vector2 Origin => Vector2s.Eight;

        public Size Size => new(14, 14);

        public Vector2 SpriteSheetPos { get; private set; }

        public bool Visible { get; protected set; }

        public void SetFruitItem(FruitItem item)
        {
            int x = 16 * (int)item;

            SpriteSheetPos = new(490 + x, 50);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/IGameStorage.cs<|end_filename|>
﻿using System.Threading.Tasks;

namespace PacMan.GameComponents
{
    public interface IGameStorage
    {
        ValueTask<int> GetHighScore();

        ValueTask SetHighScore(int highScore);
    }
}

<|start_filename|>src/PacMan.GameComponents/Tweening/Exponential.cs<|end_filename|>
// ReSharper disable CompareOfFloatsByEqualityOperator

using System;

namespace PacMan.GameComponents.Tweening
{
    public static class Exponential
    {
        public static float EaseIn(float t, float b, float c, float d) => (t == 0) ? b : (c * (float)Math.Pow(2, 10 * ((t / d) - 1))) + b;

        public static float EaseOut(float t, float b, float c, float d) => (t == d) ? b + c : (c * (float)(-Math.Pow(2, -10 * t / d) + 1)) + b;

        public static float EaseInOut(float t, float b, float c, float d)
        {
            if (t == 0)
            {
                return b;
            }

            if (t == d)
            {
                return b + c;
            }

            if ((t /= d / 2) < 1)
            {
                return (c / 2 * (float)Math.Pow(2, 10 * (t - 1))) + b;
            }

            return (c / 2 * (float)(-Math.Pow(2, -10 * --t) + 2)) + b;
        }
    }
}


<|start_filename|>src/PacMan.GameComponents/GameActs/NullAct.cs<|end_filename|>
﻿// ReSharper disable RedundantUsingDirective

using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.GameActs
{
    // ReSharper disable once FunctionRecursiveOnAllPaths
    // ReSharper disable once UnusedType.Global
    public class NullAct : IAct
    {

        public string Name => "NullAct";

        public ValueTask Reset()
        {
            return default;
        }

        public ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing) =>
            new(ActUpdateResult.Running);

        public ValueTask Draw(CanvasWrapper session) => default;
    }
}

<|start_filename|>src/PacMan.GameComponents/PlayerStats.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents
{
    public class PlayerStats
    {
        readonly IMediator _mediator;
        GhostHouseDoor _ghostHouseDoor;
        LevelStats _levelStats;
        int _levelNumber;

        GhostMovementConductor _ghostMovementConductor;

        readonly List<int> _extraLives;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public PlayerStats(int playerIndex, IMediator mediator)
        {
            _mediator = mediator;
            PlayerIndex = playerIndex;

            Score = 0;

            // cheat
            LivesRemaining = Constants.PacManLives;
            _levelNumber = -1;

            _extraLives = new() { 10000 };
            _levelStats = new(0);
            _ghostHouseDoor = new(0, _mediator);

            var props = LevelStats.GetGhostPatternProperties();

            _ghostMovementConductor = new(props);
        }

        public void Update(CanvasTimingInformation timing)
        {
            if (FrightSession != null && !FrightSession.IsFinished)
            {
                FrightSession.Update(timing);
            }
            else
            {
                _ghostMovementConductor.Update(timing);
            }

            _ghostHouseDoor.Update(timing);
        }

        public GhostMovementConductor ghostMoveConductor => _ghostMovementConductor;

        public GhostHouseDoor ghostHouseDoor => _ghostHouseDoor;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public void NewLevel()
        {
            _levelStats = new(++_levelNumber);
            _ghostHouseDoor = new(0, _mediator);

            var props = LevelStats.GetGhostPatternProperties();

            _ghostMovementConductor = new(props);
        }

        public int PlayerIndex { get; }

        public LevelStats LevelStats => _levelStats;

        public int LivesRemaining { get; protected set; }

        protected async virtual ValueTask IncreaseScoreBy(int amount)
        {
            Score += amount;

            if (_extraLives.Count == 0)
            {
                return;
            }

            if (Score > _extraLives[0])
            {
                await _mediator.Publish(new ExtraLifeEvent());

                LivesRemaining += 1;

                _extraLives.RemoveAt(0);
            }
        }

        public async ValueTask PillEaten(CellIndex point)
        {
            await _ghostHouseDoor.PillEaten();
            await IncreaseScoreBy(10);
            _levelStats.PillEaten(point);
        }

        public async ValueTask FruitEaten()
        {
            await IncreaseScoreBy(_levelStats.GetLevelProps().FruitPoints);
            LevelStats.FruitSession.FruitEaten();
        }

        public GhostFrightSession? FrightSession { get; private set; }

        public bool IsInFrightSession => FrightSession != null && !FrightSession.IsFinished;

        public async ValueTask PowerPillEaten(CellIndex point)
        {
            FrightSession = new(_levelStats.GetLevelProps());

            await _ghostHouseDoor.PillEaten();
            await IncreaseScoreBy(50);
            _levelStats.PillEaten(point);
        }

        public void PacManEaten()
        {
            _ghostHouseDoor.SwitchToUseGlobalCounter();

            var props = LevelStats.GetGhostPatternProperties();

            _ghostMovementConductor = new(props);
        }

        public void DecreaseLives()
        {
            LivesRemaining -= 1;
        }

        public async ValueTask<int> GhostEaten()
        {
            if (FrightSession == null)
            {
                throw new InvalidOperationException("ghost can't be eaten as there's no fright session");
            }

            var points = FrightSession.GhostEaten();

            await IncreaseScoreBy(points);

            return points;
        }

        public int Score { get; private set; }
    }
}

<|start_filename|>src/PacMan.GameComponents/Canvas/MazeCanvas.cs<|end_filename|>
﻿using System.Drawing;
using System.Threading.Tasks;
using Blazor.Extensions.Canvas.Canvas2D;

namespace PacMan.GameComponents.Canvas
{
    /// <summary>
    /// Represents the maze canvas.  Each Player has their own canvas and when they eat pills
    /// etc., their canvas has individual cells cleared.  When the 'main' maze is drawn,
    /// the current player's canvas is drawn.
    /// </summary>
    public class MazeCanvas : CanvasWrapper
    {
        public MazeCanvas(Canvas2DContext context) : base(context)
        {
        }

        public async ValueTask Reset()
        {
            int width = Spritesheet.Size.Width;
            int height = Spritesheet.Size.Height;

            var dim = Constants.UnscaledCanvasSize;

            await Clear((int)dim.X, (int)dim.Y);

            await DrawImage(
                Spritesheet.Reference,
                new Point(0, 0),
                new(0, 0, width, height));
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/ICoinBox.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public interface ICoinBox
    {
        void UseCredits(int amount);

        int Credits { get; }

        void CoinInserted();
    }
}

<|start_filename|>src/PacMan.GameComponents/Pill.cs<|end_filename|>
﻿using System.Numerics;

namespace PacMan.GameComponents
{
    public class Pill : GeneralSprite
    {
        public Pill() : base(Vector2.Zero, new(8, 8), Vector2s.Four, new(8, 8))
        {
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/GameStats.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class GameStats : IGameStats
    {
        readonly IMediator _mediator;
        readonly IGameStorage _storage;
        int _currentPlayerIndex;

        List<PlayerStats> _playerStats;
        int _ghostsThatAreEyes;

        public GameStats(IMediator mediator, IGameStorage storage)
        {
            _mediator = mediator;
            _storage = storage;

            _playerStats = new();

#pragma warning disable 4014
            Reset(0);
#pragma warning restore 4014
        }

        public async ValueTask Reset(int players)
        {
            _ghostsThatAreEyes = 0;

            HighScore =
                Math.Max(await _storage.GetHighScore(), HighScore);

            HasPlayedIntroTune = false;

            IsDemo = false;

            // ReSharper disable once HeapView.ObjectAllocation.Evident
            _playerStats = new();

            for (int i = 0; i < players; i++)
            {
                _playerStats.Add(new(i, _mediator));
            }

            _currentPlayerIndex = -1;
        }

        public bool IsDemo { get; private set; }

        public bool HasPlayedIntroTune
        {
            get;
            set;
        }

        public bool AreAnyGhostsInEyeState => _ghostsThatAreEyes > 0;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public PlayerStats ResetForDemo()
        {
            IsDemo = true;

            _playerStats = new();

            var playerStats = new DemoPlayerStats(_mediator);

            _playerStats.Add(playerStats);

            _currentPlayerIndex = -1;

            ChoseNextPlayer();

            return playerStats;
        }

        public void Update(CanvasTimingInformation timing)
        {
            CurrentPlayerStats?.Update(timing);
        }

        public PlayerStats GetPlayerStats(int index) => _playerStats[index];

        public ValueTask FruitEaten() => CurrentPlayerStats.FruitEaten();

        public int HighScore { get; private set; } = 10000;

        public bool HasPlayerStats(int playerNumber) => _playerStats.Count > playerNumber;

        public int AmountOfPlayers => _playerStats.Count;

        public bool AnyonePlaying => _currentPlayerIndex != -1;

        public bool IsGameOver => _playerStats.All(p => p.LivesRemaining == 0);

        public PlayerStats CurrentPlayerStats
        {
            get
            {
                if (_currentPlayerIndex < 0)
                {
                    throw new InvalidOperationException("Nobody playing!");
                }

                return _playerStats[_currentPlayerIndex];
            }
        }

        public void ChoseNextPlayer()
        {
            PlayerStats[] players =
                _playerStats.Where(p => p.PlayerIndex > _currentPlayerIndex && p.LivesRemaining > 0)
                    .ToArray();

            if (players.Length == 0)
            {
                players = _playerStats.Where(p => p.LivesRemaining > 0).ToArray();
            }

            if (players.Length > 0)
            {
                _currentPlayerIndex = players[0].PlayerIndex;
            }
            else
            {
                _currentPlayerIndex = -1;
            }
        }

        void updateHighScore()
        {
            HighScore = _playerStats.Count switch
            {
                1 => Math.Max(_playerStats[0].Score, HighScore),
                2 => Math.Max(_playerStats[1].Score, HighScore),
                _ => HighScore
            };
        }

        public async ValueTask PillEaten(CellIndex point)
        {
            await _playerStats[_currentPlayerIndex].PillEaten(point);
            updateHighScore();
        }

        public async ValueTask PowerPillEaten(CellIndex point)
        {
            await _playerStats[_currentPlayerIndex].PowerPillEaten(point);
            updateHighScore();
        }

        public void PacManEaten()
        {
            _playerStats[_currentPlayerIndex].PacManEaten();
            _ghostsThatAreEyes = 0;
        }

        public async ValueTask<int> GhostEaten()
        {
            ++_ghostsThatAreEyes;

            var points = await _playerStats[_currentPlayerIndex].GhostEaten();
            updateHighScore();
            return points;
        }

        public void LevelFinished()
        {
            CurrentPlayerStats.NewLevel();
        }

        public ValueTask HandleGhostBackInsideHouse()
        {
            --_ghostsThatAreEyes;

            return default;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Canvas/CanvasTimingInformation.cs<|end_filename|>
﻿using System;

namespace PacMan.GameComponents.Canvas
{
    public class CanvasTimingInformation
    {
        public TimeSpan TotalTime { get; private set; } = TimeSpan.Zero;

        public TimeSpan ElapsedTime { get; private set; }

        public void Update(float elapsedSinceLastCall)
        {
            ElapsedTime = TimeSpan.FromMilliseconds(elapsedSinceLastCall);

            TotalTime += ElapsedTime;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/DirectionChoices.cs<|end_filename|>
﻿using System.Collections.Generic;

namespace PacMan.GameComponents
{
    public class DirectionChoices
    {
        readonly Dictionary<Direction, bool> _lookup;

        public DirectionChoices()
        {
            _lookup = new();
        }

        public int Possibilities { get; private set; }

        public void Set(Direction direction)
        {
            _lookup[direction] = true;

            Possibilities = calcPossibilities();
        }

        public void Unset(Direction direction)
        {
            _lookup[direction] = false;

            Possibilities = calcPossibilities();
        }

        public bool IsSet(Direction direction) => _lookup[direction];

        public void ClearAll()
        {
            _lookup[Direction.Up] = false;
            _lookup[Direction.Down] = false;
            _lookup[Direction.Left] = false;
            _lookup[Direction.Right] = false;
        }

        int calcPossibilities()
        {
            int count = 0;

            if (_lookup[Direction.Up])
            {
                ++count;
            }

            if (_lookup[Direction.Down])
            {
                ++count;
            }

            if (_lookup[Direction.Left])
            {
                ++count;
            }

            if (_lookup[Direction.Right])
            {
                ++count;
            }

            return count;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/Clyde.cs<|end_filename|>
﻿using System;
using System.Drawing;
using System.Threading.Tasks;
using MediatR;

namespace PacMan.GameComponents.Ghosts
{
    public class Clyde : Ghost
    {
        readonly IMaze _maze;
        readonly IPacMan _pacman;
        readonly ValueTask<CellIndex> _scatterTarget = new(new CellIndex(0, 29));

        public Clyde(IGameStats gameStats, IMediator mediator, IMaze maze, IPacMan pacman, IHumanInterfaceParser input) : base(
                gameStats,
                mediator,
                input,
                pacman,
                GhostNickname.Clyde, maze, new(11.5f, 12), GameComponents.Direction.Up)
        {
            _maze = maze;
            _pacman = pacman;

            HouseOffset = 1;
        }

        public override Color GetColor() => Color.YellowGreen;

        public override ValueTask<CellIndex> GetScatterTarget() => _scatterTarget;

        public override ValueTask<CellIndex> GetChaseTarget() => _getChaseTargetCell();

        public override void Reset()
        {
            base.Reset();

            Direction = new(GameComponents.Direction.Up, GameComponents.Direction.Up);

            State = GhostState.Normal;

            MovementMode = GhostMovementMode.InHouse;

            SetMover(new(this, _maze, CurrentPlayerStats.ghostHouseDoor));
        }

        // Whenever Clyde needs to determine his target tile, he first calculates his distance from Pac-Man.
        // If he is farther than eight tiles away, his targeting is identical to Blinky’s,
        // using Pac-Man’s current tile as his target. However, as soon as his distance
        // to Pac-Man becomes less than eight tiles, Clyde’s target is set to the same tile as his fixed
        // one in Scatter mode, just outside the bottom-left corner of the maze
        // Pac-Man’s current position and orientation, and selecting the location four tiles straight
        // ahead of him. Works when PacMan is facing left, down, or right, but when facing upwards,
        // it's also four tiles to the left
        ValueTask<CellIndex> _getChaseTargetCell()
        {
            CellIndex pacCellPos = _pacman.Tile.Index;

            CellIndex myPos = Tile.Index;

            var distance = Math.Abs(Extensions.DistanceBetween(myPos, pacCellPos));

            if (distance >= 8)
            {
                return new(pacCellPos);
            }

            return _scatterTarget;
        }

        // todo:
        // draw(canvas: Canvas) : void {
        //    super.draw(canvas);
        //
        //    if (Diags.enabled) {
        //        this.maze.highlightCell(canvas, this._getChaseTargetCell(), "orange");
        //    }
        // };
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/PowerPillEatenEvent.cs<|end_filename|>
﻿using System;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;
using PacMan.GameComponents.Ghosts;
using PacMan.GameComponents.Requests;

namespace PacMan.GameComponents.Events
{
    public readonly struct PowerPillEatenEvent : INotification
    {
        public PowerPillEatenEvent(CellIndex cellIndex) => CellIndex = cellIndex;

        public CellIndex CellIndex { get; }

        public class Handler : INotificationHandler<PowerPillEatenEvent>
        {
            readonly IGame _game;
            readonly IGameStats _gameStats;
            readonly IGameSoundPlayer _gameSoundPlayer;
            readonly IMediator _mediator;
            readonly IGhostCollection _ghostCollection;

            public Handler(
                IGame game,
                IGameStats gameStats,
                IGameSoundPlayer gameSoundPlayer,
                IMediator mediator,
                IGhostCollection ghostCollection)
            {
                _game = game;
                _gameStats = gameStats;
                _gameSoundPlayer = gameSoundPlayer;
                _mediator = mediator;
                _ghostCollection = ghostCollection;
            }

            public async Task Handle(PowerPillEatenEvent notification, CancellationToken cancellationToken)
            {
                await _gameSoundPlayer.PowerPillEaten();

                await _gameStats.PowerPillEaten(notification.CellIndex);
                
                GhostFrightSession? frightSession = _gameStats.CurrentPlayerStats.FrightSession;

                if (frightSession == null)
                {
                    throw new InvalidOperationException("no fright session");
                }

                foreach (IGhost eachGhost in _ghostCollection.Ghosts)
                {
                    eachGhost.PowerPillEaten(frightSession);
                }

                await checkForNoMorePills();
            }

            async Task checkForNoMorePills()
            {
                if (_gameStats.CurrentPlayerStats.LevelStats.PillsRemaining == 0)
                {
                    // ReSharper disable once HeapView.BoxingAllocation
                    var act = await _mediator.Send(new GetActRequest("LevelFinishedAct"));

                    await act.Reset();

                    _game.SetAct(act);
                }
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/ModeAndDuration.cs<|end_filename|>
﻿using System;
using JetBrains.Annotations;

namespace PacMan.GameComponents.Ghosts
{
    [CannotApplyEqualityOperator]
    public class ModeAndDuration
    {
        public ModeAndDuration(GhostMovementMode mode, TimeSpan duration)
        {
            Mode = mode;
            Duration = duration;
        }

        public GhostMovementMode Mode { get; }

        public TimeSpan Duration { get; private set; }

        public void DecreaseDurationBy(in TimeSpan elapsedTime) => Duration -= elapsedTime;
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/TornGhostChaseAct.cs<|end_filename|>
﻿using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;

namespace PacMan.GameComponents.GameActs
{
    public class TornGhostChaseAct : IAct
    {
        readonly IMediator _mediator;
        readonly IGameSoundPlayer _gameSoundPlayer;
        readonly AttractScenePacMan _pacMan;
        readonly GeneralSprite _worm;

        readonly GeneralSprite _blinky;

        readonly StartAndEndPos _pacPositions;
        readonly EggTimer _pacTimer;

        StartAndEndPos _ghostStartAndEndPos;
        EggTimer _ghostTimer;

        bool _finished;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public TornGhostChaseAct(IMediator mediator, IGameSoundPlayer gameSoundPlayer)
        {
            _mediator = mediator;
            _gameSoundPlayer = gameSoundPlayer;
            _finished = false;

            var justOffScreen = new Vector2(250, 140);

            _ghostTimer = new(4500.Milliseconds(), reverseChase);

            _pacTimer = new(4800.Milliseconds(), () => { });

            _pacMan = new() {
                Direction = Direction.Left
            };

            _worm = new(
                Vector2.Zero,
                new(22, 11),
                new(11, 5.5f),
                new(594, 132),
                new(626, 132),
                110.Milliseconds())
            {
                Visible = false
            };

            _blinky = new(
                Vector2.Zero,
                new(14, 14),
                new(7.5f, 7.5f),
                new(618, 113),
                new(634, 113),
                110.Milliseconds());

            _pacPositions = new(justOffScreen, new(-70, justOffScreen.Y));

            _ghostStartAndEndPos = new(justOffScreen + new Vector2(50, 0), new(-40, justOffScreen.Y));
        }

        public string Name { get; } = "TornGhostChaseAct";

        public async ValueTask Reset()
        {
            _finished = false;
            _pacMan.Position = _pacPositions.Start;
            _blinky.Position = _ghostStartAndEndPos.Start;

            await _gameSoundPlayer.CutScene();
        }

        public async ValueTask<ActUpdateResult> Update(CanvasTimingInformation gameContext)
        {
            _ghostTimer.Run(gameContext);
            _pacTimer.Run(gameContext);
            await _worm.Update(gameContext);

            lerpBlinky();
            lerpPacMan();

            await _pacMan.Update(gameContext);
            await _blinky.Update(gameContext);
            await _worm.Update(gameContext);

            return _finished ? ActUpdateResult.Finished : ActUpdateResult.Running;
        }

        public async ValueTask Draw(CanvasWrapper canvas)
        {
            await _blinky.Draw(canvas);
            await _pacMan.Draw(canvas);
            await _worm.Draw(canvas);
        }

        void lerpBlinky()
        {
            var pc = _ghostTimer.Progress;
            _blinky.Position = Vector2.Lerp(_ghostStartAndEndPos.Start, _ghostStartAndEndPos.End, pc);
            _worm.Position = Vector2.Lerp(_ghostStartAndEndPos.Start, _ghostStartAndEndPos.End, pc);
        }

        void lerpPacMan()
        {
            var pc = _pacTimer.Progress;

            _pacMan.Position = Vector2.Lerp(_pacPositions.Start, _pacPositions.End, pc);
        }

        void reverseChase()
        {
            _ghostTimer = new(4600.Milliseconds(), async () =>
            {
                _finished = true;
                await _mediator.Publish(new CutSceneFinishedEvent());
            });

            _pacMan.Visible = false;
            _blinky.Visible = false;
            _worm.Visible = true;

            _ghostStartAndEndPos = _ghostStartAndEndPos.Reverse();
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/IGhostCollection.cs<|end_filename|>
﻿using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents
{
    public interface IGhostCollection
    {
        IGhost GetGhost(GhostNickname nickName);

        IGhost[] Ghosts { get; }

        ValueTask DrawAll(CanvasWrapper canvas);

        ValueTask Update(CanvasTimingInformation timing);
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/BigPacChaseAct.cs<|end_filename|>
﻿using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;
using PacMan.GameComponents.Ghosts;

// ReSharper disable HeapView.ObjectAllocation.Evident

namespace PacMan.GameComponents.GameActs
{
    public class BigPacChaseAct : IAct
    {
        // these need to send playerstartingevent when finished
        readonly IMediator _mediator;
        readonly IGameSoundPlayer _gameSoundPlayer;
        readonly AttractScenePacMan _pacMan;
        readonly GeneralSprite _bigPacMan;

        readonly AttractGhost _blinky;

        StartAndEndPos _pacPositions;
        EggTimer _pacTimer;

        StartAndEndPos _blinkyPositions;
        EggTimer _blinkyTimer;

        bool _finished;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public BigPacChaseAct(IMediator mediator, IGameSoundPlayer gameSoundPlayer)
        {
            _mediator = mediator;
            _gameSoundPlayer = gameSoundPlayer;
            _finished = false;

            var justOffScreen = new Vector2(250, 140);

            _blinkyTimer = new(4500.Milliseconds(), reverseChase);

            _pacTimer = new(4750.Milliseconds(), static () => { });

            _pacMan = new() { Direction = Direction.Left };

            _bigPacMan = new(
                Vector2.Zero,
                new(31, 32),
                new(16, 16),
                new(488, 16),
                new(520, 16),
                110.Milliseconds())
            {
                Visible = false
            };

            _blinky = new(GhostNickname.Blinky, Direction.Left);

            _pacPositions = new(justOffScreen, new(-70, justOffScreen.Y));

            _blinkyPositions = new(justOffScreen + new Vector2(20, 0), new(-40, justOffScreen.Y));
        }

        public string Name { get; } = "BigPacChaseAct";

        public async ValueTask Reset()
        {
            _pacMan.Position = _pacPositions.Start;
            _blinky.Position = _blinkyPositions.Start;

            await _gameSoundPlayer.CutScene();
        }

        public async ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing)
        {
            _blinkyTimer.Run(timing);
            _pacTimer.Run(timing);
            await _bigPacMan.Update(timing);

            lerpBlinky();
            lerpPacMan();

            await _pacMan.Update(timing);
            await _blinky.Update(timing);
            await _bigPacMan.Update(timing);

            return _finished ? ActUpdateResult.Finished : ActUpdateResult.Running;
        }

        public async ValueTask Draw(CanvasWrapper canvas)
        {
            await _blinky.Draw(canvas);
            await _pacMan.Draw(canvas);
            await _bigPacMan.Draw(canvas);
        }

        void lerpBlinky()
        {
            float pc = _blinkyTimer.Progress;

            _blinky.Position = Vector2.Lerp(_blinkyPositions.Start, _blinkyPositions.End, pc);
        }

        void lerpPacMan()
        {
            float pc = _pacTimer.Progress;

            _pacMan.Position = Vector2.Lerp(_pacPositions.Start, _pacPositions.End, pc);
            _bigPacMan.Position = Vector2.Lerp(_pacPositions.Start, _pacPositions.End, pc);
        }

        void reverseChase()
        {
            _blinkyTimer = new(4600.Milliseconds(), static () => { });

            _pacTimer = new(4350.Milliseconds(), async () =>
            {
                _finished = true;
                await _mediator.Publish(new CutSceneFinishedEvent());
            });

            _pacMan.Visible = false;
            _bigPacMan.Visible = true;

            var s = new LevelStats(0);
            var sess = new GhostFrightSession(s.GetLevelProps());

            _blinky.Direction = new(Direction.Right, Direction.Right);
            _blinky.SetFrightSession(sess);
            _blinky.SetFrightened();
            _blinkyPositions = _blinkyPositions.Reverse();

            var bigPacPos = _pacPositions.Reverse();
            bigPacPos = new(bigPacPos.Start - new Vector2(100, 0), bigPacPos.End);

            _pacPositions = bigPacPos;
        }
    }
}

<|start_filename|>src/PacMan/ExceptionNotificationService.cs<|end_filename|>
﻿using System;
using System.IO;
using System.Text;

namespace PacMan
{
    public class ExceptionNotificationService : TextWriter, IExceptionNotificationService
    {
        private readonly TextWriter _decorated;

        public override Encoding Encoding => Encoding.UTF8;

        /// <summary>
        /// Raised is an exception occurs. The exception message will be send to the listeners
        /// </summary>
        public event EventHandler<string> OnException;

        public ExceptionNotificationService()
        {
            _decorated = Console.Error;

            Console.SetError(this);
        }

        public override void WriteLine(string value)
        {
            // notify the listeners
            OnException?.Invoke(this, value);

            _decorated.WriteLine(value);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Requests/GetActRequest.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.GameActs;

namespace PacMan.GameComponents.Requests
{
    public readonly struct GetActRequest : IRequest<IAct>
    {
        public GetActRequest(string actName)
        {
            ActName = actName;
        }

        public string ActName { get; }

        public class Handler : IRequestHandler<GetActRequest, IAct>
        {
            readonly IActs _acts;

            public Handler(IActs acts)
            {
                _acts = acts;
            }

            public Task<IAct> Handle(GetActRequest request, CancellationToken cancellationToken)
            {
                return Task.FromResult(_acts.GetActNamed(request.ActName));
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/StatusPanel.cs<|end_filename|>
﻿using System;
using System.Drawing;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class StatusPanel : IStatusPanel
    {
        readonly IGameStats _gameStats;
        readonly ICoinBox _coinBox;

        // ReSharper disable once HeapView.ObjectAllocation.Evident
        readonly StringBuilder _sb = new();

        readonly Vector2 _creditTextPoint = new(10, 00);

        readonly LoopingTimer _timer;

        readonly SimpleFruit _fruit;

        bool _tickTock = true;

        public StatusPanel(IGameStats gameStats, ICoinBox coinBox)
        {
            _gameStats = gameStats;
            _coinBox = coinBox;

            // ReSharper disable once HeapView.ObjectAllocation.Evident
            _timer = new(250.Milliseconds(), () => _tickTock = !_tickTock);

            // ReSharper disable once HeapView.ObjectAllocation.Evident
            _fruit = new();
        }

        public void Update(CanvasTimingInformation timing)
        {
            _timer.Run(timing);
        }

        public async ValueTask Draw(CanvasWrapper ds)
        {
            if (_gameStats.AnyonePlaying)
            {
                await drawPlayerLives(ds);
                await drawFruit(ds);
            }
            else
            {
                await drawCredits(ds);
            }
        }

        async ValueTask drawPlayerLives(CanvasWrapper ds)
        {
            int x = 0;

            for (var i = 0; i < _gameStats.CurrentPlayerStats.LivesRemaining; i++, x += 16)
            {
                await ds.DrawImage(
                    Spritesheet.Reference,
                    new Rectangle(x, 0, 16, 16),
                    new(
                        (int)PacMan.FacingLeftSpritesheetPos.X,
                        (int)PacMan.FacingLeftSpritesheetPos.Y,
                        16,
                        16));
            }
        }

        ValueTask drawCredits(CanvasWrapper ds)
        {
            _sb.Clear();
            return ds.DrawMyText(_sb.Append("CREDIT ").Append(_coinBox.Credits).ToString(), _creditTextPoint, Colors.White);
        }

        // drawSprite max 7 fruit from max level 21
        async ValueTask drawFruit(CanvasWrapper ds)
        {
            if (_gameStats.IsDemo)
            {
                return;
            }

            var highestLevel = Math.Min(
                20,
                _gameStats.CurrentPlayerStats.LevelStats.LevelNumber);

            var lowestLevel = Math.Max(
                0,
                highestLevel - 6);

            var x = 204;

            // starting from the right
            for (var i = lowestLevel; i <= highestLevel; i++, x -= 16)
            {
                var item = LevelStats.GetLevelProps(i).Fruit1;

                _fruit.SetFruitItem(item);
                _fruit.Position = new(x, 10);

                await ds.DrawSprite(_fruit, Spritesheet.Reference);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/TileContent.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public enum TileContent
    {
        Nothing,
        Pill,
        PowerPill,
        Wall
    }
}

<|start_filename|>src/PacMan.GameComponents/AttractGhost.cs<|end_filename|>
﻿using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents
{
    public class AttractGhost : SimpleGhost
    {
        public AttractGhost(GhostNickname nickName, Direction direction) : base(nickName, direction)
        {
            Alive = true;
        }

        public void SetFrightened()
        {
            State = GhostState.Frightened;
        }

        public bool Alive { get; set; }
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/DemoPlayerIntroAct.cs<|end_filename|>
﻿using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Requests;

namespace PacMan.GameComponents.GameActs
{
    /// <summary>
    /// Introduces the current player, shows player X and ready for 3 seconds, then ghosts and ready for 3 seconds.
    /// Transitions to either the 'demo act' (if in demo mode), otherwise the 'game act'
    /// </summary>
    public class DemoPlayerIntroAct : IAct
    {
        readonly IMediator _mediator;
        readonly IGame _game;
        readonly IGhostCollection _ghostCollection;
        readonly IPacMan _pacman;
        readonly IMaze _maze;
        int _progress;
        bool _finished;

        LoopingTimer _currentTimer = LoopingTimer.DoNothing;

        public DemoPlayerIntroAct(
            IMediator mediator,
            IGame game,
            IGhostCollection ghostCollection,
            IPacMan pacman,
            IMaze maze)
        {
            _mediator = mediator;
            _game = game;
            _ghostCollection = ghostCollection;
            _pacman = pacman;
            _maze = maze;
        }

        public string Name => "DemoPlayerIntroAct";

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public ValueTask Reset()
        {
            _progress = 0;

            var timeToShowPlayerNumberAndHideGhosts = 0.Seconds();

            _currentTimer = new(timeToShowPlayerNumberAndHideGhosts, () =>
            {
                _progress += 1;

                _currentTimer = new(2.Seconds(), () => _finished = true);
            });

            return default;
        }

        public async ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing)
        {
            _currentTimer.Run(timing);

            if (_finished)
            {
                // ReSharper disable once HeapView.BoxingAllocation
                var act = await _mediator.Send(new GetActRequest("GameAct"));

                _game.SetAct(act);
            }

            return ActUpdateResult.Finished;
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            await _maze.Draw(session);

            await session.DrawMyText("GAME OVER", TextPoints.GameOverPoint, Colors.Red);

            if (_progress == 1)
            {
                await _pacman.Draw(session);
                await _ghostCollection.DrawAll(session);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/FrightenedSpritesheet.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Ghosts
{
    public class FrightenedSpritesheet
    {
        public FrightenedSpritesheet(FramePair blue, FramePair white)
        {
            Blue = blue;
            White = white;
        }

        public FramePair Blue { get; }

        public FramePair White { get; }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/GhostFrightenedMover.cs<|end_filename|>
﻿using System.Threading.Tasks;

namespace PacMan.GameComponents.Ghosts
{
    /// Moves the ghost in a psuedo-random fashion while they are 'frightened' (i.e. blue)
    public class GhostFrightenedMover : GhostMover
    {
        public GhostFrightenedMover(Ghost ghost, IMaze maze) : base(ghost, GhostMovementMode.Frightened, maze, GetTargetCell)
        {
        }

        private static ValueTask<CellIndex> GetTargetCell()
        {
            var random = Pnrg.Value;

            CellIndex cell = (random % 4) switch
            {
                0 => new((int)MazeBounds.TopLeft.X, (int)MazeBounds.TopLeft.Y),
                1 => new(MazeBounds.Dimensions.Width, 0),
                2 => new(MazeBounds.Dimensions.Width, MazeBounds.Dimensions.Height),
                _ => new(0, MazeBounds.Dimensions.Height)
            };

            return new(cell);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/IStatusPanel.cs<|end_filename|>
﻿using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public interface IStatusPanel
    {
        void Update(CanvasTimingInformation timing);

        ValueTask Draw(CanvasWrapper ds);
    }
}

<|start_filename|>src/PacMan.GameComponents/DemoKeyPresses.cs<|end_filename|>
﻿using System;

namespace PacMan.GameComponents
{
    public class DemoKeyPresses
    {
        // 01234567890123456789012345678901234567890123456789012345678901234567890123456789
        const string _presses = "ldrdrruluruluuulllllddlllddldlul";

        int _index;

        public DemoKeyPresses() => _index = 0;

        public void Reset() => _index = 0;

        public Direction Next()
        {
            if (_index >= _presses.Length)
            {
                // ReSharper disable once HeapView.ObjectAllocation.Evident
                throw new InvalidOperationException("Used up all key presses!");
            }

            return _presses[_index++] switch
            {
                'u' => Direction.Up,
                'd' => Direction.Down,
                'l' => Direction.Left,
                'r' => Direction.Right,
                // ReSharper disable once HeapView.BoxingAllocation
                _ => throw new InvalidOperationException($"Don't know what direction ${_presses[_index]} is!")
            };
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/NewGameEvent.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using MediatR;

namespace PacMan.GameComponents.Events
{
    public readonly struct NewGameEvent : INotification
    {
        public int AmountOfPlayers { get; }

        public NewGameEvent(in int amountOfPlayers)
        {
            AmountOfPlayers = amountOfPlayers;
        }

        public class Handler : INotificationHandler<NewGameEvent>
        {
            readonly IMediator _mediator;
            readonly ICoinBox _coinBox;
            readonly IHaveTheMazeCanvases _mazeCanvases;
            readonly IGameStats _gameStats;

            public Handler(
                IGameStats gameStats,
                IMediator mediator,
                ICoinBox coinBox,
                IHaveTheMazeCanvases mazeCanvases)
            {
                _gameStats = gameStats;
                _mediator = mediator;
                _coinBox = coinBox;
                _mazeCanvases = mazeCanvases;
            }

            public async Task Handle(NewGameEvent notification, CancellationToken cancellationToken)
            {
                await _mazeCanvases.GetForPlayer(0).Reset();
                await _mazeCanvases.GetForPlayer(1).Reset();

                _coinBox.UseCredits(notification.AmountOfPlayers);

                await _gameStats.Reset(notification.AmountOfPlayers);
                _gameStats.ChoseNextPlayer();

                var playerStartingEvent = new PlayerStartingEvent();

                await _mediator.Publish(playerStartingEvent, cancellationToken);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/MovementResult.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Ghosts
{
    public enum MovementResult
    {
        Finished,
        NotFinished
    }
}

<|start_filename|>src/PacMan.GameComponents/Maze.cs<|end_filename|>
﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class Maze : ISprite, IMaze
    {
        // the point where the ghost goes to just before going into chase/scatter mode
        public readonly static Vector2 TileHouseEntrance = Tile.FromCell(13.5f, 11);

        public readonly static Vector2 PixelHouseEntrancePoint = Tile.ToCenterCanvas(new(13.5f, 11));

        // the point where the ghost goes to before going up and out of the house
        public readonly static Vector2 PixelCenterOfHouse = Tile.ToCenterCanvas(new(13.5f, 14));

        readonly static CellIndex[] _specialIntersections =
        {
            new(12, 11),
            new(15, 11),
            new(12, 26),
            new(15, 26)
        };

        readonly static CellIndex[] _powerPillPositions =
        {
            new(1, 3),
            new(26, 3),
            new(1, 23),
            new(26, 23)
        };

        readonly LoopingTimer _timer;

        readonly DirectionChoices _directionChoices = new();
        readonly PowerPill _powerPill;
        readonly GeneralSprite _whiteMazeCanvas;

        bool _tickTock = true;

        bool _flashing;

        readonly static Size _spritesheetSize = new(225, 248);
        readonly static Rectangle _mazeRect = new(0, 0, 225, 248);

        readonly static Direction[] _directions =
        {
            Direction.Left,
            Direction.Right,
            Direction.Up,
            Direction.Down
        };

        MazeCanvas? _currentPlayerCanvas;
        LevelStats? _levelStats;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public Maze()
        {
            Visible = true;
            Origin = Vector2.Zero;
            _powerPill = new();
            _timer = new(250.Milliseconds(), () => _tickTock = !_tickTock);

            _whiteMazeCanvas = new(
                Vector2.Zero,
                _spritesheetSize,
                Vector2.Zero, new(228, 0));
        }

        public Vector2 SpriteSheetPos => Vector2.Zero;

        public bool Visible { get; }

        public Size Size => _spritesheetSize;

        // special intersections have an extra restriction
        // ghosts can not choose to turn upwards from these tiles.
        public static bool IsSpecialIntersection(CellIndex cell) =>
            cell == _specialIntersections[0] ||
            cell == _specialIntersections[1] ||
            cell == _specialIntersections[2] ||
            cell == _specialIntersections[3];

        public Vector2 Position => Vector2.Zero;

        public ValueTask Update(CanvasTimingInformation timing)
        {
            _timer.Run(timing);
            _powerPill.Update(timing);
            return default;
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            ensureCanvas();
            if (_flashing)
            {
                if (_tickTock)
                {
                    await session.DrawSprite(_whiteMazeCanvas, Spritesheet.Reference);
                }
                else
                {
                    await session.DrawFromOther(_currentPlayerCanvas!, new(0, 0), _mazeRect);
                }

                return;
            }

            await session.DrawFromOther(_currentPlayerCanvas!, new(0, 0), _mazeRect);

            await drawPowerPills(session);

            // this.drawGrid(8, 8, canvas);
        }

        void ensureCanvas()
        {
            if (_currentPlayerCanvas == null)
            {
                throw new InvalidOperationException("no current player canvas");
            }
        }

        public Vector2 Origin { get; }

        async ValueTask drawPowerPills(CanvasWrapper session)
        {
            ensureLevelStats();

            foreach (var p in _powerPillPositions)
            {
                if (_levelStats!.GetCellContent(p) == '*')
                {
                    _powerPill.Position = p.ToVector2() * Vector2s.Eight;// - Vector2s.Four;

                    await session.DrawSprite(_powerPill, Spritesheet.Reference);
                }
            }
        }

        void ensureLevelStats()
        {
            if (_levelStats == null)
            {
                throw new InvalidOperationException("no level stats");
            }
        }

        public async ValueTask ClearCell(CellIndex cell)
        {
            var topLeft = Tile.FromIndex(cell).TopLeft;

            // CanvasBitmap bitmap = _bitmapsForPlayers[_gameStats.CurrentPlayerStats.PlayerIndex];

            // await bitmap.BeginBatch();

            await _currentPlayerCanvas!.Clear((int) topLeft.X, (int) topLeft.Y, 8, 8);

            // await bitmap.FillRect((int)topLeft.X, (int)topLeft.Y, 8, 8, Color.Black);
            // await bitmap.EndBatch();
        }

        public static bool IsInTunnel(CellIndex point)
        {
            if (point.Y != 14)
            {
                return false;
            }

            if (point.X <= 5)
            {
                return true;
            }

            if (point.X >= 22)
            {
                return true;
            }

            return false;
        }

        public bool CanContinueInDirection(Direction direction, Tile tile)
        {
            var nextTile = tile.NextTile(direction);

            return !isAWall(nextTile);
        }

        public DirectionChoices GetChoicesAtCellPosition(CellIndex cellPos)
        {
            _directionChoices.ClearAll();

            var tile = Tile.FromIndex(cellPos);

            foreach (var eachDirection in _directions)
            {
                if (!isAWall(tile.NextTileWrapped(eachDirection)))
                {
                    _directionChoices.Set(eachDirection);
                }
            }

            return _directionChoices;
        }

        bool isAWall(Tile tile) => GetTileContent(tile) == TileContent.Wall;

        public void StartFlashing() => _flashing = true;

        public void StopFlashing() => _flashing = false;

        public TileContent GetTileContent(Tile cell)
        {
            ensureLevelStats();
            var a = _levelStats!.GetCellContent(cell.Index);

            if (a == ' ')
            {
                return TileContent.Wall;
            }

            if (a == 'o')
            {
                return TileContent.Pill;
            }

            if (a == '*')
            {
                return TileContent.PowerPill;
            }

            if (a == '+')
            {
                return TileContent.Nothing;
            }

            return TileContent.Nothing;
        }

        // todo: use clamp
        public static CellIndex ConstrainCell(CellIndex cell)
        {
            var x = cell.X;
            var y = cell.Y;

            x = x < 0 ? 0 : x;
            x = x > MazeBounds.Dimensions.Width ? MazeBounds.Dimensions.Width : x;

            y = y < 0 ? 0 : y;
            y = y > MazeBounds.Dimensions.Height ? MazeBounds.Dimensions.Height : y;

            return new(x, y);
        }

        public ValueTask HandlePlayerStarted(PlayerStats playerStats, MazeCanvas playerCanvas)
        {
            _levelStats = playerStats.LevelStats;
            _currentPlayerCanvas = playerCanvas;

            return default;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/TimedSprite.cs<|end_filename|>
﻿using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class TimedSprite
    {
        double _timeToLive;

        readonly ISprite _sprite;

        public TimedSprite(int timeToLive, ISprite sprite)
        {
            _timeToLive = timeToLive;
            _sprite = sprite;
        }

        public void Update(CanvasTimingInformation timing)
        {
            _timeToLive -= timing.ElapsedTime.TotalMilliseconds;
        }

        public bool Expired => _timeToLive < 0;

        public async ValueTask Draw(CanvasWrapper session)
        {
            await _sprite.Draw(session);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/GameOverAct.cs<|end_filename|>
﻿using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.GameActs
{
    public class GameOverAct : IAct
    {
        readonly IMaze _maze;
        bool _finished;

        LoopingTimer _currentTimer = LoopingTimer.DoNothing;

        public GameOverAct(IMaze maze) => _maze = maze;

        public string Name => "GameOverAct";

        public ValueTask Reset()
        {
            _finished = false;

            _currentTimer = new(2.Seconds(), () =>
            {
                _currentTimer = new(2.Seconds(), () =>
                {
                    _finished = true;
                });
            });

            return default;
        }

        public ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing)
        {
            _currentTimer.Run(timing);

            return new(_finished ? ActUpdateResult.Finished : ActUpdateResult.Running);
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            await _maze.Draw(session);

            await session.DrawMyText("GAME OVER", TextPoints.GameOverPoint, Colors.Red);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/PacMan.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;

namespace PacMan.GameComponents
{
    public class PacMan : ISprite, IPacMan
    {
        readonly IMaze _maze;
        readonly IHumanInterfaceParser _input;
        readonly IMediator _mediator;
        public static readonly Vector2 FacingLeftSpritesheetPos = new(455, 16);

        CellIndex _lastDemoKeyPressAt = CellIndex.Zero;

        CellIndex _pillEatenAt = CellIndex.Zero;

        LifeStatus _lifeStatus;

        Vector2 _spriteSheetPos;

        Direction _direction;
        float _dyingFramePointer;

        Vector2 _frame1InSpriteMap;
        Vector2 _frame2InSpriteMap;
        TwoFrameAnimation _animDirection;

        float _speed = Constants.PacManBaseSpeed;

        bool _isDemoMode;

        readonly Dictionary<Direction, FramePointers> _framePointers;
        readonly List<Vector2> _dyingFrames;
        readonly DemoKeyPresses _demoKeyPresses;
        readonly Tile _tile;
        readonly KeyPressedEvent _keyPress;
        PlayerStats? _currentPlayerStats;

        public PacMan(IMaze maze, IHumanInterfaceParser input, IMediator mediator)
        {
            _maze = maze;
            _input = input;
            _mediator = mediator;
            _demoKeyPresses = new();

            _lifeStatus = LifeStatus.Alive;

            _tile = new();

            _keyPress = new();
            _framePointers = new();

            const int left = 456;
            const int left2 = 472;

            _framePointers[GameComponents.Direction.Up] = new(
                new(left, 32), new(left2, 32));

            _framePointers[GameComponents.Direction.Down] = new(
                new(left, 48), new(left2, 48));

            _framePointers[GameComponents.Direction.Left] = new(
                new(left, 16), new(left2, 16));

            _framePointers[GameComponents.Direction.Right] = new(
                new(left, 0), new(left2, 0));

            _dyingFrames = new();
            _dyingFramePointer = 0;

            for (var i = 0; i < 12; i++)
            {
                _dyingFrames.Add(new(489 + (i * 16), 0));
            }

            _animDirection = new(65.Milliseconds());

            Reset();
        }

        void resetAll(bool isDemoMode = false)
        {
            Visible = true;
            _demoKeyPresses.Reset();
            _isDemoMode = isDemoMode;
            _direction = GameComponents.Direction.Left;
            _speed = Constants.PacManBaseSpeed;
            _dyingFramePointer = 0;
            Position = Tile.ToCenterCanvas(new(13.5f, 23));
            _lifeStatus = LifeStatus.Alive;

            _animDirection = new(65.Milliseconds());
            _frame1InSpriteMap = _framePointers[_direction].Frame1;
            _frame2InSpriteMap = _framePointers[_direction].Frame2;

            _spriteSheetPos = _frame1InSpriteMap;
        }

        public bool Visible { get; set; }

        public Vector2 SpriteSheetPos => _spriteSheetPos;

        public Size Size { get; } = new(15, 15);

        public ValueTask Draw(CanvasWrapper session) => session.DrawSprite(this, Spritesheet.Reference);

        public Vector2 Origin => Vector2s.Eight;

        public Tile Tile => _tile;

        public void Reset()
        {
            resetAll();
        }

        public Direction Direction => _direction;

        void updateAnimation(CanvasTimingInformation args)
        {
            if (Math.Abs(_speed) < .0001)
            {
                return;
            }

            _animDirection.Run(args);
            _frame1InSpriteMap = _framePointers[_direction].Frame1;
            _frame2InSpriteMap = _framePointers[_direction].Frame2;

            _spriteSheetPos = _animDirection.Flag ? _frame1InSpriteMap : _frame2InSpriteMap;
        }

        void handleDying()
        {
            _dyingFramePointer += .15f;

            if (_dyingFramePointer > _dyingFrames.Count)
            {
                _lifeStatus = LifeStatus.Dead;
            }
            else
            {
                _spriteSheetPos = _dyingFrames[(int)Math.Floor(_dyingFramePointer)];
            }
        }

        public void StartDying()
        {
            _lifeStatus = LifeStatus.Dying;
        }

        public void StartDigesting()
        {
            _lifeStatus = LifeStatus.BeingDigested;
        }

        public async ValueTask Update(CanvasTimingInformation timing)
        {
            if (_currentPlayerStats == null)
            {
                throw new InvalidOperationException("no current player stats");
            }

            if (_lifeStatus == LifeStatus.BeingDigested)
            {
                return;
            }

            if (_lifeStatus is LifeStatus.Dying or LifeStatus.Dead)
            {
                handleDying();
                return;
            }

            updateAnimation(timing);

            if (_tile.IsNearCenter(2))
            {
                recordInput(timing);

                recenterInLane();

                handleDirection();
            }

            if (_tile.IsNearCenter(1.5))
            {
                await handleWhatIsUnderCell();

                var can = _maze.CanContinueInDirection(_direction, _tile);

                _speed = can ? Constants.PacManBaseSpeed : 0;
            }

            float speed = _speed;

            var levelProps = _currentPlayerStats.LevelStats.GetLevelProps();
            var inPillCell = _tile.Index == _pillEatenAt;

            var pcToUse = inPillCell ? levelProps.PacManDotsSpeedPc : levelProps.PacManSpeedPc;

            if (_currentPlayerStats.IsInFrightSession)
            {
                pcToUse = inPillCell ? levelProps.FrightPacManDotSpeedPc : levelProps.FrightPacManSpeedPc;
            }

            if (!inPillCell)
            {
                _pillEatenAt = CellIndex.Zero;
            }

            speed = speed * (pcToUse / 100);

            var offset = DirectionToIndexLookup.IndexVectorFor(_direction) * speed;

            Position = Position + offset;
        }

        public Vector2 Position
        {
            get => _tile.SpritePos;
            private set => _tile.UpdateWithSpritePos(value);
        }

        public void PillEaten()
        {
            _pillEatenAt = _tile.Index;
        }

        void handleDirection()
        {
            if (_maze.CanContinueInDirection(_keyPress.Direction, _tile))
            {
                _direction = _keyPress.Direction;
            }
        }

        async ValueTask handleWhatIsUnderCell()
        {
            TileContent contents = _maze.GetTileContent(_tile);

            if (contents == TileContent.Pill)
            {
                await _maze.ClearCell(_tile.Index);

                await _mediator.Publish(new PillEatenEvent(_tile.Index));

                // _ = _game.PillEaten(_tile.Index);
            }

            if (contents == TileContent.PowerPill)
            {
                await _maze.ClearCell(_tile.Index);

                await _mediator.Publish(new PowerPillEatenEvent(_tile.Index));

                // _ = _game.PowerPillEaten(_tile.Index);
            }
        }

        void recordInput(CanvasTimingInformation context)
        {
            Direction requestedDirection = _direction;

            if (_isDemoMode)
            {
                if (_tile.IsNearCenter(4) && _tile.Index != _lastDemoKeyPressAt)
                {
                    var choices = _maze.GetChoicesAtCellPosition(_tile.Index);

                    choices.Unset(_direction);

                    switch (_direction)
                    {
                        case Direction.Left:
                            choices.Unset(Direction.Right);
                            break;
                        case Direction.Right:
                            choices.Unset(Direction.Left);
                            break;
                        case Direction.Up:
                            choices.Unset(Direction.Down);
                            break;
                        case Direction.Down:
                            choices.Unset(Direction.Up);
                            break;
                    }

                    if (choices.Possibilities >= 1)
                    {
                        requestedDirection = _demoKeyPresses.Next();

                        _keyPress.When = context.TotalTime.TotalMilliseconds;

                        _keyPress.Direction = requestedDirection;

                        _lastDemoKeyPressAt = _tile.Index;
                    }
                }
            }
            else
            {
                if (_input.IsRightKeyDown || _input.IsPanning(Keys.Right))
                {
                    requestedDirection = Direction.Right;
                }
                else if (_input.IsLeftKeyDown || _input.IsPanning(Keys.Left))
                {
                    requestedDirection = Direction.Left;
                }
                else if (_input.IsDownKeyDown || _input.IsPanning(Keys.Down))
                {
                    requestedDirection = Direction.Down;
                }
                else if (_input.IsUpKeyDown || _input.IsPanning(Keys.Up))
                {
                    requestedDirection = Direction.Up;
                }
            }

            _keyPress.Direction = requestedDirection;
            _keyPress.When = context.TotalTime.TotalMilliseconds;
        }

        // debt: SD: refactor into something common as this is also used for the ghosts
        void recenterInLane()
        {
            var tileCenter = _tile.CenterPos;

            var speed = _speed;

            if (_direction == Direction.Down || _direction == Direction.Up)
            {
                var wayToMove = new Vector2(speed, 0);

                if (Position.X > tileCenter.X)
                {
                    Position = Position - wayToMove;
                    Position = new(Math.Max(Position.X, tileCenter.X), Position.Y);
                }
                else if (Position.X < tileCenter.X)
                {
                    Position = Position + wayToMove;
                    Position = new(Math.Min(Position.X, tileCenter.X), Position.Y);
                }
            }

            if (_direction == Direction.Left || _direction == Direction.Right)
            {
                var wayToMove = new Vector2(0, speed);

                if (Position.Y > tileCenter.Y)
                {
                    Position = Position - wayToMove;
                    Position = new(Position.X, Math.Max(Position.Y, tileCenter.Y));
                }
                else if (Position.Y < tileCenter.Y)
                {
                    Position = Position + wayToMove;
                    Position = new(Position.X, Math.Min(Position.Y, tileCenter.Y));
                }
            }
        }

        public ValueTask HandlePlayerStarting(PlayerStats playerStats, bool isDemo)
        {
            _currentPlayerStats = playerStats;

            resetAll(isDemo);

            return default;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/IFruit.cs<|end_filename|>
﻿using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents
{
    public interface IFruit
    {
        Vector2 Position { get; set; }

        Vector2 Origin { get; }

        Size Size { get; }

        Vector2 SpriteSheetPos { get; }

        bool Visible { get; }

        ValueTask Update(CanvasTimingInformation timing);

        ValueTask Draw(CanvasWrapper session);

        void SetFruitItem(FruitItem item);

        void HandlePlayerStarted(PlayerStats playerStats, bool isDemo);
    }
}

<|start_filename|>src/PacMan.GameComponents/IMaze.cs<|end_filename|>
﻿using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public interface IMaze
    {
        // Vector2 SpriteSheetPos { get; }
        // bool Visible { get; }
        // Size Size { get; }
        // Vector2 Position { get; }
        // Vector2 Origin { get; }
        ValueTask Update(CanvasTimingInformation timing);

        ValueTask Draw(CanvasWrapper session);

        ValueTask ClearCell(CellIndex cell);

        bool CanContinueInDirection(Direction direction, Tile tile);

        DirectionChoices GetChoicesAtCellPosition(CellIndex cellPos);

        void StartFlashing();

        void StopFlashing();

        TileContent GetTileContent(Tile cell);

        ValueTask HandlePlayerStarted(PlayerStats playerStats, MazeCanvas playerCanvas);
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/IGhost.cs<|end_filename|>
﻿using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.Ghosts
{
    public interface IGhost
    {
        Tile Tile { get; }

        GhostNickname NickName { get; }

        DirectionInfo Direction { get; set; }

        bool Visible { get; set; }

        Size Size { get; }

        GhostState State { get; }

        bool IsInHouse { get; }

        Vector2 SpriteSheetPos { get; }

        Vector2 Origin { get; }

        void PowerPillEaten(GhostFrightSession session);

        void Reset();

        void StopMoving();

        ValueTask Update(CanvasTimingInformation timing);

        ValueTask Draw(CanvasWrapper session);
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/LevelProps.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Ghosts
{
    public record LevelProps(
        IntroCutScene CutScene,
        FruitItem Fruit1,
        int FruitPoints,
        float PacManSpeedPc,
        float PacManDotsSpeedPc,
        float GhostSpeedPc,
        float GhostTunnelSpeedPc,
        int Elroy1DotsLeft,
        float Elroy1SpeedPc,
        int Elroy2DotsLeft,
        float Elroy2SpeedPc,
        float FrightPacManSpeedPc,
        float FrightPacManDotSpeedPc,
        float FrightGhostSpeedPc,
        int FrightGhostTime,
        int FrightGhostFlashes);
}

<|start_filename|>src/PacMan.GameComponents/Events/CutSceneFinishedEvent.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using MediatR;

namespace PacMan.GameComponents.Events
{
    /// <summary>
    /// Then the cut scene has finished playing.
    /// </summary>
    public readonly struct CutSceneFinishedEvent : INotification
    {
        public class Handler : INotificationHandler<CutSceneFinishedEvent>
        {
            readonly IMediator _mediator;

            public Handler(IMediator mediator) => _mediator = mediator;

            public async Task Handle(CutSceneFinishedEvent notification, CancellationToken cancellationToken) =>
                await _mediator.Publish(new PlayerStartingEvent(), cancellationToken);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/SimpleGhost.cs<|end_filename|>
﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.Ghosts
{
    public class SimpleGhost : ISprite
    {
        protected readonly GhostSpritesheetInfo SpritesheetInfoNormal;

        readonly FrightenedSpritesheet _spritesheetInfoFrightened;
        readonly EyesSpritesheetInfo _spriteSheetEyes;

        readonly TwoFrameAnimation _toggle = new(65.Milliseconds());

        GhostFrightSession? _frightSession;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public SimpleGhost(GhostNickname nickName, Direction direction)
        {
            NickName = nickName;
            Direction = new(direction, direction);

            Visible = true;

            var spriteSheet = new GhostSpritesheet();

            SpritesheetInfoNormal = spriteSheet.GetEntry(nickName);
            _spritesheetInfoFrightened = new GhostSpritesheet().GetFrightened();
            _spriteSheetEyes = spriteSheet.Eyes;

            SpriteSheetPos = SpritesheetInfoNormal.GetSourcePosition(Direction.Next, true);
        }

        public GhostNickname NickName { get; }

        public DirectionInfo Direction { get; set; }

        public bool Visible { get; set; }

        public Size Size => new(16, 16);

        protected GhostMovementMode MovementMode { get; set; }

        public GhostState State { get; protected set; }

        public bool IsInHouse => MovementMode == GhostMovementMode.InHouse;

        public Vector2 SpriteSheetPos { get; protected set; }

        public Vector2 Origin => Vector2s.Eight;

        public virtual Vector2 Position { get; set; }

        public void SetFrightSession(GhostFrightSession value)
        {
            _frightSession = value ?? throw new ArgumentNullException(nameof(value));
        }

        public virtual ValueTask Update(CanvasTimingInformation timing)
        {
            updateAnimation(timing);
            return default;
        }

        public virtual ValueTask Draw(CanvasWrapper session)
        {
            if (Visible)
            {
                return session.DrawSprite(this, Spritesheet.Reference);
            }

            return default;
        }

        void updateAnimation(CanvasTimingInformation context)
        {
            _toggle.Run(context);

            if (State == GhostState.Frightened)
            {
                SpriteSheetPos = getGhostFrame();
            }
            else if (State == GhostState.Eyes)
            {
                SpriteSheetPos = _spriteSheetEyes.GetSourcePosition(Direction.Next);
            }
            else
            {
                SpriteSheetPos = SpritesheetInfoNormal.GetSourcePosition(Direction.Next, _toggle.Flag);
            }
        }

        public virtual void PowerPillEaten(GhostFrightSession session) => SetFrightSession(session);

        Vector2 getGhostFrame()
        {
            if (_frightSession == null)
            {
                throw new InvalidOperationException("Cannot get ghost frame - not in a fright session");
            }

            FramePair pair = _frightSession.IsWhite
                ? _spritesheetInfoFrightened.White
                : _spritesheetInfoFrightened.Blue;

            return _toggle.Flag ? pair.First : pair.Second;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Cheats.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public static class Cheats
    {
        public static bool PacManNeverDies
        {
#if DEBUG
            get { return false; }
#else
            get { return false; }
#endif
        }

        public static bool ShowDiags
        {
#if DEBUG
            get { return true; }
#else
            get { return false; }
#endif
        }

        public static bool AllowDebugKeys
        {
#if DEBUG
            get { return true; }
#else
            get { return false; }
#endif
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/DemoStartedEvent.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;

namespace PacMan.GameComponents.Events
{
    public readonly struct DemoStartedEvent : INotification
    {
        public class Handler : INotificationHandler<DemoStartedEvent>
        {
            readonly IGameSoundPlayer _gameSoundPlayer;
            readonly IGameStats _gameStats;
            readonly IMediator _mediator;
            readonly IHaveTheMazeCanvases _mazeCanvases;

            public Handler(
                IGameSoundPlayer gameSoundPlayer,
                IGameStats gameStats,
                IMediator mediator,
                IHaveTheMazeCanvases mazeCanvases)
            {
                _gameSoundPlayer = gameSoundPlayer;
                _gameStats = gameStats;
                _mediator = mediator;
                _mazeCanvases = mazeCanvases;
            }

            public async Task Handle(DemoStartedEvent notification, CancellationToken cancellationToken)
            {
                _gameStats.ResetForDemo();

                await _gameSoundPlayer.Disable();

                var canvasWrapper = _mazeCanvases.GetForPlayer(0);
                await canvasWrapper.Reset();

                var playerStartedEvent = new DemoPlayerStartingEvent();

                await _mediator.Publish(playerStartedEvent, cancellationToken);
            }
        }
    }
}

<|start_filename|>src/PacMan/wwwroot/sounds.js<|end_filename|>
﻿class SoundPlayer {
    constructor() {
        this.sounds = [];
    }

    loadSound(name, path) {
        const soundEffect = new SoundEffect(name);
        soundEffect.loadSound(path);
        this.sounds[name] = soundEffect;
    }

    getSound(name) {
        return this.sounds[name];
    }

    stop(name) {
        this.getSound(name).stop();
    }

    getVolume(name) {
        return this.getSound(name).getVolume();
    }

    setVolume(name, value) {
        this.getSound(name).setVolume(value);
    }

    isPlaying(name) {
        return this.getSound(name).isPlaying();
    }

    setLoop(name, value) {
        this.getSound(name).setLoop(value);
    }

    mute(name) {
        return this.getSound(name).mute();
    }

    unmute(name) {
        return this.getSound(name).unmute();
    }

    isLoaded(name) {
        return this.getSound(name).isLoaded();
    }

    play(name) {
        this.getSound(name).play();
    }

    tryPlay(name) {
        this.getSound(name).tryPlay();
    }
};

class SoundEffect {
    
    constructor(name) {
        this._name = name;
    }

    loadSound(path) {
        this._howl= new Howl({
            src: path
        });

        this._howl.on("load",
            () => {

                this._loaded = true;
            });

        this._howl.on("loaderror",
            () => {

                this._loaded = true;
            });

        this._howl.on("end",
            () => {
                window.theInstance.invokeMethodAsync('SoundEnded', this._name);
            });
    }

    getVolume() {
        return this._howl.volume();
    }

    setVolume(value) {
        this._howl.volume(value);
    }

    isPlaying() {
        return this._howl.playing();
    }

    setLoop(value) {
        this._howl.loop(value);
    }

    mute() {
        return this._howl.mute(true);
    }

    unmute() {
        return this._howl.mute(false);
    }

    isLoaded() {
        return this._loaded;
    }

    stop() {
        this._howl.stop();
    }

    play() {
        if (!this.isLoaded) {
            throw new Error(`Not loaded!`);
        }

        if (this._howl.loop() && this._howl.playing()) {
            return;
        }

        this._howl.play();
    }

    tryPlay() {
        if (this._howl.loop() && !this._howl.playing()) {
            return;
        }

        this._howl.play();
    }
}

window.soundPlayer = new SoundPlayer();


<|start_filename|>test/SmallTests/LevelStatsTests.cs<|end_filename|>
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using PacMan.GameComponents;
using Xunit;
// ReSharper disable All

namespace SmallTests
{
    public class LevelStatsTests
    {
        [Fact]
        [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")]
        public void Equality()
        {
            LevelStats sut = new(2);
            var props = sut.GetGhostPatternProperties();
            props.Scatter1.Should().Be(7);
            props.Chase1.Should().Be(20);
            props.Scatter2.Should().Be(7);
            props.Chase2.Should().Be(20);
            props.Scatter3.Should().Be(5);
            props.Chase3.Should().Be(1033);
            props.Scatter4.Should().Be(0);
            props.Chase4.Should().Be(int.MaxValue);
        }

        [Fact]
        public void Adding()
        {
            CellIndex c10_10 = new(10, 10);

            (c10_10 + c10_10).Should().Be(new CellIndex(20,20));
        }

        [Fact]
        public void Subtracting()
        {
            CellIndex c10_10 = new(10, 10);

            (c10_10 - c10_10).Should().Be(new CellIndex(0,0));
            (c10_10 - c10_10).Should().Be(CellIndex.Zero);
        }
    }
}


<|start_filename|>src/PacMan.GameComponents/LicenseMode.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public enum LicenseMode
    {
        Trial,
        Full,
        Inactive
    }
}

<|start_filename|>src/PacMan.GameComponents/Colors.cs<|end_filename|>
﻿using System.Drawing;

namespace PacMan.GameComponents
{
    public static class Colors
    {
        public static Color White => Color.White;

        public static Color Red => Color.Red;

        public static Color Pink => Color.Pink;

        public static Color Cyan => Color.Cyan;

        public static Color Yellow => Color.Yellow;

        public static Color Orange => Color.Orange;
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/GhostEyesBackToHouseMover.cs<|end_filename|>
﻿using System;
using System.Numerics;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;

namespace PacMan.GameComponents.Ghosts
{
    /// Moves the ghost back to the house.
    public class GhostEyesBackToHouseMover : GhostMover
    {
        readonly IMediator _mediator;
        readonly Vector2 _ghostPosInHouse;
        Func<CanvasTimingInformation, ValueTask<MovementResult>> _currentAction;

        public GhostEyesBackToHouseMover(Ghost ghost, IMaze maze, IMediator mediator)
            : base(ghost, GhostMovementMode.GoingToHouse, maze, () => new(Maze.TileHouseEntrance.ToCellIndex()))
        {
            _mediator = mediator;
            _ghostPosInHouse = Maze.PixelCenterOfHouse + new Vector2(ghost.OffsetInHouse * 16, 0);

            _currentAction = navigateEyesBackToJustOutsideHouse;
        }

        async ValueTask<MovementResult> navigateEyesBackToJustOutsideHouse(CanvasTimingInformation context)
        {
            await base.Update(context);

            if (isNearHouseEntrance())
            {
                await _mediator.Publish(new GhostInsideHouseEvent());
                Ghost.Position = Maze.PixelHouseEntrancePoint;
                _currentAction = navigateToCenterOfHouse;
            }

            return MovementResult.NotFinished;
        }

        ValueTask<MovementResult> navigateToCenterOfHouse(CanvasTimingInformation context)
        {
            var diff = Maze.PixelCenterOfHouse - Maze.PixelHouseEntrancePoint;

            if (diff != Vector2.Zero)
            {
                diff = diff.Normalize();
                Ghost.Position = Ghost.Position + diff;
            }

            if (Ghost.Position.Round() == Maze.PixelCenterOfHouse)
            {
                _currentAction = navigateToGhostIndexInHouse;
            }

            return new(MovementResult.NotFinished);
        }

        ValueTask<MovementResult> navigateToGhostIndexInHouse(CanvasTimingInformation context)
        {
            var diff = _ghostPosInHouse - Maze.PixelCenterOfHouse;

            if (diff != Vector2.Zero)
            {
                diff = diff.Normalize();
                Ghost.Position = Ghost.Position + diff;
            }

            if (Ghost.Position.Round() == _ghostPosInHouse)
            {
                Ghost.Direction = new(Direction.Down, Direction.Down);
                Ghost.SetMovementMode(GhostMovementMode.InHouse);
                return new(MovementResult.Finished);
            }

            return new(MovementResult.NotFinished);
        }

        public async override ValueTask<MovementResult> Update(CanvasTimingInformation context) => await _currentAction(context);

        bool isNearHouseEntrance() => Vector2s.AreNear(Ghost.Position, Maze.PixelHouseEntrancePoint, .75);
    }
}

<|start_filename|>src/PacMan.GameComponents/Tweening/Vector2Tweener.cs<|end_filename|>
﻿using System;
using System.Numerics;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents.Tweening
{
    public class Vector2Tweener
    {
        readonly Tweener _tweenerX;
        readonly Tweener _tweenerY;

        public delegate void EndHandler();

        public event TweenEndHandler? Ended;

        public Vector2Tweener(Vector2 start, Vector2 end, Type type, Easing easing, TimeSpan duration)
        {
            _tweenerX = new(start.X, end.X, duration, Tweener.CreateTweeningFunction(type, easing));
            _tweenerY = new(start.Y, end.Y, duration, Tweener.CreateTweeningFunction(type, easing));
            
            _tweenerX.Ended += () =>
            {
                if (_tweenerY.HasEnded)
                {
                    Ended?.Invoke();
                }
            };

            _tweenerY.Ended += () =>
            {
                if (_tweenerX.HasEnded)
                {
                    Ended?.Invoke();
                }
            };
        }

        public Vector2 Position
        {
            get
            {
                return new(_tweenerX.Position, _tweenerY.Position);
            }
        }

        public void Update(CanvasTimingInformation gameTime)
        {
            _tweenerX.Update(gameTime);
            _tweenerY.Update(gameTime);
        }

        public static Vector2Tweener Create<T>(Vector2 start, Vector2 end, Easing easing, TimeSpan duration)
        {
            return Create(start, end, typeof(T), easing, duration);
        }

        public static Vector2Tweener Create(Vector2 start, Vector2 end, Type type, Easing easing, TimeSpan duration)
        {
            return new(start, end, type, easing, duration);
        }

        public static Vector2Tweener Create(Vector2 start, Vector2 end, TweenCreationSettings tweenCreationSettings)
        {
            return Create(start, end, tweenCreationSettings.Type, tweenCreationSettings.Easing, tweenCreationSettings.Duration);
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/FramePointers.cs<|end_filename|>
﻿using System.Numerics;

namespace PacMan.GameComponents
{
    public class FramePointers
    {
        public FramePointers(
            Vector2 frame1,
            Vector2 frame2)
        {
            Frame1 = frame1;
            Frame2 = frame2;
        }

        public Vector2 Frame1 { get; }

        public Vector2 Frame2 { get; }
    }
}

<|start_filename|>src/PacMan.GameComponents/Vector2s.cs<|end_filename|>
﻿using System.Drawing;
using System.Numerics;

namespace PacMan.GameComponents
{

    // ReSharper disable once InconsistentNaming
    public static class Vector2s
    {
        public static Vector2 Zero = new(0, 0);
        public static Vector2 Two = new(2, 2);
        public static Vector2 Four = new(4, 4);
        public static Vector2 Eight = new(8, 8);

        public static bool AreNear(Vector2 first, Vector2 second, double range)
        {
            var r1 = new Rectangle((int)first.X, (int)first.Y, 1, 1);
            r1 = r1.Expand(range);

            var r2 = new Rectangle((int)second.X, (int)second.Y, 1, 1);
            r2 = r2.Expand(range);

            return r1.Intersects(r2);
        }

        public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) => new(
            MathHelper.Lerp(value1.X, value2.X, amount),
            MathHelper.Lerp(value1.Y, value2.Y, amount));
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/FruitEatenEvent.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;

namespace PacMan.GameComponents.Events
{
    public readonly struct FruitEatenEvent : INotification
    {
        public IFruit Fruit { get; }

        public FruitEatenEvent(IFruit fruit)
        {
            Fruit = fruit;
        }

        public class Handler : INotificationHandler<FruitEatenEvent>
        {
            readonly IGame _game;
            readonly IGameStats _gameStats;
            readonly IGameSoundPlayer _gameSoundPlayer;

            public Handler(IGame game, IGameStats gameStats, IGameSoundPlayer gameSoundPlayer)
            {
                _game = game;
                _gameStats = gameStats;
                _gameSoundPlayer = gameSoundPlayer;
            }

            public async Task Handle(FruitEatenEvent notification, CancellationToken cancellationToken)
            {
                await _gameSoundPlayer.FruitEaten();

                await _gameStats.FruitEaten();

                int points = _gameStats.CurrentPlayerStats.LevelStats.GetLevelProps().FruitPoints;

                await _game.FruitEaten(points);
            }
        }
    }

    public readonly struct ExtraLifeEvent : INotification
    {
        public class Handler : INotificationHandler<ExtraLifeEvent>
        {
            readonly IGameSoundPlayer _gameSoundPlayer;

            public Handler(IGameSoundPlayer gameSoundPlayer)
            {
                _gameSoundPlayer = gameSoundPlayer;
            }

            public async Task Handle(ExtraLifeEvent notification, CancellationToken cancellationToken) =>
                await _gameSoundPlayer.GotExtraLife();
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/GameAct.cs<|end_filename|>
﻿using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;

namespace PacMan.GameComponents.GameActs
{
    /// <summary>
    /// The main 'Game' act.  Draws everything (maze, ghosts, pacman), and updates everything (keyboard, sound etc.). Transitions to the 'player intro' act.
    /// </summary>
    public class GameAct : IAct
    {
        readonly ICoinBox _coinBox;
        readonly IMediator _mediator;
        readonly IHumanInterfaceParser _input;
        readonly IGameSoundPlayer _gameSoundPlayer;
        readonly IGameStats _gameStats;
        readonly IGhostCollection _ghostCollection;
        readonly IMaze _maze;
        readonly IPacMan _pacman;
        readonly IFruit _fruit;

        bool _paused;

        public GameAct(
            ICoinBox coinBox,
            IMediator mediator,
            IHumanInterfaceParser input,
            IGameSoundPlayer gameSoundPlayer,
            IGameStats gameStats,
            IGhostCollection ghostCollection,
            IMaze maze,
            IPacMan pacman,
            IFruit fruit)
        {
            _coinBox = coinBox;
            _mediator = mediator;
            _input = input;
            _gameSoundPlayer = gameSoundPlayer;
            _gameStats = gameStats;
            _ghostCollection = ghostCollection;
            _maze = maze;
            _pacman = pacman;
            _fruit = fruit;
        }

        public string Name { get; } = "GameAct";

        public ValueTask Reset()
        {
            Pnrg.ResetPnrg();

            return default;
        }

        public async ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing)
        {
            if (_gameStats.IsDemo)
            {
                if (_input.WasKeyPressedAndReleased(Keys.Space) ||
                   _input.WasKeyPressedAndReleased(Keys.One) ||
                   _input.WasTapped)
                {
                    await _gameSoundPlayer.Enable();
                    _coinBox.CoinInserted();
                    await _mediator.Publish(new NewGameEvent(1));

                    return ActUpdateResult.Running;
                }

                if (_input.WasKeyPressedAndReleased(Keys.Two) || _input.WasLongPress)
                {
                    await _gameSoundPlayer.Enable();

                    _coinBox.CoinInserted();
                    _coinBox.CoinInserted();

                    await _mediator.Publish(new NewGameEvent(2));

                    return ActUpdateResult.Running;
                }

                if (_input.WasKeyPressedAndReleased(Keys.Five))
                {
                    await _mediator.Publish(new CoinInsertedEvent());

                    return ActUpdateResult.Finished;
                }
            }

            if (_input.WasKeyPressedAndReleased(Keys.P))
            {
                _paused = !_paused;

                if (_paused)
                {
                    await _gameSoundPlayer.Disable();
                }

                else
                {
                    await _gameSoundPlayer.Enable();
                }
            }

            if (_paused)
            {
                return ActUpdateResult.Running;
            }

            // play sounds first, as if we play them last, that state of the game
            // might've updated and the 'current act' might've changed
            // so we don't want to be playing our sounds if
            // another act is queued up.
            await _gameSoundPlayer.Update();

            _gameStats.Update(timing);

            await _maze.Update(timing);
            await _pacman.Update(timing);
            await _fruit.Update(timing);

            await _ghostCollection.Update(timing);

            return ActUpdateResult.Running;
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            await _maze.Draw(session);
            await _pacman.Draw(session);

            await _fruit.Draw(session);

            await _ghostCollection.DrawAll(session);

            if (_gameStats.IsDemo)
            {
                await session.DrawMyText("GAME OVER", TextPoints.GameOverPoint, Colors.Red);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Pnrg.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public static class Pnrg
    {
        static int _pnrg;

        public static void ResetPnrg()
        {
            _pnrg = 0;
        }

        public static int Value => _pnrg;

        public static void Update()
        {
            ++_pnrg;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/EggTimer.cs<|end_filename|>
﻿using System;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class EggTimer
    {
        readonly Action _whenFinished;
        readonly TimeSpan _duration;

        bool _isPaused;
        bool _isFinished;
        TimeSpan _currentTime;

        static void doNothing()
        {
        }

        public EggTimer(TimeSpan duration, Action whenFinished) : this(duration) =>
            _whenFinished = whenFinished;

        public EggTimer(TimeSpan duration)
        {
            _duration = duration;
            _currentTime = duration;
            _whenFinished = doNothing;
        }

        public void Reset() => _currentTime = _duration;

        double getPercentProgress()
        {
            TimeSpan msGone = _duration - _currentTime;

            double pc = msGone.TotalMilliseconds * 100f / _duration.TotalMilliseconds;

            return pc;
        }

        public float Progress => (float)(getPercentProgress() / 100f);

        public bool Finished => _isFinished;

        public static EggTimer Unset => new(TimeSpan.MaxValue);

        public void Run(CanvasTimingInformation timing)
        {
            if (_isFinished)
            {
                return;
            }

            if (_isPaused)
            {
                return;
            }

            _currentTime -= timing.ElapsedTime;

            if (_currentTime < TimeSpan.Zero)
            {
                _isFinished = true;
                _whenFinished();
            }
        }

        public void Pause() => _isPaused = true;

        public void Resume() => _isPaused = false;
    }
}

<|start_filename|>src/PacMan.GameComponents/KeyPressedEvent.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public class KeyPressedEvent
    {
        public Direction Direction = Direction.None;

        public double When = 0;
    }
}

<|start_filename|>src/PacMan.GameComponents/SoundName.cs<|end_filename|>
﻿namespace PacMan.GameComponents
{
    public enum SoundName
    {
        CoinInserted,
        CutScene,
        PacManDying,
        ExtraLife,
        Frightened,
        FruitEaten,
        GhostEaten,
        GhostEyes,
        Munch1,
        Munch2,
        PlayerStart,
        Siren1,
        Siren2,
        Siren3,
        Siren4,
        Siren5
    }
}

<|start_filename|>src/PacMan.GameComponents/GameEventArgs.cs<|end_filename|>
﻿using System;

namespace PacMan.GameComponents
{
    public class GameEventArgs : EventArgs
    {
    }
}

<|start_filename|>src/PacMan.GameComponents/DiagInfo.cs<|end_filename|>
﻿using System;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public static class DiagInfo
    {
        static float _lastTimestamp;
        static TimeSpan _lastTimeKeyboardChecked;

        public static bool ShouldShow { get; private set; }

        public static int Fps
        {
            get;
            set;
        }

        public static TimeSpan TotalTime { get; private set; }

        public static ValueTask Update(CanvasTimingInformation info, IHumanInterfaceParser input)
        {
            TotalTime = info.TotalTime;

            if (info.TotalTime - _lastTimeKeyboardChecked > .25f.Seconds())
            {
                _lastTimeKeyboardChecked = info.TotalTime;

                if (input.IsKeyCurrentlyDown(Keys.D))
                {
                    ShouldShow = !ShouldShow;
                }
            }

            if (input.WasKeyPressedAndReleased(Keys.A))
            {
                Constants.FramesPerSecond -= 5;
                Constants.FramesPerSecond = Math.Max(5, Constants.FramesPerSecond);
            }

            if (input.WasKeyPressedAndReleased(Keys.S))
            {
                Constants.FramesPerSecond += 5;
            }

            return default;
        }

        public static void IncrementUpdateCount() => ++UpdateCount;

        public static void IncrementDrawCount(float timestamp)
        {
            ++DrawCount;

            Elapsed = TimeSpan.FromMilliseconds(timestamp - _lastTimestamp);

            _lastTimestamp = timestamp;
        }

        public static int UpdateCount { get; private set; }

        public static int DrawCount { get; private set; }

        public static TimeSpan Elapsed { get; private set; }

        public static void UpdateTimeLoopTaken(in long elapsedMs)
        {
            if (elapsedMs > 17)
            {
                ++SlowElapsedCount;
            }

            GameLoopDurationMs = elapsedMs;

            MaxGameLoopDurationMs = Math.Max(MaxGameLoopDurationMs, GameLoopDurationMs);
        }

        public static int SlowElapsedCount { get; private set; }

        public static long GameLoopDurationMs { get; private set; }

        public static long MaxGameLoopDurationMs { get; private set; }
    }
}

<|start_filename|>src/PacMan.GameComponents/Requests/ClassThatLivesInGameComponents.cs<|end_filename|>
﻿namespace PacMan.GameComponents.Requests
{
    // ReSharper disable once UnusedType.Global
    public class ClassThatLivesInGameComponentsActsAsAMarkerForThisAssemblyForReflection
    {
    }
}

<|start_filename|>src/PacMan.GameComponents/TimerList.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public class TimerList
    {
        readonly List<EggTimer> _timers;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public TimerList() => _timers = new();

        public void Add(EggTimer timer) => _timers.Add(timer);

        public void Update(CanvasTimingInformation timing)
        {
            foreach (var s in _timers)
            {
                s.Run(timing);
            }

            for (var i = _timers.Count - 1; i >= 0; i--)
            {
                EggTimer eggTimer = _timers[i];

                if (eggTimer.Finished)
                {
                    _timers.RemoveAt(i);

                    break;
                }
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Ghosts/FramePair.cs<|end_filename|>
﻿using System.Numerics;
using JetBrains.Annotations;

namespace PacMan.GameComponents.Ghosts
{
    [CannotApplyEqualityOperator]
    public struct FramePair
    {
        public FramePair(Vector2 first, Vector2 second)
        {
            First = first;
            Second = second;
        }

        public Vector2 First { get; }

        public Vector2 Second { get; }
    }
}

<|start_filename|>src/PacMan.GameComponents/Events/DemoPlayerStartingEvent.cs<|end_filename|>
﻿using System.Threading;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;
using PacMan.GameComponents.GameActs;

namespace PacMan.GameComponents.Events
{
    /// <summary>
    /// A Player is starting.  We're told their index (0 or 1), their stats, whether it's the demo player,
    /// and their 'canvas' (their maze where each cell is cleared when eat pills).
    /// </summary>
    public readonly struct DemoPlayerStartingEvent : INotification
    {
        public class Handler : INotificationHandler<DemoPlayerStartingEvent>
        {
            readonly IGameSoundPlayer _gameSoundPlayer;
            readonly IGhostCollection _ghostCollection;
            readonly IFruit _fruit;
            readonly IMaze _maze;
            readonly IPacMan _pacman;
            readonly IGame _game;
            readonly IActs _acts;
            readonly IGameStats _gameStats;
            readonly IHaveTheMazeCanvases _mazeCanvases;

            public Handler(
                IGameSoundPlayer gameSoundPlayer,
                IGhostCollection ghostCollection,
                IFruit fruit,
                IMaze maze,
                IPacMan pacman,
                IGame game,
                IActs acts,
                IGameStats gameStats,
                IHaveTheMazeCanvases mazeCanvases)
            {
                _gameSoundPlayer = gameSoundPlayer;
                _ghostCollection = ghostCollection;
                _fruit = fruit;
                _maze = maze;
                _pacman = pacman;
                _game = game;
                _acts = acts;
                _gameStats = gameStats;
                _mazeCanvases = mazeCanvases;
            }

            public async Task Handle(DemoPlayerStartingEvent notification, CancellationToken cancellationToken)
            {
                var playerStats = _gameStats.CurrentPlayerStats;

                await _gameSoundPlayer.Disable();

                _ghostCollection.Ghosts.ForEach(g => g.Reset());

                _fruit.HandlePlayerStarted(playerStats, true);

                await _maze.HandlePlayerStarted(
                    playerStats,
                    _mazeCanvases.GetForPlayer(_gameStats.CurrentPlayerStats.PlayerIndex));

                await _pacman.HandlePlayerStarting(playerStats, true);

                var act = _acts.GetActNamed("DemoPlayerIntroAct");
                await act.Reset();

                _game.SetAct(act);
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/Spritesheet.cs<|end_filename|>
﻿using System;
using System.Drawing;
using Microsoft.AspNetCore.Components;

namespace PacMan.GameComponents
{
    public static class Spritesheet
    {
        static ElementReference? _reference;

        public static ElementReference Reference => _reference ?? throw new InvalidOperationException("Nothing set yet!");

        public static void SetReference(ElementReference reference)
        {
            _reference = reference;
        }

        public static Size Size => new(225, 248);
    }
}

<|start_filename|>src/PacMan.GameComponents/DistanceAndDirection.cs<|end_filename|>
﻿using JetBrains.Annotations;

namespace PacMan.GameComponents
{
    [CannotApplyEqualityOperator]
    public struct DistanceAndDirection
    {
        public DistanceAndDirection(
            float distance,
            Direction direction)
        {
            Distance = distance;
            Direction = direction;
        }

        public float Distance { get; }

        public Direction Direction { get; }
    }
}

<|start_filename|>src/PacMan.GameComponents/IGameStats.cs<|end_filename|>
﻿using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;

namespace PacMan.GameComponents
{
    public interface IGameStats
    {
        ValueTask Reset(int players);

        bool IsDemo { get; }

        bool HasPlayedIntroTune { get; set; }

        int HighScore { get; }

        int AmountOfPlayers { get; }

        bool AnyonePlaying { get; }

        bool IsGameOver { get; }

        PlayerStats CurrentPlayerStats { get; }

        bool AreAnyGhostsInEyeState { get; }

        void Update(CanvasTimingInformation timing);

        PlayerStats GetPlayerStats(int index);

        ValueTask FruitEaten();

        bool HasPlayerStats(int playerNumber);

        void ChoseNextPlayer();

        ValueTask PillEaten(CellIndex point);

        ValueTask PowerPillEaten(CellIndex point);

        void PacManEaten();

        ValueTask<int> GhostEaten();

        void LevelFinished();

        PlayerStats ResetForDemo();

        ValueTask HandleGhostBackInsideHouse();
    }
}

<|start_filename|>src/PacMan.GameComponents/Audio/GameSoundPlayer.cs<|end_filename|>
﻿// ReSharper disable HeapView.ObjectAllocation.Evident
#pragma warning disable 8618

using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents.Audio
{
    /// <summary>
    /// The sound player for game related events.
    /// </summary>
    public class GameSoundPlayer : IGameSoundPlayer
    {
        readonly ISoundLoader _loader;
        readonly IGameStats _gameStats;
        SoundEffect[] _sirens;
        SoundEffect _frightened;
        SoundEffect _ghostEyes;
        bool _loaded;
        bool _enabled;

        public GameSoundPlayer(ISoundLoader loader, IGameStats gameStats)
        {
            _loader = loader ?? throw new ArgumentNullException(nameof(loader));
            _gameStats = gameStats;
            _enabled = true;
        }

        public async ValueTask LoadAll(IJSRuntime runtime)
        {
            await _loader.LoadAll(runtime);

            _frightened = _loader.GetSoundEffect(SoundName.Frightened);
            _ghostEyes = _loader.GetSoundEffect(SoundName.GhostEyes);

            _sirens = new[]
            {
                _loader.GetSoundEffect(SoundName.Siren1),
                _loader.GetSoundEffect(SoundName.Siren2),
                _loader.GetSoundEffect(SoundName.Siren3),
                _loader.GetSoundEffect(SoundName.Siren4),
                _loader.GetSoundEffect(SoundName.Siren5)
            };

            _frightened.Loop();
            _ghostEyes.Loop();

            _sirens.ForEach(async s =>
            {
                s.Loop();
                await s.SetVolume(.5f);
            });

            _loaded = true;
        }

        public async ValueTask Reset()
        {
            _sirens.ForEach(async s => await s.Stop());
            await _ghostEyes.Stop();
            await _frightened.Stop();
        }

        public async ValueTask Update()
        {
            if (!_gameStats.AnyonePlaying)
            {
                return;
            }

            if (!_enabled)
            {
                return;
            }

            bool thereAreEyes = _gameStats.AreAnyGhostsInEyeState;

            PlayerStats playerStats = _gameStats.CurrentPlayerStats;

            await handleFright(playerStats, thereAreEyes);
            await handleSiren(playerStats.LevelStats.PillsEaten, thereAreEyes);
            await handleEyes(thereAreEyes);
        }

        async ValueTask handleFright(PlayerStats currentPlayerStats, bool thereAreEyes)
        {
            throwIfNotLoaded();

            if (thereAreEyes)
            {
                return;
            }

            GhostFrightSession? frightSession = currentPlayerStats.FrightSession;

            if (frightSession != null)
            {
                float volume = frightSession.IsFinished ? .5f : 0;

                _sirens.ForEach(async s => await s.SetVolume(volume));

                if (frightSession.IsFinished)
                {
                    await _frightened.Stop();
                }
            }
        }

        async ValueTask handleSiren(int pillsEaten, bool thereAreEyes)
        {
            throwIfNotLoaded();

            if (thereAreEyes)
            {
                return;
            }

            if (_loader.GetSoundEffect(SoundName.PlayerStart).IsPlaying)
            {
                return;
            }

            int level;

            if (pillsEaten < 117)
            {
                level = 0;
            }
            else if (pillsEaten < 180)
            {
                level = 1;
            }
            else if (pillsEaten < 212)
            {
                level = 2;
            }
            else if (pillsEaten < 230)
            {
                level = 3;
            }
            else
            {
                level = 4;
            }

            await playSiren(level);
        }

        async ValueTask playSiren(int level)
        {
            throwIfNotLoaded();

            for (int i = 0; i < _sirens.Length; i++)
            {
                SoundEffect siren = _sirens[i];

                if (i == level)
                {
                    await siren.Play();
                }
                else
                {
                    await siren.Stop();
                }
            }
        }

        async ValueTask handleEyes(bool thereAreEyes)
        {
            throwIfNotLoaded();

            if (thereAreEyes == false)
            {
                await _ghostEyes.Stop();
            }
            else
            {
                await _ghostEyes.Play();
            }
        }

        public async ValueTask Disable()
        {
            throwIfNotLoaded();

            if (!_enabled)
            {
                return;
            }

            _enabled = false;

            foreach (var s in _loader.AllSounds)
            {
                await s.Mute();
            }
        }

        public async ValueTask Enable()
        {
            throwIfNotLoaded();

            if (_enabled)
            {
                return;
            }

            _enabled = true;

            foreach (var s in _loader.AllSounds)
            {
                await s.Unmute();
            }
        }

        public async ValueTask PowerPillEaten()
        {
            throwIfNotLoaded();

            await play(SoundName.Frightened);
        }

        public async ValueTask FruitEaten()
        {
            throwIfNotLoaded();

            await play(SoundName.FruitEaten);
        }

        public async ValueTask GhostEaten()
        {
            throwIfNotLoaded();

            await play(SoundName.GhostEaten);
            await play(SoundName.GhostEyes);
        }

        public async ValueTask GotExtraLife()
        {
            throwIfNotLoaded();

            await play(SoundName.ExtraLife);
        }

        public async ValueTask CutScene()
        {
            throwIfNotLoaded();
            await play(SoundName.CutScene);
        }

        public async ValueTask PacManDying()
        {
            throwIfNotLoaded();
            await play(SoundName.PacManDying);
        }

        public async ValueTask PlayerStart()
        {
            throwIfNotLoaded();
            await play(SoundName.PlayerStart);
        }

        public async ValueTask CoinInserted()
        {
            throwIfNotLoaded();
            await play(SoundName.CoinInserted);

            // it might have been set in the demo mode
            await _frightened.Stop();
        }

        public async ValueTask Munch1()
        {
            await play(SoundName.Munch1);
        }

        public async ValueTask Munch2()
        {
            await play(SoundName.Munch2);
        }

        async ValueTask play(SoundName soundName)
        {
            throwIfNotLoaded(); SoundEffect audio = _loader.GetSoundEffect(soundName);

            await audio.Play();
        }

        public void MarkAsFinished(string name)
        {
            throwIfNotLoaded();
            bool parsed = Enum.TryParse(name, out SoundName val);

            if (!parsed)
            {
                throw new InvalidOperationException($"No such sound: {name}");
            }

            _loader.GetSoundEffect(val).MarkAsFinished();
        }

        void throwIfNotLoaded()
        {
            if (!_loaded)
            {
                throw new InvalidOperationException("sounds not loaded");
            }
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/GlobalDotCounter.cs<|end_filename|>
﻿using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents
{
    public class GlobalDotCounter : DotCounter
    {
        bool _finished;
        GhostNickname? _nextOneToForceOut;
        GhostNickname? _lastOneForcedOut;

        public GlobalDotCounter(int limit = 0) : base(limit, "GLOBAL")
        {
        }

        public virtual void Reset()
        {
            _finished = false;
            Counter = 0;
        }

        // the thing that calls this switches dot counters if all the ghosts are out.
        // this never finishes if Clyde is out the house when the counter reaches
        // 32 - this mimics the bug in the arcade game
        public override bool IsFinished => _finished;

        public override void SetTimedOut() =>
            _nextOneToForceOut = _lastOneForcedOut switch 
            {
                null => GhostNickname.Pinky,
                GhostNickname.Clyde => GhostNickname.Pinky,
                GhostNickname.Pinky => GhostNickname.Inky,
                GhostNickname.Inky => GhostNickname.Clyde,
                _ => _nextOneToForceOut
            };

        public bool CanGhostLeave(GhostNickname nickName)
        {
            if (_nextOneToForceOut == nickName)
            {
                _nextOneToForceOut = null;
                _lastOneForcedOut = nickName;
                return true;
            }

            bool canLeave = false;

            switch (nickName)
            {
                case GhostNickname.Pinky when Counter == 7:
                    canLeave = true;
                    break;
                case GhostNickname.Inky when Counter == 17:
                    canLeave = true;
                    break;
                case GhostNickname.Clyde when Counter == 32:
                    canLeave = true;
                    
                    // debt: SD: confusing!
                    _finished = Counter == 32;
                    break;
            }

            return canLeave;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/AttractAct.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using MediatR;
using PacMan.GameComponents.Audio;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Events;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents.GameActs
{
    /// Show's the attract screen (ghost names and pictures and the 'chase' sub act).  Transitions to either
    /// the 'player intro' act (to start the demo mode if nothing's was pressed/clicked/touched),
    /// or the 'start button' act if a coin was 'inserted'.
    public class AttractAct : IAct
    {
        readonly ICoinBox _coinBox;
        readonly IMediator _mediator;
        readonly IHumanInterfaceParser _input;
        readonly IGameSoundPlayer _gameSoundPlayer;
        readonly Marquee _marquee;
        readonly GeneralSprite _pacmanLogo;

        struct Instruction
        {
            public TimeSpan When;
            public SimpleGhost Ghost;
            public string Text;
            public Vector2 Where;
            public Color Color;
        }

        readonly SimpleGhost _blinky;
        readonly SimpleGhost _pinky;
        readonly SimpleGhost _inky;
        readonly SimpleGhost _clyde;

        readonly List<Instruction> _instructions;

        readonly object _lock;
        readonly BlazorLogo _blazorLogo;
        readonly TimeSpan _chaseSubActReadyAt;

        ChaseSubAct _chaseSubAct;
        TimeSpan _startTime;
        TimeSpan _drawUpTo;
        bool _chaseSubActReady;

        bool _finished;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public AttractAct(
            ICoinBox coinBox,
            IMediator mediator,
            IHumanInterfaceParser input,
            IGameSoundPlayer gameSoundPlayer)
        {
            MarqueeText[] texts =
            {
                new() {
                    Text = "tap/space - 1 player",
                    YPosition = 195,
                    TimeIdle = 1.Seconds(),
                    TimeIn = 2.Seconds(),
                    TimeStationary = 2.Seconds(),
                    TimeOut = 1.Seconds()
                },
                new() {
                    Text = "long press/2 - 2 players",
                    YPosition = 195,
                    TimeIdle = 0.Seconds(),
                    TimeIn = 2.Seconds(),
                    TimeStationary = 2.Seconds(),
                    TimeOut = 1.Seconds()
                }
            };

            _marquee = new(texts);
            _coinBox = coinBox;
            _mediator = mediator;
            _input = input;
            _gameSoundPlayer = gameSoundPlayer;
            _pacmanLogo =
                new(new(192, 25), new(36, 152), Vector2.Zero, new(456, 173));
            _blazorLogo = new();

            _instructions = new();

            _startTime = TimeSpan.MinValue;

            _blinky = new(GhostNickname.Blinky, Direction.Right);
            _pinky = new(GhostNickname.Pinky, Direction.Right);
            _inky = new(GhostNickname.Inky, Direction.Right);
            _clyde = new(GhostNickname.Clyde, Direction.Right);
            _startTime = TimeSpan.MinValue;
            _chaseSubActReadyAt = 9.Seconds();

            _chaseSubAct = new();
            _lock = new();
        }

        public string Name { get; } = "AttractAct";

        public ValueTask Reset()
        {
            _startTime = TimeSpan.MinValue;
            _finished = false;
            _instructions.Clear();
            _chaseSubAct = new();
            populateDelayedInstructions();

            return default;
        }

        public async ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing)
        {
            await _blazorLogo.Update(timing);

            await _marquee.Update(timing);

            if (_startTime == TimeSpan.MinValue)
            {
                _startTime = timing.TotalTime;
            }

            _drawUpTo = timing.TotalTime - _startTime;

            if (_input.WasKeyPressedAndReleased(Keys.Left))
            {
                await startDemoGame();
                return ActUpdateResult.Running;
            }

            if (_input.WasKeyPressedAndReleased(Keys.Five))
            {
                await _gameSoundPlayer.Enable();
                await _mediator.Publish(new CoinInsertedEvent());

                return ActUpdateResult.Running;
            }

            if (_input.WasKeyPressedAndReleased(Keys.Space) ||
                _input.WasKeyPressedAndReleased(Keys.One) ||
                _input.WasTapped)
            {
                await _gameSoundPlayer.Enable();
                _coinBox.CoinInserted();
                await _mediator.Publish(new NewGameEvent(1));

                return ActUpdateResult.Running;
            }

            if (_input.WasKeyPressedAndReleased(Keys.Two) || _input.WasLongPress)
            {
                _coinBox.CoinInserted();

                _coinBox.CoinInserted();
                _coinBox.CoinInserted();

                await _mediator.Publish(new NewGameEvent(2));

                return ActUpdateResult.Running;
            }

            _chaseSubActReady = timing.TotalTime - _startTime >= _chaseSubActReadyAt;

            if (_chaseSubActReady)
            {
                if (await _chaseSubAct.Update(timing) == ActUpdateResult.Finished)
                {
                    if (!_finished)
                    {
                        await startDemoGame();
                        _finished = true;
                    }

                    return ActUpdateResult.Finished;
                }
            }

            return ActUpdateResult.Running;
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            for (int i = 0; i < _instructions.Count; i++)
            {
                var inst = _instructions[i];

                if (inst.When > _drawUpTo)
                {
                    break;
                }

                var ghost = inst.Ghost;

                if (ghost != null)
                {
                    ghost.Position = inst.Where;

                    await session.DrawSprite(ghost, Spritesheet.Reference);
                }
                else
                {
                    await drawText(session, inst.Text, inst.Where, inst.Color);
                }
            }

            if (_chaseSubActReady)
            {
                await _chaseSubAct.Draw(session);
            }

            await _marquee.Draw(session);

            await session.SetGlobalAlphaAsync(.75f);
            await session.DrawSprite(_pacmanLogo, Spritesheet.Reference);
            await session.SetGlobalAlphaAsync(1f);

            await _blazorLogo.Draw(session);
        }

        void populateDelayedInstructions()
        {
            lock (_lock)
            {
                TimeSpan clock = 1500.Milliseconds();

                _instructions.Add(new() {
                    When = clock,
                    Where = new(32, 12),
                    Text = "CHARACTER / NICKNAME",
                    Color = Color.White
                });

                var gap = new Vector2(0, 24);

                var pos = new Vector2(16, 30);

                var timeForEachOne = 600.Milliseconds();

                writeInstructionsForGhost(ref clock, _blinky, Colors.Red, "SHADOW", "BLINKY", pos);

                clock += timeForEachOne;
                pos += gap;
                writeInstructionsForGhost(ref clock, _pinky, Colors.Pink, "SPEEDY", "PINKY", pos);

                clock += timeForEachOne;
                pos += gap;
                writeInstructionsForGhost(ref clock, _inky, Colors.Cyan, "BASHFUL", "INKY", pos);

                clock += timeForEachOne;
                pos += gap;
                writeInstructionsForGhost(ref clock, _clyde, Colors.Yellow, "POKEY", "CLYDE", pos);
            }
        }

        static ValueTask drawText(CanvasWrapper canvasWrapper, string text, Vector2 point, Color color)
        {
            return canvasWrapper.DrawMyText(text, point, color);
        }

        void writeInstructionsForGhost(
            ref TimeSpan clock,
            SimpleGhost ghost,
            Color color,
            string name,
            string nickname,
            Vector2 point)
        {
            _instructions.Add(new() {
                Ghost = ghost,
                When = clock,
                Where = point
            });

            point += new Vector2(18, -4);

            clock += 1.Seconds();

            _instructions.Add(new() {
                Where = point,
                Text = $@" - {name}",
                When = clock,
                Color = color
            });

            point += new Vector2(90, 0);

            clock += 500.Milliseconds();

            _instructions.Add(new() {
                Where = point,
                Text = $@"""{nickname}""",
                When = clock,
                Color = color
            });
        }

        async ValueTask startDemoGame() => await _mediator.Publish(new DemoStartedEvent());
    }
}

<|start_filename|>src/PacMan.GameComponents/GameActs/ChaseSubAct.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Threading.Tasks;
using PacMan.GameComponents.Canvas;
using PacMan.GameComponents.Ghosts;

namespace PacMan.GameComponents.GameActs
{
    public class ChaseSubAct
    {
        readonly TimerList _tempTimers;

        readonly List<AttractGhost> _ghosts;

        readonly AttractScenePacMan _pacMan;

        readonly PowerPill _powerPillToEat;
        readonly PowerPill _powerPillLegend;
        readonly Pill _pillLegend;

        readonly Dictionary<GhostNickname, StartAndEndPos> _ghostPositions;
        readonly TimedSpriteList _tempSprites;

        bool _legendVisible;
        bool _copyrightVisible;
        bool _ghostsChasing;

        int _ghostScore;
        StartAndEndPos _pacPositions;

        EggTimer _ghostTimer;
        EggTimer _pacTimer;

        EggTimer _ghostEatenTimer;

        bool _finished;

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        public ChaseSubAct()
        {
            _finished = false;
            _tempTimers = new();

            _ghostScore = 200;

            _tempSprites = new();
            _ghosts = new();

            var justOffScreen = new Vector2(250, 140);

            _ghostEatenTimer = new(0.Milliseconds(), static () => { });
            _ghostTimer = new(5.Seconds(), async () => await reverseChase());
            _pacTimer = new(5.1f.Seconds(), static () => { });

            _powerPillToEat = new() {
                Visible = false,
                Position = new(30, justOffScreen.Y - 4)
            };

            _pillLegend = new() { Position = new(70, 178) };

            _powerPillLegend = new() { Position = new(66, 182) };

            _pacMan = new() { Direction = Direction.Left };

            var blinky = new AttractGhost(GhostNickname.Blinky, Direction.Left);
            var pinky = new AttractGhost(GhostNickname.Pinky, Direction.Left);
            var inky = new AttractGhost(GhostNickname.Inky, Direction.Left);
            var clyde = new AttractGhost(GhostNickname.Clyde, Direction.Left);

            _ghosts.Add(blinky);
            _ghosts.Add(pinky);
            _ghosts.Add(inky);
            _ghosts.Add(clyde);

            var gap = new Vector2(16, 0);

            _pacPositions = new(justOffScreen, new(30, justOffScreen.Y));
            _pacMan.Position = _pacPositions.Start;

            _ghostPositions = new();

            var startPos = justOffScreen + new Vector2(50, 0);
            var endPos = new Vector2(50, justOffScreen.Y);

            blinky.Position = startPos;
            _ghostPositions[blinky.NickName] = new(startPos, endPos);

            startPos = startPos + gap;
            endPos = endPos + gap;
            pinky.Position = startPos;
            _ghostPositions[pinky.NickName] = new(startPos, endPos);

            startPos = startPos + gap;
            endPos = endPos + gap;
            inky.Position = startPos;
            _ghostPositions[inky.NickName] = new(startPos, endPos);

            startPos = startPos + gap;
            endPos = endPos + gap;
            clyde.Position = startPos;
            _ghostPositions[clyde.NickName] = new(startPos, endPos);

            _tempTimers.Add(new(1.Seconds(), () => _legendVisible = true));
            _tempTimers.Add(new(3.Seconds(), () =>
            {
                _powerPillToEat.Visible = true;
                _copyrightVisible = true;
            }));

            _tempTimers.Add(new(4500.Milliseconds(), () =>
            {
                _ghostsChasing = true;
            }));
        }

        public async ValueTask<ActUpdateResult> Update(CanvasTimingInformation timing)
        {
            if (_finished)
            {
                return ActUpdateResult.Finished;
            }

            _tempTimers.Update(timing);

            if (_ghostsChasing)
            {
                await _powerPillLegend.Update(timing);
                await _powerPillToEat.Update(timing);
                _ghostTimer.Run(timing);
                _pacTimer.Run(timing);
                _ghostEatenTimer.Run(timing);
            }

            await _pillLegend.Update(timing);

            _tempSprites.Update(timing);

            lerpPacMan();

            foreach (var g in _ghosts)
            {
                if (!g.Alive)
                {
                    continue;
                }

                lerpGhost(g);

                await g.Update(timing);

                if (Vector2s.AreNear(_pacMan.Position, g.Position, 2))
                {
                    ghostEaten(g);

                    if (g.NickName == GhostNickname.Clyde)
                    {
                        _tempTimers.Add(new(1.Seconds(), () =>
                        {
                            _finished = true;
                        }));
                    }
                }
            }

            await _pacMan.Update(timing);

            return ActUpdateResult.Running;
        }

        public async ValueTask Draw(CanvasWrapper session)
        {
            await _powerPillToEat.Draw(session);

            if (_legendVisible)
            {
                await _powerPillLegend.Draw(session);
                await _pillLegend.Draw(session);

                await session.DrawMyText("10 pts", new(80, 170), Colors.White);
                await session.DrawMyText("50 pts", new(80, 180), Colors.White);
            }

            if (_copyrightVisible)
            {
                await session.DrawMyText("© 1980 midway mfg. co.", new(10, 220), Colors.White);
            }

            await _tempSprites.Draw(session);

            foreach (var eachGhost in _ghosts)
            {
                if (eachGhost.Alive)
                {
                    await eachGhost.Draw(session);
                }
            }

            await _pacMan.Draw(session);

            var gp = _ghosts[0].Position;

            await session.DrawText("STEVE DUNN 2020", new((int)gp.X + 2, (int)(gp.Y + 22)), Color.Black);
            await session.DrawText("STEVE DUNN 2020", new((int)gp.X, (int)(gp.Y + 20)), Color.Yellow);
        }

        void ghostEaten(AttractGhost ghost)
        {
            ghost.Alive = false;

            _pacMan.Visible = false;

            _ghostTimer.Pause();
            _pacTimer.Pause();

            showScore(ghost.Position, _ghostScore);

            _ghostScore *= 2;

            // ReSharper disable once HeapView.ObjectAllocation.Evident
            _ghostEatenTimer = new(1.Seconds(), () =>
            {
                _ghostTimer.Resume();
                _pacTimer.Resume();
                _pacMan.Visible = true;
            });
        }

        void showScore(Vector2 pos, int amount) =>
            _tempSprites.Add(new(900, new ScoreSprite(pos, amount)));

        void lerpGhost(AttractGhost ghost)
        {
            StartAndEndPos positions = _ghostPositions[ghost.NickName];

            ghost.Position = Vector2s.Lerp(positions.Start, positions.End, _ghostTimer.Progress);
        }

        void lerpPacMan()
        {
            _pacMan.Position = Vector2s.Lerp(_pacPositions.Start, _pacPositions.End, _pacTimer.Progress);
        }

        [SuppressMessage("ReSharper", "HeapView.ObjectAllocation.Evident")]
        ValueTask reverseChase()
        {
            _powerPillToEat.Visible = false;
            _ghostTimer = new(12.Seconds(), () => { });
            _pacTimer = new(6.Seconds(), () => { });

            var levelStats = new LevelStats(0);
            var frightSessions = new GhostFrightSession(levelStats.GetLevelProps());

            foreach (var eachGhost in _ghosts)
            {
                eachGhost.Direction.Update(Direction.Right);
                eachGhost.SetFrightSession(frightSessions);
                eachGhost.SetFrightened();

                _ghostPositions[eachGhost.NickName] = _ghostPositions[eachGhost.NickName].Reverse();
            }

            _pacPositions = _pacPositions.Reverse();

            _pacMan.Direction = Direction.Right;

            return default;
        }
    }
}

<|start_filename|>src/PacMan.GameComponents/ScoreSprite.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Numerics;

namespace PacMan.GameComponents
{
    public class ScoreSprite : GeneralSprite
    {
        static readonly Dictionary<int, (Vector2 Pos, int Width)> _scorePositions =
            new()
            {
                { 100, (new(456, 148), 15) },
                { 200, (new(457, 133), 15) },
                { 300, (new(473, 148), 15) },
                { 400, (new(473, 133), 15) },
                { 500, (new(489, 148), 15) },
                { 700, (new(505, 148), 15) },
                { 800, (new(489, 133), 15) },
                { 1000, (new(521, 148), 21) },
                { 1600, (new(505, 133), 16) },
                { 2000, (new(518, 164), 21) },
                { 3000, (new(518, 180), 21) },
                { 5000, (new(518, 196), 21) }
            };

        public ScoreSprite(Vector2 position, int amount) : base(
            position,
            new(_scorePositions[amount].Width, 7),
            new(_scorePositions[amount].Width / 2f, 7 / 2f),
            _scorePositions[amount].Pos)
        {
        }
    }
}