From 925723143b2a125033ed7e1ead123b15262e589c Mon Sep 17 00:00:00 2001 From: ilya-krivtsov <180809461+ilya-krivtsov@users.noreply.github.com> Date: Fri, 23 May 2025 17:18:15 +0300 Subject: [PATCH] Tic-tac-toe --- .../TicTacToe.Tests/GlobalSuppressions.cs | 11 ++ .../TicTacToe.Tests/TicTacToe.Tests.csproj | 27 +++++ .../TicTacToe.Tests/TicTacToeGameTests.cs | 66 +++++++++++ TicTacToe/TicTacToe.UI/App.axaml | 10 ++ TicTacToe/TicTacToe.UI/App.axaml.cs | 27 +++++ TicTacToe/TicTacToe.UI/GlobalSuppressions.cs | 16 +++ TicTacToe/TicTacToe.UI/MainWindow.axaml | 25 ++++ TicTacToe/TicTacToe.UI/MainWindow.axaml.cs | 79 +++++++++++++ TicTacToe/TicTacToe.UI/Program.cs | 25 ++++ TicTacToe/TicTacToe.UI/TicTacToe.UI.csproj | 22 ++++ TicTacToe/TicTacToe.UI/app.manifest | 18 +++ TicTacToe/TicTacToe.sln | 34 ++++++ TicTacToe/TicTacToe/Cell.cs | 26 +++++ TicTacToe/TicTacToe/MoveResult.cs | 36 ++++++ TicTacToe/TicTacToe/TicTacToe.csproj | 9 ++ TicTacToe/TicTacToe/TicTacToeGame.cs | 107 ++++++++++++++++++ 16 files changed, 538 insertions(+) create mode 100644 TicTacToe/TicTacToe.Tests/GlobalSuppressions.cs create mode 100644 TicTacToe/TicTacToe.Tests/TicTacToe.Tests.csproj create mode 100644 TicTacToe/TicTacToe.Tests/TicTacToeGameTests.cs create mode 100644 TicTacToe/TicTacToe.UI/App.axaml create mode 100644 TicTacToe/TicTacToe.UI/App.axaml.cs create mode 100644 TicTacToe/TicTacToe.UI/GlobalSuppressions.cs create mode 100644 TicTacToe/TicTacToe.UI/MainWindow.axaml create mode 100644 TicTacToe/TicTacToe.UI/MainWindow.axaml.cs create mode 100644 TicTacToe/TicTacToe.UI/Program.cs create mode 100644 TicTacToe/TicTacToe.UI/TicTacToe.UI.csproj create mode 100644 TicTacToe/TicTacToe.UI/app.manifest create mode 100644 TicTacToe/TicTacToe.sln create mode 100644 TicTacToe/TicTacToe/Cell.cs create mode 100644 TicTacToe/TicTacToe/MoveResult.cs create mode 100644 TicTacToe/TicTacToe/TicTacToe.csproj create mode 100644 TicTacToe/TicTacToe/TicTacToeGame.cs diff --git a/TicTacToe/TicTacToe.Tests/GlobalSuppressions.cs b/TicTacToe/TicTacToe.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..5292943 --- /dev/null +++ b/TicTacToe/TicTacToe.Tests/GlobalSuppressions.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This is project with tests")] diff --git a/TicTacToe/TicTacToe.Tests/TicTacToe.Tests.csproj b/TicTacToe/TicTacToe.Tests/TicTacToe.Tests.csproj new file mode 100644 index 0000000..0cfaf2c --- /dev/null +++ b/TicTacToe/TicTacToe.Tests/TicTacToe.Tests.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/TicTacToe/TicTacToe.Tests/TicTacToeGameTests.cs b/TicTacToe/TicTacToe.Tests/TicTacToeGameTests.cs new file mode 100644 index 0000000..467e211 --- /dev/null +++ b/TicTacToe/TicTacToe.Tests/TicTacToeGameTests.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe.Tests; + +public class TicTacToeGameTests +{ + private TicTacToeGame game; + + [SetUp] + public void Setup() + { + game = new(); + } + + [Test] + public void FirstRow_OfX_Wins() + { + Assert.That(game.MakeMove(0, 0), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(0, 1), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(1, 0), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(1, 1), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(2, 0), Is.EqualTo(MoveResult.XWins)); + } + + [Test] + public void FirstRow_OfO_Wins() + { + Assert.That(game.MakeMove(0, 0), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(0, 1), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(1, 0), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(1, 1), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(2, 2), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(2, 1), Is.EqualTo(MoveResult.OWins)); + } + + [Test] + public void FirstColumn_OfO_Wins() + { + Assert.That(game.MakeMove(1, 0), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(0, 0), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(1, 2), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(0, 1), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(2, 2), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(0, 2), Is.EqualTo(MoveResult.OWins)); + } + + [Test] + public void Diagonal_OfX_Wins() + { + Assert.That(game.MakeMove(0, 0), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(0, 1), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(1, 1), Is.EqualTo(MoveResult.XTurn)); + Assert.That(game.MakeMove(1, 2), Is.EqualTo(MoveResult.OTurn)); + + Assert.That(game.MakeMove(2, 2), Is.EqualTo(MoveResult.XWins)); + } +} diff --git a/TicTacToe/TicTacToe.UI/App.axaml b/TicTacToe/TicTacToe.UI/App.axaml new file mode 100644 index 0000000..840b2b0 --- /dev/null +++ b/TicTacToe/TicTacToe.UI/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/TicTacToe/TicTacToe.UI/App.axaml.cs b/TicTacToe/TicTacToe.UI/App.axaml.cs new file mode 100644 index 0000000..fb50f3f --- /dev/null +++ b/TicTacToe/TicTacToe.UI/App.axaml.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe.UI; + +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/TicTacToe/TicTacToe.UI/GlobalSuppressions.cs b/TicTacToe/TicTacToe.UI/GlobalSuppressions.cs new file mode 100644 index 0000000..d28023d --- /dev/null +++ b/TicTacToe/TicTacToe.UI/GlobalSuppressions.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Entry point", Scope = "type", Target = "~T:TicTacToe.UI.Program")] + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:Partial elements should be documented", Justification = "Application class", Scope = "type", Target = "~T:TicTacToe.UI.App")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:Partial elements should be documented", Justification = "Window class", Scope = "type", Target = "~T:TicTacToe.UI.MainWindow")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Application class", Scope = "type", Target = "~T:TicTacToe.UI.App")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Window class", Scope = "type", Target = "~T:TicTacToe.UI.MainWindow")] diff --git a/TicTacToe/TicTacToe.UI/MainWindow.axaml b/TicTacToe/TicTacToe.UI/MainWindow.axaml new file mode 100644 index 0000000..46bd4ec --- /dev/null +++ b/TicTacToe/TicTacToe.UI/MainWindow.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/TicTacToe/TicTacToe.UI/MainWindow.axaml.cs b/TicTacToe/TicTacToe.UI/MainWindow.axaml.cs new file mode 100644 index 0000000..f99ccae --- /dev/null +++ b/TicTacToe/TicTacToe.UI/MainWindow.axaml.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe.UI; + +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.VisualTree; +using MsBox.Avalonia; + +public partial class MainWindow : Window +{ + private readonly TicTacToeGame game; + + private bool won = false; + + public MainWindow() + { + InitializeComponent(); + + game = new(); + + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + var button = new Button() + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, + HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center, + FontSize = 64, + }; + int row = i; + int column = j; + + button.Click += (o, e) => OnClick(button, row, column); + + Grid.SetRow(button, row); + Grid.SetColumn(button, column); + mainGrid.Children.Add(button); + } + } + } + + private void OnClick(Button button, int row, int column) + { + if (won) + { + return; + } + + var result = game.MakeMove(column, row); + + var xTurn = result is MoveResult.XTurn or MoveResult.XWins; + var oTurn = result is MoveResult.OTurn or MoveResult.OWins; + + if (!xTurn && !oTurn) + { + return; + } + + if (result is MoveResult.XWins or MoveResult.OWins) + { + won = true; + MessageBoxManager + .GetMessageBoxStandard("Win!", $"{(result == MoveResult.XWins ? "X" : "O")} has won!") + .ShowWindowAsync(); + } + + button.Content = xTurn ? "X" : "O"; + button.Foreground = xTurn ? SolidColorBrush.Parse("#f02020") : SolidColorBrush.Parse("#2020f0"); + } +} diff --git a/TicTacToe/TicTacToe.UI/Program.cs b/TicTacToe/TicTacToe.UI/Program.cs new file mode 100644 index 0000000..de9c834 --- /dev/null +++ b/TicTacToe/TicTacToe.UI/Program.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe.UI; + +using System; +using Avalonia; + +public class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/TicTacToe/TicTacToe.UI/TicTacToe.UI.csproj b/TicTacToe/TicTacToe.UI/TicTacToe.UI.csproj new file mode 100644 index 0000000..6d4a0b9 --- /dev/null +++ b/TicTacToe/TicTacToe.UI/TicTacToe.UI.csproj @@ -0,0 +1,22 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + diff --git a/TicTacToe/TicTacToe.UI/app.manifest b/TicTacToe/TicTacToe.UI/app.manifest new file mode 100644 index 0000000..6348f3f --- /dev/null +++ b/TicTacToe/TicTacToe.UI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/TicTacToe/TicTacToe.sln b/TicTacToe/TicTacToe.sln new file mode 100644 index 0000000..70c58b3 --- /dev/null +++ b/TicTacToe/TicTacToe.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TicTacToe", "TicTacToe\TicTacToe.csproj", "{13DED376-EA25-47D0-A76A-1C417967F3E3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TicTacToe.Tests", "TicTacToe.Tests\TicTacToe.Tests.csproj", "{671F028F-5CA0-4D46-94D7-5250C1EBEAEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TicTacToe.UI", "TicTacToe.UI\TicTacToe.UI.csproj", "{D1BAFEA2-7EEA-4602-98A0-F639C9265A2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13DED376-EA25-47D0-A76A-1C417967F3E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13DED376-EA25-47D0-A76A-1C417967F3E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13DED376-EA25-47D0-A76A-1C417967F3E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13DED376-EA25-47D0-A76A-1C417967F3E3}.Release|Any CPU.Build.0 = Release|Any CPU + {671F028F-5CA0-4D46-94D7-5250C1EBEAEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {671F028F-5CA0-4D46-94D7-5250C1EBEAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {671F028F-5CA0-4D46-94D7-5250C1EBEAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {671F028F-5CA0-4D46-94D7-5250C1EBEAEF}.Release|Any CPU.Build.0 = Release|Any CPU + {D1BAFEA2-7EEA-4602-98A0-F639C9265A2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1BAFEA2-7EEA-4602-98A0-F639C9265A2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1BAFEA2-7EEA-4602-98A0-F639C9265A2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1BAFEA2-7EEA-4602-98A0-F639C9265A2D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/TicTacToe/TicTacToe/Cell.cs b/TicTacToe/TicTacToe/Cell.cs new file mode 100644 index 0000000..bfc3698 --- /dev/null +++ b/TicTacToe/TicTacToe/Cell.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe; + +/// +/// Cell on the tic-tac-toe board. +/// +public enum Cell +{ + /// + /// Empty cell. + /// + Empty, + + /// + /// X cell. + /// + X, + + /// + /// O cell. + /// + O, +} diff --git a/TicTacToe/TicTacToe/MoveResult.cs b/TicTacToe/TicTacToe/MoveResult.cs new file mode 100644 index 0000000..957b527 --- /dev/null +++ b/TicTacToe/TicTacToe/MoveResult.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe; + +/// +/// Result of the move on the tic-tac-toe board. +/// +public enum MoveResult +{ + /// + /// X made turn. + /// + XTurn, + + /// + /// X made turn. + /// + OTurn, + + /// + /// X wins. + /// + XWins, + + /// + /// O wins. + /// + OWins, + + /// + /// Incorrect move has been made. + /// + IncorrectMove, +} diff --git a/TicTacToe/TicTacToe/TicTacToe.csproj b/TicTacToe/TicTacToe/TicTacToe.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/TicTacToe/TicTacToe/TicTacToe.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/TicTacToe/TicTacToe/TicTacToeGame.cs b/TicTacToe/TicTacToe/TicTacToeGame.cs new file mode 100644 index 0000000..0382c58 --- /dev/null +++ b/TicTacToe/TicTacToe/TicTacToeGame.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) Ilya Krivtsov. All rights reserved. +// + +namespace TicTacToe; + +/// +/// The game of tic-tac-toe. +/// +public class TicTacToeGame +{ + private readonly Cell[,] cells = new Cell[3, 3]; + + private Cell turn; + + private MoveResult? winner = null; + + /// + /// Initializes a new instance of the class. + /// + public TicTacToeGame() + { + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + cells[i, j] = Cell.Empty; + } + } + + turn = Cell.X; + } + + /// + /// Makes a move and returns , if player won this turn. + /// + /// Column on the board to set X or O. + /// Row on the board to set X or O. + /// if player won the game with this move, otherwise. + public MoveResult MakeMove(int column, int row) + { + if (winner != null) + { + return winner.Value; + } + + if (column < 0 || column > 2 || row < 0 || row > 2) + { + return MoveResult.IncorrectMove; + } + + if (cells[column, row] != Cell.Empty) + { + return MoveResult.IncorrectMove; + } + + cells[column, row] = turn; + + if (CheckVictory(turn)) + { + winner = turn == Cell.X ? MoveResult.XWins : MoveResult.OWins; + return winner.Value; + } + + var oldTurn = turn; + + turn = turn == Cell.X ? Cell.O : Cell.X; + + return oldTurn == Cell.X ? MoveResult.XTurn : MoveResult.OTurn; + } + + private bool CheckVictory(Cell cell) + { + if (cell != Cell.X && cell != Cell.O) + { + return false; + } + + for (int y = 0; y < 3; y++) + { + if (Enumerable.Range(0, 3).All(i => cells[i, y] == cell)) + { + return true; + } + } + + for (int x = 0; x < 3; x++) + { + if (Enumerable.Range(0, 3).All(i => cells[x, i] == cell)) + { + return true; + } + } + + if (Enumerable.Range(0, 3).All(i => cells[i, i] == cell)) + { + return true; + } + + if (Enumerable.Range(0, 3).All(i => cells[2 - i, i] == cell)) + { + return true; + } + + return false; + } +}