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;
+ }
+}