// Copyright 2025 Takuto Nakamura // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using RunCat365.Properties; using System.Collections; using System.Globalization; using System.Text.RegularExpressions; using FormsTimer = System.Windows.Forms.Timer; namespace RunCat365 { internal class EndlessGameForm : Form { private const int JUMP_THRESHOLD = 17; private readonly FormsTimer timer; private readonly Theme systemTheme; private GameStatus status = GameStatus.NewGame; private Cat cat = new Cat.Running(Cat.Running.Frame.Frame0); private readonly List roads = []; private readonly Dictionary catIcons = []; private readonly Dictionary roadIcons = []; private int counter = 0; private int limit = 5; private int score = 0; private int highScore = UserSettings.Default.HighScore; private bool isJumpRequested = false; private readonly bool isAutoPlay = false; internal EndlessGameForm(Theme systemTheme) { this.systemTheme = systemTheme; DoubleBuffered = true; ClientSize = new Size(600, 250); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; StartPosition = FormStartPosition.CenterScreen; Text = Strings.Window_EndlessGame; Icon = Resources.AppIcon; BackColor = systemTheme == Theme.Light ? Color.Gainsboro : Color.Gray; var rm = Resources.ResourceManager; var rs = rm.GetResourceSet(CultureInfo.CurrentUICulture, true, true); if (rs is not null) { var catRegex = new Regex(@"^cat_.*_.*$"); var color = systemTheme.GetContrastColor(); foreach (DictionaryEntry entry in rs) { var key = entry.Key.ToString(); if (string.IsNullOrEmpty(key)) continue; if (catRegex.IsMatch(key)) { if (entry.Value is Bitmap icon) { catIcons.Add(key, systemTheme == Theme.Light ? new Bitmap(icon) : icon.Recolor(color)); } } else if (key.StartsWith("road")) { if (entry.Value is Bitmap icon) { roadIcons.Add(key, systemTheme == Theme.Light ? new Bitmap(icon) : icon.Recolor(color)); } } } } Paint += RenderScene; KeyDown += HandleKeyDown; timer = new FormsTimer { Interval = 100 }; timer.Tick += GameTick; Initialize(); timer.Start(); } protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); timer.Stop(); timer.Dispose(); foreach (var bitmap in catIcons.Values) bitmap.Dispose(); foreach (var bitmap in roadIcons.Values) bitmap.Dispose(); catIcons.Clear(); roadIcons.Clear(); } private void Initialize() { counter = JUMP_THRESHOLD; isJumpRequested = false; score = 0; cat = new Cat.Running(Cat.Running.Frame.Frame0); roads.RemoveAll(r => r == Road.Sprout); Enumerable.Range(0, 20 - roads.Count).ToList().ForEach( _ => roads.Add((Road)(new Random().Next(0, 3))) ); } private bool Judge() { if (status != GameStatus.Playing) return false; var sproutIndices = roads .Select((road, index) => { return road == Road.Sprout ? index : (int?)null; }) .OfType() .ToList(); if (cat.ViolationIndices().HasCommonElements(sproutIndices)) { status = GameStatus.GameOver; return false; } else { return true; } } private void UpdateRoads() { var firstRoad = roads.First(); roads.RemoveAt(0); if (firstRoad == Road.Sprout) { score = Math.Min(score + 1, 999); highScore = Math.Max(score, highScore); } counter = counter > 0 ? counter - 1 : limit - 1; if (counter == 0) { var randomValue = new Random().Next(0, 27); var subRoads = new List(); if (randomValue % 3 == 0) { subRoads.Add(Road.Sprout); } if (randomValue % 9 == 0) { subRoads.Add(Road.Sprout); } if (randomValue % 27 == 0) { subRoads.Add(Road.Sprout); } roads.AddRange(subRoads); limit = subRoads.Count == 0 ? 5 : 10; } if (roads.Count < 20) { roads.Add((Road)(new Random().Next(0, 3))); } } private void UpdateCat() { if (cat is Cat.Running runningCat) { if (runningCat.CurrentFrame == Cat.Running.Frame.Frame4 && isJumpRequested) { cat = new Cat.Jumping(Cat.Jumping.Frame.Frame0); isJumpRequested = false; return; } } else if (cat is Cat.Jumping jumpingCat) { if (jumpingCat.CurrentFrame == Cat.Jumping.Frame.Frame9) { if (isJumpRequested) { cat = cat.Next(); isJumpRequested = false; return; } else { cat = new Cat.Running(Cat.Running.Frame.Frame0); return; } } } cat = cat.Next(); } private void AutoJump() { if (isAutoPlay && roads[JUMP_THRESHOLD - 1] == Road.Sprout) { isJumpRequested = true; } } private void GameTick(object? sender, EventArgs e) { if (Judge()) { UpdateRoads(); UpdateCat(); AutoJump(); } Invalidate(); } private void HandleKeyDown(object? sender, KeyEventArgs e) { if (e.KeyCode == Keys.Space) { switch (status) { case GameStatus.NewGame: case GameStatus.GameOver: Initialize(); status = GameStatus.Playing; break; case GameStatus.Playing when !isAutoPlay: isJumpRequested = true; break; default: break; } } } private void RenderScene(object? sender, PaintEventArgs e) { var rm = Resources.ResourceManager; var textColor = systemTheme.GetContrastColor(); var g = e.Graphics; using (Font font15 = new("Consolas", 15)) using (Brush brush = new SolidBrush(textColor)) { var stringFormat = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center }; var scoreText = status == GameStatus.GameOver ? $"HI {highScore:D3} {score:D3}" : $"{score:D3}"; g.DrawString(scoreText, font15, brush, new Rectangle(20, 0, 560, 50), stringFormat); } roads.Take(20).Select((road, index) => new { road, index }).ToList().ForEach( item => { var fileName = $"road_{item.road.GetString()}".ToLower(); if (!roadIcons.TryGetValue(fileName, out Bitmap? image)) return; g.DrawImage(image, new Rectangle(item.index * 30, 200, 30, 50)); } ); var fileName = $"cat_{cat.GetString()}".ToLower(); if (!catIcons.TryGetValue(fileName, out Bitmap? image)) return; g.DrawImage(image, new Rectangle(120, 130, 120, 100)); if (status != GameStatus.Playing) { using Brush fillBrush = new SolidBrush(Color.FromArgb(77, 0, 0, 0)); g.FillRectangle(fillBrush, new Rectangle(0, 0, 600, 250)); using Font font18 = new("Segoe UI", 16, FontStyle.Bold); using Brush brush = new SolidBrush(textColor); var message = Strings.Game_PressSpaceToPlay; if (status == GameStatus.GameOver) { if (score >= highScore) { SaveRecord(score); message = $"{Strings.Game_NewRecord}!!\n{message}"; } else { message = $"{Strings.Game_GameOver}\n{message}"; } } var stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; g.DrawString(message, font18, brush, new Rectangle(0, 0, 600, 250), stringFormat); } } private void SaveRecord(int score) { UserSettings.Default.HighScore = score; UserSettings.Default.Save(); } } internal static class ListExtension { internal static bool HasCommonElements(this List list1, List list2) { return list1.Intersect(list2).Any(); } } }