Files
2026-07-13 12:26:32 +08:00

318 lines
11 KiB
C#

// 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<Road> roads = [];
private readonly Dictionary<string, Bitmap> catIcons = [];
private readonly Dictionary<string, Bitmap> 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<int>()
.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<Road>();
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<int> list1, List<int> list2)
{
return list1.Intersect(list2).Any();
}
}
}