318 lines
11 KiB
C#
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();
|
|
}
|
|
}
|
|
} |