Created
June 1, 2022 16:47
-
-
Save eisterman/9e55556c29394364a3f3c0312eb6bfb5 to your computer and use it in GitHub Desktop.
WPF Game of Life
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Windows; | |
using System.Windows.Controls; | |
using System.Windows.Media; | |
using System.Windows.Shapes; | |
using System.Windows.Threading; | |
namespace WpfGameOfLife; | |
public class RectangleCell { | |
public double Left { get; internal set; } | |
public double Top { get; internal set; } | |
public Rectangle Rect { get; internal set; } | |
public int X { get; private set; } | |
public int Y { get; private set; } | |
public RectangleCell(int x, int y) | |
{ | |
X = x; | |
Y = y; | |
} | |
} | |
public class CanvasGameOfLife { | |
public Brush LiveBrush { get; } = Brushes.Orange; | |
public Brush DeadBrush { get; } = Brushes.WhiteSmoke; | |
private double _rectangleWidth, _rectangleHeight; | |
private Canvas _canvas; | |
private RectangleCell[,] _cells; | |
public CanvasGameOfLife(Canvas canvas, int width, int height, GoLRule rule) { | |
Width = width; | |
Height = height; | |
Rule = rule; | |
_table = new int[width, height]; | |
_canvas = canvas; | |
canvas.Children.Clear(); | |
_rectangleWidth = _canvas.ActualWidth / width; | |
_rectangleHeight = _canvas.ActualHeight / height; | |
_rectangleWidth = _rectangleHeight = Math.Min(_rectangleWidth, _rectangleHeight); | |
_cells = new RectangleCell[width, height]; | |
for (int y = 0; y < height; y++) | |
{ | |
for (int x = 0; x < width; x++) | |
{ | |
var cell = new RectangleCell(x, y); | |
cell.Left = x * _rectangleWidth; | |
cell.Top = y * _rectangleHeight; | |
cell.Rect = new Rectangle | |
{ | |
Width = _rectangleWidth, | |
Height = _rectangleHeight, | |
Stroke = Brushes.LightGray, | |
StrokeThickness = 0.6, | |
// ToolTip = "0", | |
Tag = cell, | |
}; | |
Canvas.SetLeft(cell.Rect, cell.Left); | |
Canvas.SetTop(cell.Rect, cell.Top); | |
canvas.Children.Add(cell.Rect); | |
cell.Rect.Fill = DeadBrush; | |
_cells[x, y] = cell; | |
} | |
} | |
} | |
public int Width { get; } | |
public int Height { get; } | |
public int this[int x, int y] { | |
get => _table[Math.Abs(x % Width), Math.Abs(y % Height)]; | |
set => _table[Math.Abs(x % Width), Math.Abs(y % Height)] = value; | |
} | |
private int[,] _table; | |
public GoLRule Rule { get; set; } | |
public int Generation { get; private set; } | |
private int GetNeighbors(int x, int y) { | |
var neigh = 0; | |
for (var i = -1; i <= 1; i++) { | |
for (var j = -1; j <= 1; j++) { | |
if (i == 0 && j == 0) continue; | |
if (this[x + i, y + j] > 0) neigh++; | |
} | |
} | |
return neigh; | |
} | |
public void RefreshCellsColor() { | |
for (var i = 0; i < Width; i++) { | |
for (var j = 0; j < Height; j++) { | |
_cells[i, j].Rect.Fill = this[i, j] > 0 ? LiveBrush : DeadBrush; | |
} | |
} | |
} | |
public void RunOnce() { | |
var ntable = new int[Width, Height]; | |
for (var i = 0; i < Width; i++) { | |
for (var j = 0; j < Height; j++) { | |
ntable[i, j] = (_table[i, j] == 0 ? Rule.Birth : Rule.Stay).Contains(GetNeighbors(i, j)) ? 1 : 0; | |
} | |
} | |
_table = ntable; | |
Generation++; | |
RefreshCellsColor(); | |
} | |
public int[]? MouseToCoord(Point p) { | |
var x = (int)(p.X / _rectangleWidth); | |
var y = (int)(p.Y / _rectangleHeight); | |
if (x < 0 || x >= Width || y < 0 || y >= Height) { | |
return null; | |
} | |
return new int[] { x, y }; | |
} | |
public override string ToString() { | |
var output = $"Generation:\t{Generation}\n"; | |
for (var j = 0; j < Height; j++) { | |
var line = ""; | |
for (var i = 0; i < Width; i++) { | |
line += _table[i, j] > 0 ? "#" : "."; | |
} | |
output += line + "\n"; | |
} | |
return output; | |
} | |
public void Randomize(int seed) { | |
var random = new Random(seed); | |
for (var i = 0; i < Width; i++) | |
for (var j = 0; j < Height; j++) { | |
_table[i, j] = random.Next(0, 2); | |
} | |
RefreshCellsColor(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
namespace WpfGameOfLife; | |
public interface IGoLRule { | |
public int[] Birth { get; } | |
public int[] Stay { get; } | |
} | |
public class GoLRule: IGoLRule { | |
public GoLRule() { | |
Birth = new[] { 3 }; | |
Stay = new[] { 2, 3 }; | |
} | |
public GoLRule(int[] birth, int[] stay) { | |
if (birth.Any(x => x is >= 0 and < 9)) throw new ArgumentOutOfRangeException(nameof(birth)); | |
if (stay.Any(x => x is >= 0 and < 9)) throw new ArgumentOutOfRangeException(nameof(stay)); | |
Birth = birth; | |
Stay = stay; | |
} | |
public GoLRule(string rule) { | |
var parts = rule.Split('/'); | |
var strBirth = parts[0].ToCharArray(); | |
var strStay = parts[1].ToCharArray(); | |
var iaBirth = strBirth.Select(r => int.Parse(r.ToString())); | |
var iaStay = strStay.Select(r => int.Parse(r.ToString())); | |
Birth = iaBirth as int[] ?? throw new InvalidOperationException(); | |
Stay = iaStay as int[] ?? throw new InvalidOperationException(); | |
} | |
public int[] Birth { get;} | |
public int[] Stay { get; } | |
public override string ToString() { | |
var strBirth = string.Join("", Birth.Select(x => x.ToString())); | |
var strStay = string.Join("", Stay.Select(x => x.ToString())); | |
return string.Join("/", strBirth, strStay); | |
} | |
/* | |
public int ApplyRule(int cellstatus, IEnumerable<int> neighbors) { | |
var enumerable = neighbors as int[] ?? neighbors.ToArray(); | |
if (enumerable.Length != 8) throw new ArgumentException("neighbors need to be 8"); | |
return (cellstatus == 0 ? Birth : Stay).Contains(enumerable.Count(x => x > 0)) ? 1 : 0; | |
} | |
public int ApplyRule(int cellstatus, int numberalive) { | |
return (cellstatus == 0 ? Birth : Stay).Contains(numberalive) ? 1 : 0; | |
} | |
*/ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Window x:Class="WpfGameOfLife.MainWindow" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |
xmlns:local="clr-namespace:WpfGameOfLife" | |
mc:Ignorable="d" | |
Title="Conway's Game Of Life" Height="450" Width="800"> | |
<Window.Resources> | |
<Style TargetType="TextBlock"> | |
<Setter Property="VerticalAlignment" Value="Center"/> | |
<Setter Property="Margin" Value="5 0"/> | |
</Style> | |
<Style TargetType="TextBox"> | |
<Setter Property="VerticalAlignment" Value="Center"/> | |
<Setter Property="Margin" Value="5 0"/> | |
<Setter Property="MinWidth" Value="50"/> | |
</Style> | |
<Style TargetType="ComboBox"> | |
<Setter Property="VerticalAlignment" Value="Center"/> | |
<Setter Property="Margin" Value="5 0"/> | |
<Setter Property="MinWidth" Value="100"/> | |
<Setter Property="SelectedIndex" Value="0"/> | |
</Style> | |
<Style TargetType="Button"> | |
<Setter Property="VerticalAlignment" Value="Center"/> | |
<Setter Property="Margin" Value="5 0"/> | |
<Setter Property="MinWidth" Value="100"/> | |
</Style> | |
</Window.Resources> | |
<Grid d:DataContext="{local:MainWindow}"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="40"/> | |
<RowDefinition Height="*"/> | |
</Grid.RowDefinitions> | |
<DockPanel> | |
<TextBlock Text="Width:"></TextBlock> | |
<TextBox Name="TbWidth" Text="{Binding AreaWidth}"></TextBox> | |
<TextBlock Text="Height:"></TextBlock> | |
<TextBox Name="TbHeight" Text="{Binding AreaHeight}"></TextBox> | |
<TextBlock Text="Seed:"></TextBlock> | |
<TextBox Name="TbSeed" Text="{Binding Seed}"></TextBox> | |
<TextBlock Text="Rule:"></TextBlock> | |
<ComboBox Name="CbRule" ItemsSource="{Binding Rules}" SelectedItem="{Binding Rule}"></ComboBox> | |
<Button x:Name="BRun" Click="BRun_OnClick" HorizontalAlignment="Right" Width="100">Start</Button> | |
</DockPanel> | |
<Canvas x:Name="GoLCanvas" Grid.Row="1" Background="AliceBlue" | |
MouseLeftButtonDown="Canvas_OnMouseLeftButtonDown" | |
MouseMove="GoLCanvas_OnMouseMove" | |
MouseLeftButtonUp="GoLCanvas_OnMouseLeftButtonUp" | |
> | |
</Canvas> | |
</Grid> | |
</Window> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Windows; | |
using System.Windows.Input; | |
using System.Windows.Threading; | |
using System.Diagnostics; | |
using System.Linq; | |
namespace WpfGameOfLife { | |
/// <summary> | |
/// Interaction logic for MainWindow.xaml | |
/// </summary> | |
public partial class MainWindow : Window { | |
private CanvasGameOfLife? _gol; | |
private GoLRule _rule = new GoLRule(); | |
private bool _isrunning = false; | |
public string Rule { | |
get => _rule.ToString(); | |
set => _rule = new GoLRule(value); | |
} | |
private DispatcherTimer? _timer; | |
public int AreaWidth { get; set; } = 60; | |
public int AreaHeight { get; set; } = 60; | |
public int Seed { get; set; } = 1; | |
public string[] Rules { get; } = new string[] { | |
"3/23", | |
"1/1", | |
}; | |
public MainWindow() | |
{ | |
InitializeComponent(); | |
DataContext = this; | |
} | |
private void BRun_OnClick(object sender, RoutedEventArgs e) { | |
if (_isrunning) { | |
BRun.Content = "Run"; | |
} else { | |
BRun.Content = "Stop"; | |
if (_gol is null) InitGoL(); | |
RunGoL(); | |
} | |
_isrunning = !_isrunning; | |
} | |
private void InitGoL() { | |
_gol = new CanvasGameOfLife(GoLCanvas, AreaWidth, AreaHeight, _rule); | |
_gol.Randomize(Seed); | |
TbWidth.IsEnabled = false; | |
TbHeight.IsEnabled = false; | |
TbSeed.IsEnabled = false; | |
CbRule.IsEnabled = false; | |
} | |
private void RunGoL() { | |
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) }; | |
_timer.Tick += (o, e) => | |
{ | |
_timer.Stop(); | |
_gol!.RunOnce(); | |
if (_isrunning) | |
{ | |
_timer.Start(); | |
} | |
}; | |
_timer.Start(); | |
} | |
private int[]? _lastChangedCoords; | |
private void DrawLineTick(Point p) { | |
var coords = _gol?.MouseToCoord(p); | |
if (coords is null) return; | |
if (_lastChangedCoords != null && _lastChangedCoords.SequenceEqual(coords)) return; | |
_gol![coords[0], coords[1]] = _gol![coords[0], coords[1]] > 0 ? 0 : 1; | |
_lastChangedCoords = coords; | |
_gol!.RefreshCellsColor(); | |
} | |
private void Canvas_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { | |
DrawLineTick(e.GetPosition(GoLCanvas)); | |
} | |
private void GoLCanvas_OnMouseMove(object sender, MouseEventArgs e) { | |
if (e.LeftButton == MouseButtonState.Pressed) { | |
DrawLineTick(e.GetPosition(GoLCanvas)); | |
} else { | |
_lastChangedCoords = null; | |
} | |
} | |
private void GoLCanvas_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { | |
_lastChangedCoords = null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment