Finished BonusTicTacToe

This commit is contained in:
That_One_Nerd 2024-09-24 11:53:56 -04:00
parent 884f51108f
commit 0014bd0af6
23 changed files with 1305 additions and 0 deletions

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34309.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BonusTicTacToe", "BonusTicTacToe\BonusTicTacToe.csproj", "{B21DDB60-F0D6-41DE-8FDF-AA26DC85A993}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B21DDB60-F0D6-41DE-8FDF-AA26DC85A993}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B21DDB60-F0D6-41DE-8FDF-AA26DC85A993}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B21DDB60-F0D6-41DE-8FDF-AA26DC85A993}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B21DDB60-F0D6-41DE-8FDF-AA26DC85A993}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FCDC64AF-9F7E-47B8-B223-B9CDEB525AB2}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Update="Resources\Fonts.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Fonts.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Fonts.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Fonts.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Compile Update="MainForm.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
namespace BonusTicTacToe;
public enum Direction
{
LeftRight,
UpDown,
DiagonalTopLeftBottomRight,
DiagonalTopRightBottomLeft,
}

View File

@ -0,0 +1,218 @@
using System.Text.Json;
namespace BonusTicTacToe;
public class Game
{
public const string GamePath = "./game.json";
public static readonly bool SaveToFile = false;
public static Game Defaults => new()
{
TilesX = 3,
TilesY = 3,
Players = 2,
ConnectToWin = 3,
MovesPerTurn = 1,
WinsToFinish = 1,
};
public required int TilesX { get; set; }
public required int TilesY { get; set; }
public required int Players { get; set; }
public required int ConnectToWin { get; set; }
public required int MovesPerTurn { get; set; }
public required int WinsToFinish { get; set; }
public int CurrentTurn { get; set; } = 0;
public byte[] Board
{
// We can't create the array in the constructor
// because setting required properties happens
// after that step.
get => _board ??= new byte[TilesX * TilesY];
set => _board = value;
}
public byte HasFinished { get; set; } = 0;
public bool IsDraw { get; set; } = false;
public int TotalMoves { get; set; } = 0;
private byte[]? _board = null;
public byte this[int x, int y]
{
get => Board[y * TilesX + x];
set => Board[y * TilesX + x] = value;
}
public static Game Load()
{
if (!SaveToFile) return Defaults;
Game game;
if (File.Exists(GamePath))
{
FileStream fs = new(GamePath, FileMode.Open);
game = JsonSerializer.Deserialize<Game>(fs)!;
fs.Close();
// Game is already completed.
if (game.HasFinished != 0 || game.IsDraw) game = Defaults;
}
else
{
game = Defaults;
game.Save();
}
return game;
}
public void Save()
{
if (!SaveToFile) return;
FileStream fs = new(GamePath, FileMode.Create);
JsonSerializer.Serialize(fs, this);
fs.Close();
}
public List<WinObject> GetWins()
{
// Check all tiles for three/four/... of a kind either
// left-right, top-bottom, or right/left diagonals.
List<WinObject> wins = [];
for (int x1 = 0; x1 < TilesX; x1++)
{
for (int y1 = 0; y1 < TilesY; y1++)
{
byte value1 = this[x1, y1];
if (value1 == 0) continue;
int inARow = 1;
// Leftwards and rightwards.
int minX = x1;
int maxX = x1;
for (int x2 = x1 - 1; x2 >= 0; x2--)
{
byte value2 = this[x2, y1];
if (value1 != value2) break;
inARow++;
minX = x2;
}
for (int x2 = x1 + 1; x2 < TilesX; x2++)
{
byte value2 = this[x2, y1];
if (value1 != value2) break;
inARow++;
maxX = x2;
}
if (inARow >= ConnectToWin) tryAdd(new()
{
player = (byte)(value1 - 1),
direction = Direction.LeftRight,
minX = minX,
maxX = maxX,
minY = y1,
maxY = y1
});
// Upwards and downwards.
int minY = y1;
int maxY = y1;
inARow = 1;
for (int y2 = y1 - 1; y2 >= 0; y2--)
{
byte value2 = this[x1, y2];
if (value1 != value2) break;
inARow++;
minY = y2;
}
for (int y2 = y1 + 1; y2 < TilesY; y2++)
{
byte value2 = this[x1, y2];
if (value1 != value2) break;
inARow++;
maxY = y2;
}
if (inARow >= ConnectToWin) tryAdd(new()
{
player = (byte)(value1 - 1),
direction = Direction.UpDown,
minX = x1,
maxX = x1,
minY = minY,
maxY = maxY
});
// Diagonal top-left to bottom-right.
(minX, minY) = (x1, y1);
(maxX, maxY) = (x1, y1);
inARow = 1;
for ((int x2, int y2) = (x1 - 1, y1 - 1); x2 >= 0 && y2 >= 0; x2--, y2--)
{
byte value2 = this[x2, y2];
if (value1 != value2) break;
inARow++;
minX = x2;
minY = y2;
}
for ((int x2, int y2) = (x1 + 1, y1 + 1); x2 < TilesX && y2 < TilesY; x2++, y2++)
{
byte value2 = this[x2, y2];
if (value1 != value2) break;
inARow++;
maxX = x2;
maxY = y2;
}
if (inARow >= ConnectToWin) tryAdd(new()
{
player = (byte)(value1 - 1),
direction = Direction.DiagonalTopLeftBottomRight,
minX = minX,
maxX = maxX,
minY = minY,
maxY = maxY
});
// Diagonal top-right to bottom-left.
(minX, minY) = (x1, y1);
(maxX, maxY) = (x1, y1);
inARow = 1;
for ((int x2, int y2) = (x1 + 1, y1 - 1); x2 < TilesX && y2 >= 0; x2++, y2--)
{
byte value2 = this[x2, y2];
if (value1 != value2) break;
inARow++;
maxX = x2;
minY = y2;
}
for ((int x2, int y2) = (x1 - 1, y1 + 1); x2 >= 0 && y2 < TilesY; x2--, y2++)
{
byte value2 = this[x2, y2];
if (value1 != value2) break;
inARow++;
minX = x2;
maxY = y2;
}
if (inARow >= ConnectToWin) tryAdd(new()
{
player = (byte)(value1 - 1),
direction = Direction.DiagonalTopRightBottomLeft,
minX = minX,
maxX = maxX,
minY = minY,
maxY = maxY
});
}
}
void tryAdd(WinObject win)
{
if (!wins.Any(x => x.direction == win.direction &&
x.minX == win.minX && x.minY == win.minY &&
x.player == win.player))
wins.Add(win);
}
return wins;
}
}

View File

@ -0,0 +1,85 @@
namespace BonusTicTacToe
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
Toolbar = new MenuStrip();
ReplayButton = new ToolStripMenuItem();
CustomizeButton = new ToolStripMenuItem();
ExitButton = new ToolStripMenuItem();
Toolbar.SuspendLayout();
SuspendLayout();
//
// Toolbar
//
Toolbar.AutoSize = false;
Toolbar.ImageScalingSize = new Size(32, 32);
Toolbar.Items.AddRange(new ToolStripItem[] { ReplayButton, CustomizeButton, ExitButton });
Toolbar.Location = new Point(0, 0);
Toolbar.Name = "Toolbar";
Toolbar.Size = new Size(974, 56);
Toolbar.TabIndex = 0;
Toolbar.Text = "Toolbar";
//
// ReplayButton
//
ReplayButton.Name = "ReplayButton";
ReplayButton.Size = new Size(104, 52);
ReplayButton.Text = "Replay";
ReplayButton.Click += ReplayButton_Click;
//
// ExitButton
//
ExitButton.Name = "ExitButton";
ExitButton.Size = new Size(71, 52);
ExitButton.Text = "Exit";
ExitButton.Click += ExitButton_Click;
//
// MainForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(974, 929);
Controls.Add(Toolbar);
FormBorderStyle = FormBorderStyle.FixedSingle;
MainMenuStrip = Toolbar;
Name = "MainForm";
Text = "Bonus Tic Tac Toe";
Toolbar.ResumeLayout(false);
Toolbar.PerformLayout();
ResumeLayout(false);
}
#endregion
private MenuStrip Toolbar;
private ToolStripMenuItem ReplayButton;
private ToolStripMenuItem CustomizeButton;
private ToolStripMenuItem ExitButton;
}
}

View File

@ -0,0 +1,416 @@

using System.Drawing.Drawing2D;
namespace BonusTicTacToe;
public partial class MainForm : Form
{
public Game ActiveGame { get; set; } = Game.Load();
public Preferences Preferences { get; } = Preferences.Load();
public float ScalingFactor { get; private set; }
private Font fontOutfit;
public MainForm()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
InitializeComponent();
// Get DPI for the scaling factor.
Graphics tempG = CreateGraphics();
ScalingFactor = tempG.DpiX / 192;
tempG.Dispose();
// Load main font from memory.
fontOutfit = new(Program.fonts.Families.Single(x => x.Name == "Outfit SemiBold"), 12);
SetWindowSizeFromGame();
}
public void SetWindowSizeFromGame()
{
Size = new Size(
(int)(ScalingFactor * (Preferences.TileSize * ActiveGame.TilesX + Preferences.WindowBuffer * 2)),
(int)(ScalingFactor * (Preferences.TileSize * ActiveGame.TilesY + Preferences.WindowBuffer * 2)));
}
public void SetupBoard()
{
ActiveGame.Board = new byte[ActiveGame.TilesX * ActiveGame.TilesY];
ActiveGame.CurrentTurn = 0;
ActiveGame.HasFinished = 0;
ActiveGame.IsDraw = false;
ActiveGame.TotalMoves = 0;
Invalidate(true);
ActiveGame.Save();
}
protected override void OnSizeChanged(EventArgs e)
{
// Full invalidation when the window size is changed,
// since the grid is centered.
Invalidate(true);
}
private Point lastHighlightedTile;
protected override void OnMouseMove(MouseEventArgs e)
{
// Check if the tile you're highlighting has changed,
// redraw the screen with the new one.
Point highlightedTile = ScreenToTileInt(Cursor.Position);
if (highlightedTile != lastHighlightedTile) Invalidate(true);
lastHighlightedTile = highlightedTile;
}
protected override void OnMouseClick(MouseEventArgs e)
{
// Add a player move to the current selected tile and redraw.
Point highlightedTile = ScreenToTileInt(Cursor.Position);
PlayMove(highlightedTile);
}
private void PlayMove(Point tile)
{
if (ActiveGame.HasFinished != 0 || ActiveGame.IsDraw ||
!TileWithinRange(tile) || ActiveGame[tile.X, tile.Y] != 0) return;
ActiveGame[tile.X, tile.Y] = (byte)((ActiveGame.CurrentTurn / ActiveGame.MovesPerTurn) + 1);
ActiveGame.CurrentTurn = (ActiveGame.CurrentTurn + 1) % (ActiveGame.Players * ActiveGame.MovesPerTurn);
ActiveGame.TotalMoves++;
// Check for wins.
List<WinObject> wins = ActiveGame.GetWins();
for (int i = 0; i < ActiveGame.Players; i++)
{
if (wins.Count(x => x.player == i) >= ActiveGame.WinsToFinish)
{
ActiveGame.HasFinished = (byte)(i + 1);
break;
}
}
ActiveGame.IsDraw = ActiveGame.HasFinished == 0 && ActiveGame.TotalMoves >= ActiveGame.Board.Length;
Invalidate(true);
ActiveGame.Save();
}
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
float centerX = ClientRectangle.Width / 2, centerY = ClientRectangle.Height / 2;
float tileSize = Preferences.TileSize * ScalingFactor;
float lenX = tileSize * ActiveGame.TilesX,
lenY = tileSize * ActiveGame.TilesY;
float left = centerX - lenX * 0.5f, top = centerY - lenY * 0.5f,
right = centerX + lenX * 0.5f, bottom = centerY + lenY * 0.5f;
// Step 1: Draw highlighted segment.
Point mouseTilePos = ScreenToTileInt(Cursor.Position);
if (ActiveGame.HasFinished == 0 && !ActiveGame.IsDraw && TileWithinRange(mouseTilePos))
{
SolidBrush highlightColor = new(Color.FromArgb(0x18000000));
Point tl = TileToScreen(mouseTilePos);
g.FillRectangle(highlightColor, new RectangleF(tl, new SizeF(tileSize, tileSize)));
}
// Step 2: Draw grid lines.
Pen gridPen = new(Color.FromArgb(unchecked((int)0xFF000000)), Preferences.LineThickness * ScalingFactor);
g.DrawRectangle(gridPen, new RectangleF(left, top, lenX, lenY));
for (int x = 0; x <= ActiveGame.TilesX; x++)
{
float t = (float)x / ActiveGame.TilesX;
float lerpX = left + t * lenX;
g.DrawLine(gridPen, new PointF(lerpX, top), new PointF(lerpX, bottom));
}
for (int y = 0; y <= ActiveGame.TilesY; y++)
{
float t = (float)y / ActiveGame.TilesY;
float lerpY = top + t * lenY;
g.DrawLine(gridPen, new PointF(left, lerpY), new PointF(right, lerpY));
}
// Step 3: Draw player inputs.
for (int x = 0; x < ActiveGame.TilesX; x++)
{
for (int y = 0; y < ActiveGame.TilesY; y++)
{
RenderPlayerInput(g, x, y);
}
}
// Step 4: Draw text stating whose turn it is.
// (Or who has won).
string text;
byte value;
if (ActiveGame.HasFinished != 0)
{
int turns = 1 + (ActiveGame.TotalMoves - 1) / (ActiveGame.Players * ActiveGame.MovesPerTurn);
text = $" has won in {turns} turn{(turns == 1 ? "" : "s")}!";
value = ActiveGame.HasFinished;
}
else if (ActiveGame.IsDraw)
{
int turns = 1 + (ActiveGame.TotalMoves - 1) / (ActiveGame.Players * ActiveGame.MovesPerTurn);
text = $"It's a draw in {turns} turn{(turns == 1 ? "" : "s")}!";
value = 0;
}
else
{
text = " to Move";
if (ActiveGame.MovesPerTurn > 1) text += $" ({ActiveGame.CurrentTurn % ActiveGame.MovesPerTurn + 1}/{ActiveGame.MovesPerTurn})";
value = (byte)((ActiveGame.CurrentTurn / ActiveGame.MovesPerTurn) + 1);
}
SolidBrush textBrush = new(Color.FromArgb(unchecked((int)0xFF_101010)));
SizeF length = g.MeasureString(text, fontOutfit);
float textSpriteSize = 20 * ScalingFactor,
textSpriteThickness = 6 * ScalingFactor;
length.Width += textSpriteSize;
float fullTextLeft = centerX - length.Width * 0.5f,
trueTextLeft = fullTextLeft + textSpriteSize;
float padding = 10 * ScalingFactor;
float fullTextBottom = top - padding;
StringFormat textFormat = new()
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Far,
};
g.DrawString(text, fontOutfit, textBrush, new PointF(trueTextLeft, fullTextBottom), textFormat);
RenderMoveSprite(g, value, new PointF(fullTextLeft, fullTextBottom - textSpriteSize * 1.7f), textSpriteSize, textSpriteThickness);
// Step 5: If there are wins, draw them.
List<WinObject> wins = ActiveGame.GetWins();
float lineThickness = 15 * ScalingFactor;
foreach (WinObject w in wins)
{
Color color = Color.FromArgb((int)playerColors[w.player]);
Pen temp = new(color, lineThickness);
PointF min, max;
switch (w.direction)
{
case Direction.LeftRight:
min = new(w.minX + 0.1f, w.minY + 0.5f);
max = new(w.maxX + 0.9f, w.minY + 0.5f);
break;
case Direction.UpDown:
min = new(w.minX + 0.5f, w.minY + 0.1f);
max = new(w.minX + 0.5f, w.maxY + 0.9f);
break;
case Direction.DiagonalTopRightBottomLeft:
min = new(w.maxX + 0.9f, w.minY + 0.1f);
max = new(w.minX + 0.1f, w.maxY + 0.9f);
break;
case Direction.DiagonalTopLeftBottomRight:
min = new(w.minX + 0.1f, w.minY + 0.1f);
max = new(w.maxX + 0.9f, w.maxY + 0.9f);
break;
default:
min = new();
max = new();
break;
}
g.DrawLine(temp, TileToScreen(min), TileToScreen(max));
}
// Step 6: Highlight replay button if game is finished.
if (ActiveGame.HasFinished != 0 || ActiveGame.IsDraw)
{
ReplayButton.Font = new Font(ReplayButton.Font, FontStyle.Bold);
}
else ReplayButton.Font = new Font(ReplayButton.Font, FontStyle.Regular);
base.OnPaint(e);
}
private readonly uint[] playerColors = [
0xFF_454545,
0xFF_E83535,
0xFF_35A9E8,
0xFF_2AAD39,
0xFF_9E49E3,
0xFF_EBA121,
0xFF_2C5AE6,
0xFF_FA78ED,
];
private void RenderMoveSprite(Graphics g, byte value, PointF tileStart, float tileSize, float thickness)
{
Color color;
if (value > 0 && value <= playerColors.Length) color = Color.FromArgb((int)playerColors[value - 1]);
else color = Color.Black;
Pen pen = new(color, thickness);
switch (value)
{
case 0: return; // No move.
case 1:
// Draw 'X'
float xLeft = tileStart.X, xRight = tileStart.X + tileSize,
xTop = tileStart.Y, xBottom = tileStart.Y + tileSize;
g.DrawLine(pen, xLeft, xTop, xRight, xBottom);
g.DrawLine(pen, xRight, xTop, xLeft, xBottom);
break;
case 2:
// Draw 'O'
g.DrawEllipse(pen, new RectangleF(tileStart, new SizeF(tileSize, tileSize)));
break;
case 3:
// Draw triangle (equilateral requires a little more math)
const float halfSqrt3 = 0.866025403784f;
float tMiddleX = tileStart.X + tileSize * 0.5f, tLeft = tileStart.X, tRight = tileStart.X + tileSize,
tMiddleY = tileStart.Y + tileSize * 0.5f, tRadius = tileSize * halfSqrt3 * 0.5f,
tTop = tMiddleY - tRadius, tBottom = tMiddleY + tRadius;
g.DrawPolygon(pen, [
new PointF(tLeft, tBottom),
new PointF(tMiddleX, tTop),
new PointF(tRight, tBottom)
]);
break;
case 4:
// Draw square.
float sLeft = tileStart.X, sRight = tileStart.X + tileSize,
sTop = tileStart.Y, sBottom = tileStart.Y + tileSize;
g.DrawPolygon(pen, [
new PointF(sLeft, sTop),
new PointF(sRight, sTop),
new PointF(sRight, sBottom),
new PointF(sLeft, sBottom)
]);
break;
case 5:
// Draw pentagon. Uses polygon code.
PointF[] polyArray = GeneratePolygon(value);
float pRadius = tileSize * 0.5f,
pCenterX = tileStart.X + pRadius,
pCenterY = tileStart.Y + pRadius;
for (int i = 0; i < polyArray.Length; i++)
{
PointF point = polyArray[i];
polyArray[i] = new PointF(point.X * pRadius * 1.1f + pCenterX,
point.Y * pRadius * 1.1f + pCenterY);
}
g.DrawPolygon(pen, polyArray);
break;
case 6:
// Draw star. Uses polygon code.
PointF[] starArray = GeneratePolygon(10);
float sOuterRadius = tileSize * 0.5f,
sInnerRadius = sOuterRadius * 0.5f,
sCenterX = tileStart.X + sOuterRadius,
sCenterY = tileStart.Y + sOuterRadius;
for (int i = 0; i < starArray.Length; i++)
{
PointF point = starArray[i];
float scale = i % 2 == 0 ? sOuterRadius : sInnerRadius;
starArray[i] = new PointF(point.X * scale + sCenterX,
point.Y * scale + sCenterY);
}
g.DrawPolygon(pen, starArray);
break;
case 7:
// Draw asterisk. Uses polygon code.
const int astPoints = 6;
PointF[] astArray = GeneratePolygon(astPoints);
float aRadius = tileSize * 0.5f,
aCenterX = tileStart.X + aRadius,
aCenterY = tileStart.Y + aRadius;
for (int i = 0; i < astArray.Length / 2; i++)
{
PointF pointA = astArray[i], pointB = astArray[i + astPoints / 2];
pointA = new(pointA.X * aRadius * 1.1f + aCenterX,
pointA.Y * aRadius * 1.1f + aCenterY);
pointB = new(pointB.X * aRadius * 1.1f + aCenterX,
pointB.Y * aRadius * 1.1f + aCenterY);
g.DrawLine(pen, pointA, pointB);
}
break;
default:
// Draw unknown.
SolidBrush brush = new(color);
g.FillRectangle(brush, new RectangleF(tileStart, new SizeF(tileSize, tileSize)));
break;
}
}
private void RenderPlayerInput(Graphics g, int x, int y)
{
byte value = ActiveGame[x, y];
if (value == 0) return; // No move made.
float tileSize = Preferences.TileSize * ScalingFactor;
PointF tileStart = TileToScreen(new Point(x, y));
float padding = 35 * ScalingFactor;
tileStart.X += padding;
tileStart.Y += padding;
tileSize -= padding * 2;
float thickness = 15 * ScalingFactor;
RenderMoveSprite(g, value, tileStart, tileSize, thickness);
}
private bool TileWithinRange(Point tile) =>
tile.X >= 0 && tile.X < ActiveGame.TilesX &&
tile.Y >= 0 && tile.Y < ActiveGame.TilesY;
private Point TileToScreen(PointF tile)
{
float centerX = ClientRectangle.Width / 2, centerY = ClientRectangle.Height / 2;
float tileSize = Preferences.TileSize * ScalingFactor;
float lenX = tileSize * ActiveGame.TilesX,
lenY = tileSize * ActiveGame.TilesY;
float left = centerX - lenX * 0.5f, top = centerY - lenY * 0.5f;
return new((int)(left + tileSize * tile.X), (int)(top + tileSize * tile.Y));
}
private PointF ScreenToTile(Point screen, bool convertToClient = true)
{
if (convertToClient) screen = PointToClient(screen);
float centerX = ClientRectangle.Width / 2, centerY = ClientRectangle.Height / 2;
float tileSize = Preferences.TileSize * ScalingFactor;
float lenX = tileSize * ActiveGame.TilesX,
lenY = tileSize * ActiveGame.TilesY;
float left = centerX - lenX * 0.5f, top = centerY - lenY * 0.5f;
return new((screen.X - left) / tileSize, (screen.Y - top) / tileSize);
}
private Point ScreenToTileInt(Point screen, bool convertToClient = true)
{
PointF raw = ScreenToTile(screen, convertToClient);
if (raw.X < 0) raw.X--;
if (raw.Y < 0) raw.Y--;
return new Point((int)raw.X, (int)raw.Y);
}
private static PointF[] GeneratePolygon(int sides)
{
PointF[] points = new PointF[sides];
double anglePer = 2 * Math.PI / sides;
for (int i = 0; i < sides; i++)
{
points[i] = new PointF((float)Math.Sin(anglePer * i), -(float)Math.Cos(anglePer * i));
}
return points;
}
private void ExitButton_Click(object? sender, EventArgs e)
{
Close();
}
private void ReplayButton_Click(object? sender, EventArgs e)
{
SetupBoard();
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="Toolbar.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@ -0,0 +1,41 @@
using System.Text.Json;
namespace BonusTicTacToe;
public class Preferences
{
public const string PrefsPath = "./prefs.json";
public static readonly bool SaveToFile = false;
public float TileSize { get; set; } = 150;
public float WindowBuffer { get; set; } = 200;
public float LineThickness { get; set; } = 3;
public static Preferences Load()
{
if (!SaveToFile) return new();
Preferences prefs;
if (File.Exists(PrefsPath))
{
FileStream fs = new(PrefsPath, FileMode.Open);
prefs = JsonSerializer.Deserialize<Preferences>(fs)!;
fs.Close();
}
else
{
prefs = new();
prefs.Save();
}
return prefs;
}
public void Save()
{
if (!SaveToFile) return;
FileStream fs = new(PrefsPath, FileMode.Create);
JsonSerializer.Serialize(fs, this);
fs.Close();
}
}

View File

@ -0,0 +1,40 @@
/**********722871**********
* Date: 9/24/2024
* Programmer: Kyle Gilbert
* Program Name: BonusTicTacToe
* Program Description: Customizable tic-tac-toe with some nice colors.
**************************/
using BonusTicTacToe.Resources;
using System.Drawing.Text;
using System.Runtime.InteropServices;
namespace BonusTicTacToe;
public static class Program
{
internal static PrivateFontCollection fonts = new();
[STAThread]
public static void Main()
{
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.SystemAware);
// Load fonts into our collection via an unsafe marshal copy.
// Somewhat unsafe, but that's where the fun happens anyway.
// Maybe variable fonts could have worked?
IEnumerable<byte[]> fontsToLoad = [
Fonts.Outfit_SemiBold, // 600
];
foreach (byte[] toLoad in fontsToLoad)
{
int length = toLoad.Length;
nint ptr = Marshal.AllocCoTaskMem(length);
Marshal.Copy(toLoad, 0, ptr, length);
fonts.AddMemoryFont(ptr, length);
}
Application.Run(new MainForm());
}
}

View File

@ -0,0 +1,153 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace BonusTicTacToe.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Fonts {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Fonts() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BonusTicTacToe.Resources.Fonts", typeof(Fonts).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_Black {
get {
object obj = ResourceManager.GetObject("Outfit_Black", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_Bold {
get {
object obj = ResourceManager.GetObject("Outfit_Bold", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_ExtraBold {
get {
object obj = ResourceManager.GetObject("Outfit_ExtraBold", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_ExtraLight {
get {
object obj = ResourceManager.GetObject("Outfit_ExtraLight", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_Light {
get {
object obj = ResourceManager.GetObject("Outfit_Light", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_Medium {
get {
object obj = ResourceManager.GetObject("Outfit_Medium", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_Regular {
get {
object obj = ResourceManager.GetObject("Outfit_Regular", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_SemiBold {
get {
object obj = ResourceManager.GetObject("Outfit_SemiBold", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] Outfit_Thin {
get {
object obj = ResourceManager.GetObject("Outfit_Thin", resourceCulture);
return ((byte[])(obj));
}
}
}
}

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="Outfit_Black" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-Black.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_Bold" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-Bold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_ExtraBold" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-ExtraBold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_ExtraLight" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-ExtraLight.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_Light" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-Light.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_Medium" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-Medium.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_Regular" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-Regular.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_SemiBold" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-SemiBold.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Outfit_Thin" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Outfit-Thin.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>

View File

@ -0,0 +1,8 @@
namespace BonusTicTacToe;
public struct WinObject
{
public Direction direction;
public byte player;
public int minX, maxX, minY, maxY;
}

View File

@ -17,3 +17,8 @@ I have about 1-2 weeks for each project. Check the Git commits for specific date
- I made a few default strategies (draw until 17, simple card counting, simple probabilities).
- No additional libraries were used.
- It has two custom-rendered graphs on the console display. I haven't figured out how to use XTerm yet, so I'm generating individual characters.
- BonusTicTacToe/
- Plays tic-tac-toe. I made the game in Windows Forms.
- The game allows for customization for the board size (rows/columns), number of players (up to 8), amount needed in a row to win, and amount of wins needed to finish the game.
- Nice colors and sprites for each player. Scales seamlessly with a higher DPI.
- The only component used is the menu component. The board and sprites are rendered myself with OnPaint.