using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; using Snake.Core; namespace Snake.Avalonia.Views; public partial class GameView : UserControl { private const int BoardWidth = 20; private const int BoardHeight = 15; private const int BaseTickMs = 120; private SnakeGame _game = new(BoardWidth, BoardHeight); private readonly DispatcherTimer _gameTimer; private readonly DispatcherTimer _renderTimer; private DateTime _lastTickTime; private DateTime _lastRenderTime; private bool _isRunning; private bool _isPaused; private int _highScore; // Score popup state private int _scorePopupValue; private double _scorePopupElapsed; private Position _scorePopupPosition; public GameView() { InitializeComponent(); // Load high score _highScore = HighScoreManager.Load(); // Game timer — ticks the game logic _gameTimer = new DispatcherTimer(); _gameTimer.Tick += OnGameTick; // Render timer — 60fps smooth animation _renderTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; _renderTimer.Tick += OnRenderTick; // Button handlers GameOverRestartBtn.Click += (_, _) => RestartGame(); GameOverQuitBtn.Click += (_, _) => CloseWindow(); // Keyboard KeyDown += OnGlobalKeyDown; // Focus on click PointerPressed += (_, _) => Focus(); // Initial HUD UpdateHud(); // Start render loop immediately (for idle animation) _lastRenderTime = DateTime.UtcNow; _renderTimer.Start(); } private void OnGlobalKeyDown(object? sender, KeyEventArgs e) { var key = e.Key; // Quit if (key == Key.Escape) { CloseWindow(); return; } // Restart if (key == Key.R) { RestartGame(); return; } // Pause/Resume if (key == Key.Space || key == Key.P) { TogglePause(); return; } // Start game if (key == Key.Enter && (!_isRunning || _game.IsGameOver)) { if (_game.IsGameOver) ResetGame(); StartGame(); return; } // Direction input — also starts game if not running var dir = KeyToDirection(key); if (dir.HasValue) { if (!_isRunning && !_game.IsGameOver) { StartGame(); } if (_isRunning && !_game.IsGameOver && !_isPaused) { _game.SetDirection(dir.Value); } } e.Handled = true; } private static Direction? KeyToDirection(Key key) => key switch { Key.Up or Key.W => Direction.Up, Key.Down or Key.S => Direction.Down, Key.Left or Key.A => Direction.Left, Key.Right or Key.D => Direction.Right, _ => null }; private void OnGameTick(object? sender, EventArgs e) { if (!_isRunning || _game.IsGameOver || _isPaused) return; _lastTickTime = DateTime.UtcNow; var result = _game.Tick(); if (_game.IsGameOver) { _isRunning = false; _gameTimer.Stop(); // Save high score if (_game.Score > _highScore) { _highScore = _game.Score; HighScoreManager.Save(_highScore); } ShowGameOver(); } else { // Update game speed based on level var interval = SnakeGame.GetTickIntervalMs(_game.Score, BaseTickMs); var newInterval = TimeSpan.FromMilliseconds(interval); if (_gameTimer.Interval != newInterval) _gameTimer.Interval = newInterval; // Food eaten popup if (result == GameTickResult.AteFood) { _scorePopupValue = _game.Score; _scorePopupElapsed = 0; _scorePopupPosition = _game.Food.Position; } } UpdateHud(); } private void OnRenderTick(object? sender, EventArgs e) { var now = DateTime.UtcNow; _lastRenderTime = now; if (_isRunning && !_isPaused && !_game.IsGameOver) { // Calculate interpolation T var elapsedSinceTick = (now - _lastTickTime).TotalMilliseconds; var tickInterval = SnakeGame.GetTickIntervalMs(_game.Score, BaseTickMs); var t = Math.Clamp(elapsedSinceTick / tickInterval, 0.0, 1.0); SnakeCanvas.InterpolationT = t; } else { // Keep t frozen when paused/game over SnakeCanvas.InterpolationT = Math.Clamp(SnakeCanvas.InterpolationT, 0.0, 1.0); } // Update score popup timer if (_scorePopupElapsed < 1.5) { _scorePopupElapsed += 0.016; // ~60fps } // Update snake state for renderer SnakeCanvas.Segments = _game.Snake.Segments.ToList(); SnakeCanvas.PreviousSegments = _game.Snake.PreviousSegments; SnakeCanvas.SnakeDirection = _game.Snake.Direction; SnakeCanvas.FoodPosition = _game.Food.Position; SnakeCanvas.TotalElapsedSeconds += 0.016; // Score popup SnakeCanvas.ScorePopup = _scorePopupValue; SnakeCanvas.ScorePopupElapsed = _scorePopupElapsed; SnakeCanvas.ScorePopupPosition = _scorePopupPosition; // Trigger redraw SnakeCanvas.InvalidateVisual(); } private void StartGame() { _isRunning = true; _isPaused = false; var interval = SnakeGame.GetTickIntervalMs(_game.Score, BaseTickMs); _gameTimer.Interval = TimeSpan.FromMilliseconds(interval); _lastTickTime = DateTime.UtcNow; _gameTimer.Start(); StartOverlay.IsVisible = false; PauseOverlay.IsVisible = false; GameOverOverlay.IsVisible = false; UpdateHud(); Focus(); } private void TogglePause() { if (!_isRunning || _game.IsGameOver) return; _isPaused = !_isPaused; if (_isPaused) { _gameTimer.Stop(); PauseOverlay.IsVisible = true; } else { _lastTickTime = DateTime.UtcNow; _gameTimer.Start(); PauseOverlay.IsVisible = false; } UpdateHud(); } private void RestartGame() { ResetGame(); StartGame(); } private void ResetGame() { _isRunning = false; _isPaused = false; _gameTimer.Stop(); _game = new SnakeGame(BoardWidth, BoardHeight); _lastTickTime = DateTime.UtcNow; _scorePopupValue = 0; _scorePopupElapsed = 0; StartOverlay.IsVisible = false; PauseOverlay.IsVisible = false; GameOverOverlay.IsVisible = false; SnakeCanvas.InterpolationT = 0; SnakeCanvas.TotalElapsedSeconds = 0; UpdateHud(); } private void ShowGameOver() { GameOverOverlay.IsVisible = true; GameOverScoreText.Text = $"Score: {_game.Score}"; GameOverHighScoreText.Text = _game.Score >= _highScore ? "🏆 NEW HIGH SCORE!" : $"High Score: {_highScore}"; } private void CloseWindow() { if (VisualRoot is Window window) window.Close(); } private void UpdateHud() { ScoreText.Text = $"{_game.Score}"; LevelText.Text = $"Level {_game.Level}"; HighScoreText.Text = $"{_highScore}"; // Speed bar width proportional to level var maxLevel = 15; var levelFraction = Math.Min((double)_game.Level / maxLevel, 1.0); SpeedBar.Width = 4 + levelFraction * 52; // 4-56px range } }