Initial commit

This commit is contained in:
2026-05-07 03:23:56 +00:00
commit 5e8575f42a
42 changed files with 2330 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>
+101
View File
@@ -0,0 +1,101 @@
Name,Apgcode
Block,xs4_33
Blinker,xp2_7
Beehive,xs6_696
Glider,xq4_153
Loaf,xs7_2596
Boat,xs5_253
Ship,xs6_356
Tub,xs4_252
Pond,xs8_6996
Long boat,xs7_25ac
Toad,xp2_7e
Ship-tie,xs12_g8o653z11
Beacon,xp2_318c
Barge,xs6_25a4
Half-bakery,xs14_g88m952z121
Mango,xs8_69ic
Eater 1,xs7_178c
Lightweight spaceship,xq4_6frc
Long barge,xs8_25ak8
Aircraft carrier,xs6_39c
Pulsar,xp3_co9nas0san9oczgoldlo0oldlogz1047210127401
Paperclip,xs14_69bqic
Middleweight spaceship,xq4_27dee6
Long ship,xs8_35ac
Integral sign,xs9_31ego
Shillelagh,xs8_3pm
Boat-tie,xs10_g8o652z01
Snake,xs6_bd
Big S,xs14_g88b96z123
Bipond,xs16_g88m996z1221
Trans-boat with tail,xs9_178ko
Boat tie ship,xs11_g8o652z11
Hat,xs9_4aar
Very long ship,xs10_35ako
Heavyweight spaceship,xq4_27deee6
Very long boat,xs9_25ako
Tub with tail,xs8_178k8
Mirrored table,xs12_raar
Dead spark coil,xs18_rhe0ehr
Canoe,xs8_312ko
Beehive on dock,xs16_j1u0696z11
Cis-mirrored bun,xs14_6970796
Moose antlers,xs15_354cgc453
Block on table,xs10_32qr
Block on dock,xs14_j1u066z11
Scorpion,xs16_69egmiczx1
Beehive with tail,xs10_178kk8
Twin hat,xs17_2ege1ege2
Loop,xs10_69ar
Long snake,xs7_3lo
Fourteener,xs14_69bo8a6
Pentadecathlon,xp15_4r4z4r4
Cis-mirrored bookend,xs14_39e0e93
Cis-boat with tail,xs9_178kc
Cis-rotated bookend,xs14_6is079c
Elevener,xs11_g0s453z11
Mirrored dock,xs20_3lkkl3z32w23
Block on cap,xs12_330f96
Trans-loaf with tail,xs11_ggm952z1
Cis-shillelagh,xs10_358gkc
Trans-mirrored bun,xs14_69e0eic
Clock,xp2_2a54
Trans-block on long bookend,xs12_330fho
Block-laying switch engine,yl144_1_16_afb5f3db909e60548f086e22ee3353ac
Prodigal,xs10_g0s252z11
Broken snake,xs10_0drz32
Trans-bookend and bun,xs14_39e0eic
Eater with nine,xs12_178c453
Block on cover,xs12_178br
Cis-boat on dock,xs15_j1u06a4z11
Cis-block on long bookend,xs12_3hu066
Very long snake,xs8_31248c
Boat with long tail,xs10_3215ac
Long shillelagh,xs9_312453
Beehive at loaf,xs13_g88m96z121
Trans-bun and wing,xs15_259e0eic
Long integral,xs10_3542ac
Tub with long tail,xs9_25a84c
Cis-bookend and bun,xs14_39e0e96
Hook with tail,xs8_32qk
Loaf siamese loaf,xs11_69lic
Long canoe,xs9_g0g853z11
Eleven loop,xs11_178jd
Trans-loaf on table,xs13_4a960ui
Cis-loaf with tail,xs11_178kic
Symmetric scorpion,xs16_69bob96
Claw with tail,xs10_1784ko
Bee hat,xs15_3lkm96z01
Cis-mirrored dove,xs18_69is0si96
Trans-rotated bun,xs14_g8o0e96z121
Glider-producing switch engine,yl384_1_59_7aeb1999980c43b4945fb7fcdb023326
Cis-mirrored wing,xs16_259e0e952
Trans-snake on bun,xs13_69e0mq
Boat tie eater tail,xs12_256o8a6
Snorkel loop,xs12_2egm93
Beehive on table,xs12_6960ui
Cis-boat on table,xs11_2530f9
Trans-barge with tail,xs10_ggka52z1
Trans-boat on dock,xs15_3lk453z121
Beehive on cap,xs14_6960uic
1 Name Apgcode
2 Block xs4_33
3 Blinker xp2_7
4 Beehive xs6_696
5 Glider xq4_153
6 Loaf xs7_2596
7 Boat xs5_253
8 Ship xs6_356
9 Tub xs4_252
10 Pond xs8_6996
11 Long boat xs7_25ac
12 Toad xp2_7e
13 Ship-tie xs12_g8o653z11
14 Beacon xp2_318c
15 Barge xs6_25a4
16 Half-bakery xs14_g88m952z121
17 Mango xs8_69ic
18 Eater 1 xs7_178c
19 Lightweight spaceship xq4_6frc
20 Long barge xs8_25ak8
21 Aircraft carrier xs6_39c
22 Pulsar xp3_co9nas0san9oczgoldlo0oldlogz1047210127401
23 Paperclip xs14_69bqic
24 Middleweight spaceship xq4_27dee6
25 Long ship xs8_35ac
26 Integral sign xs9_31ego
27 Shillelagh xs8_3pm
28 Boat-tie xs10_g8o652z01
29 Snake xs6_bd
30 Big S xs14_g88b96z123
31 Bipond xs16_g88m996z1221
32 Trans-boat with tail xs9_178ko
33 Boat tie ship xs11_g8o652z11
34 Hat xs9_4aar
35 Very long ship xs10_35ako
36 Heavyweight spaceship xq4_27deee6
37 Very long boat xs9_25ako
38 Tub with tail xs8_178k8
39 Mirrored table xs12_raar
40 Dead spark coil xs18_rhe0ehr
41 Canoe xs8_312ko
42 Beehive on dock xs16_j1u0696z11
43 Cis-mirrored bun xs14_6970796
44 Moose antlers xs15_354cgc453
45 Block on table xs10_32qr
46 Block on dock xs14_j1u066z11
47 Scorpion xs16_69egmiczx1
48 Beehive with tail xs10_178kk8
49 Twin hat xs17_2ege1ege2
50 Loop xs10_69ar
51 Long snake xs7_3lo
52 Fourteener xs14_69bo8a6
53 Pentadecathlon xp15_4r4z4r4
54 Cis-mirrored bookend xs14_39e0e93
55 Cis-boat with tail xs9_178kc
56 Cis-rotated bookend xs14_6is079c
57 Elevener xs11_g0s453z11
58 Mirrored dock xs20_3lkkl3z32w23
59 Block on cap xs12_330f96
60 Trans-loaf with tail xs11_ggm952z1
61 Cis-shillelagh xs10_358gkc
62 Trans-mirrored bun xs14_69e0eic
63 Clock xp2_2a54
64 Trans-block on long bookend xs12_330fho
65 Block-laying switch engine yl144_1_16_afb5f3db909e60548f086e22ee3353ac
66 Prodigal xs10_g0s252z11
67 Broken snake xs10_0drz32
68 Trans-bookend and bun xs14_39e0eic
69 Eater with nine xs12_178c453
70 Block on cover xs12_178br
71 Cis-boat on dock xs15_j1u06a4z11
72 Cis-block on long bookend xs12_3hu066
73 Very long snake xs8_31248c
74 Boat with long tail xs10_3215ac
75 Long shillelagh xs9_312453
76 Beehive at loaf xs13_g88m96z121
77 Trans-bun and wing xs15_259e0eic
78 Long integral xs10_3542ac
79 Tub with long tail xs9_25a84c
80 Cis-bookend and bun xs14_39e0e96
81 Hook with tail xs8_32qk
82 Loaf siamese loaf xs11_69lic
83 Long canoe xs9_g0g853z11
84 Eleven loop xs11_178jd
85 Trans-loaf on table xs13_4a960ui
86 Cis-loaf with tail xs11_178kic
87 Symmetric scorpion xs16_69bob96
88 Claw with tail xs10_1784ko
89 Bee hat xs15_3lkm96z01
90 Cis-mirrored dove xs18_69is0si96
91 Trans-rotated bun xs14_g8o0e96z121
92 Glider-producing switch engine yl384_1_59_7aeb1999980c43b4945fb7fcdb023326
93 Cis-mirrored wing xs16_259e0e952
94 Trans-snake on bun xs13_69e0mq
95 Boat tie eater tail xs12_256o8a6
96 Snorkel loop xs12_2egm93
97 Beehive on table xs12_6960ui
98 Cis-boat on table xs11_2530f9
99 Trans-barge with tail xs10_ggka52z1
100 Trans-boat on dock xs15_3lk453z121
101 Beehive on cap xs14_6960uic
+36
View File
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
namespace GameOfLife.Entities
{
public class Cell : Tuple<short, short>
{
public Cell(short x, short y) : base(x, y)
{
}
public Cell(short x, int y) : base(x, (short)y) { }
public Cell(int x, short y) : base((short)x, y) { }
public Cell(int x, int y) : base((short)x, (short)y) { }
public static Cell operator +(Cell cell1, Cell cell2)
=> new Cell(cell1.Item1 + cell2.Item1, cell1.Item2 + cell2.Item2);
public static Cell operator -(Cell cell1, Cell cell2)
=> new Cell(cell1.Item1 - cell2.Item1, cell1.Item2 - cell2.Item2);
public Cell Negate()
=> new Cell(-Item1, -Item2);
public IEnumerable<Cell> NeighborCells => new []
{
new Cell(Item1 - 1, Item2 - 1), new Cell(Item1 - 1, Item2), new Cell(Item1 - 1, Item2 + 1),
new Cell(Item1, Item2 - 1), new Cell(Item1, Item2 + 1),
new Cell(Item1 + 1, Item2 - 1), new Cell(Item1 + 1, Item2), new Cell(Item1 + 1, Item2 + 1),
};
public override string ToString()
{
return $"[{Item1}, {Item2}]";
}
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace GameOfLife.Entities
{
public class Matcher
{
private Cell Key;
private Cell Left;
}
}
+236
View File
@@ -0,0 +1,236 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace GameOfLife.Entities
{
public class Pattern : ReadOnlyCollection<Cell>
{
public Pattern(IEnumerable<Cell> pattern) : base(pattern.OrderBy(c => c.Item2).ThenBy(c => c.Item1).ToList())
{
// https://stackoverflow.com/a/263416/99492
unchecked
{
_hashCodeCache =
this //.OrderBy(c => c)
.Aggregate(23, (hash, c)
=> hash * 37 + c.GetHashCode());
}
}
public Cell GetBoundaryMin() =>
new Cell(
this.Min(c => c.Item1) - 1,
this.Min(c => c.Item2) - 1);
public Cell GetBoundaryMax() =>
new Cell(
this.Max(c => c.Item1) + 1,
this.Max(c => c.Item2) + 1);
public Cell GetFirstRowFirstCell() =>
this.OrderBy(c => c.Item2).ThenBy(c => c.Item1).First();
public bool ContainsAll(IEnumerable<Cell> cells) => cells.All(Contains);
public bool ContainsNone(IEnumerable<Cell> cells) => cells.All(c => !Contains(c));
public short GetMinY() =>
this.Min(c => c.Item2);
// All neighboring cells
public Pattern GetBoundary =>
this.SelectMany(cell => cell.NeighborCells).Distinct().ToPattern() - this;
public Pattern Rotate() =>
/* Rotate 90deg clockwise
* ..... ..... .oo.. ...o.
* ..o.. -> .oo.. -> ..o.. -> ..oo.
* ..oo. .o... ..... .....
* ..... ..... ..... .....
* [0,0] [0,0] [0,0] [0,0]
* [0,1] [-1,0] [0,-1] [1,0]
* [1,1] [-1,1] [-1,-1] [1,-1]
*/
this
.Select(cell
=> new Cell(-cell.Item2, cell.Item1))
.ToPattern();
public Pattern Rotate(int count)
{
var p = new Pattern(this);
for (var i = 0; i < count; i++)
p = p.Rotate();
return p.ToPattern();
}
public Pattern ReflectX() =>
/* ..... ..... ..oo.
* ..o.. ..o.. ..o..
* ..oo. .oo.. .....
* ..... ..... .....
* [0,0] [0,0] [0,0]
* [0,1] [0,1] [0,-1]
* [1,1] [-1,1] [1,-1]
*/
this
.Select(cell
=> new Cell((short)-cell.Item1, cell.Item2))
.ToPattern();
public Pattern ReflectY() =>
this
.Select(cell
=> new Cell((short)cell.Item1, (short)-cell.Item2))
.ToPattern();
// move pattern to deterministic position
public Pattern Normalize() =>
this.Select(c => c - GetFirstRowFirstCell()).ToPattern();
public Pattern Offset(Cell offsetCell) =>
this.Select(c => c + offsetCell).ToPattern();
// Subtract cells from another pattern
public static Pattern operator -(Pattern p1, Pattern p2)
=> p1.Where(c => !p2.Contains(c)).ToPattern();
// Adds cells from another pattern
public static Pattern operator +(Pattern p1, Pattern p2)
=> p1.Concat(p2).Distinct().ToPattern();
/// <summary>
/// Extracts coordinates from text encoded pattern, treating 'char c' as cells
/// </summary>
public static Pattern Extract(string text, char c = 'o')
=> Extract(text.SplitByLine(), c);
public static Pattern Extract(IEnumerable<string> text, char c = 'o')
=> ExtractCells(text, c).ToPattern();
private static IEnumerable<Cell> ExtractCells(IEnumerable<string> text, char c = 'o')
{
short y = 0;
foreach (var line in text)
{
for (short x = 0; x < line.Length; x++)
if (line[x] == c)
yield return new Cell(x, y);
y++;
}
}
public IEnumerable<string> ToGrid(char live = 'O', char dead = '.')
{
var boundaryMin = GetBoundaryMin();
var boundaryMax = GetBoundaryMax();
for (var y = boundaryMin.Item2; y < boundaryMax.Item2; y++)
{
var s = "";
for (var x = boundaryMin.Item1; x < boundaryMax.Item1; x++)
s += (Contains(new Cell(x, y)) ? live : dead) + " ";
yield return s;
}
}
public override bool Equals(object obj)
{
if (!(obj is Pattern pattern))
return false;
return this.All(pattern.Contains) && pattern.All(this.Contains);
}
public override int GetHashCode() => _hashCodeCache;
private readonly int _hashCodeCache;
public IEnumerable<Pattern> FindPatterns(Projections projections)
{
var livingCells = new ConcurrentBag<Cell>(this);
var foundPatterns = new ConcurrentBag<Pattern>();
Parallel.ForEach(projections,
projection =>
{
foreach (var livingCell in livingCells)
{
var patternProjection
= projection.Select(c => livingCell + c).ToArray();
var patternMatch = ContainsAll(patternProjection);
if (!patternMatch)
continue;
// check if surrounding cells are not alive
var boundaryProjection
= projections.BoundaryCells[projection].Select(c => livingCell + c).ToArray();
var boundaryMatch = ContainsNone(boundaryProjection);
if (boundaryMatch)
foundPatterns.Add(patternProjection.ToPattern());
// shorten search time by removing these cells
// from examination for other patterns
foreach (var foundsCell in patternProjection)
{
// ReSharper disable once NotAccessedVariable
var cell = new Cell(foundsCell.Item1, foundsCell.Item2);
livingCells.TryTake(out cell);
}
}
});
return foundPatterns;
}
public IEnumerable<Pattern> FindPatterns_Serial(Projections projections)
{
var cells = new List<Cell>(this);
var foundPatterns = new ConcurrentBag<Pattern>();
foreach (var projection in projections)
{
foreach (var livingCell in cells)
{
var patternProjection
= projection.Select(c => livingCell + c).ToArray();
var patternMatch = ContainsAll(patternProjection);
if (!patternMatch)
continue;
// check if surrounding cells are dead
var boundaryProjection
= projections.BoundaryCells[projection].Select(c => livingCell + c).ToArray();
var boundaryMatch = ContainsNone(boundaryProjection);
if (boundaryMatch)
foundPatterns.Add(patternProjection.ToPattern());
// shorten search time by removing these cells
// from examination for other patterns
//foreach (var foundsCell in patternProjection)
//{
// // ReSharper disable once NotAccessedVariable
// //var cell = new Cell(foundsCell.Item1, foundsCell.Item2);
// cells.Remove(foundsCell);
//}
}
}
return foundPatterns;
}
public IEnumerable<Pattern> FindPattern(Pattern pattern)
{
return FindPatterns(new Projections(pattern));
}
public override string ToString()
{
return string.Join(", ", this);
}
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace GameOfLife.Entities
{
public enum PatternType
{
StillLife,
Oscillator,
Spaceship,
Periodic,
Methuselah,
Diehard,
Megasized,
//Oversized,
//Chaotic
}
}
+32
View File
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace GameOfLife.Entities
{
public class Projections : ReadOnlyCollection<Pattern>
{
public Dictionary<Pattern, Pattern> BoundaryCells { get; }
public Projections(IEnumerable<Pattern> pattern)
: base(pattern.SelectMany(GeneratePatterns).Distinct().ToList())
{
BoundaryCells = this.ToDictionary(p => p, p => p.GetBoundary);
}
public Projections(Pattern pattern) : this(new[] { pattern }) { }
private static List<Pattern> GeneratePatterns(Pattern pattern) =>
new[]
{
pattern.Normalize(),
pattern.Rotate(1).Normalize(),
pattern.Rotate(2).Normalize(),
pattern.Rotate(3).Normalize(),
pattern.ReflectX().Normalize(),
pattern.ReflectY().Normalize(),
pattern.ReflectX().Rotate().Normalize(),
pattern.ReflectY().Rotate().Normalize()
}.Distinct().ToList();
}
}
+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0BEA14A8-8E11-4A18-BD7D-628D3F1561B0}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GameOfLife</RootNamespace>
<AssemblyName>GameOfLife</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Entities\Cell.cs" />
<Compile Include="Entities\Class1.cs" />
<Compile Include="PatternLibrary.cs" />
<Compile Include="Entities\PatternType.cs" />
<Compile Include="IO\ApgcodeDecoder.cs" />
<Compile Include="IO\ApgcodePatternMetaData.cs" />
<Compile Include="IO\RlePatternMetaData.cs" />
<Compile Include="Neighborhood.cs" />
<Compile Include="Search\Node.cs" />
<Compile Include="Util.cs" />
<Compile Include="LifeBase.cs" />
<Compile Include="LifeArray.cs" />
<Compile Include="LifeHashSet.cs" />
<Compile Include="ILife.cs" />
<Compile Include="Entities\Pattern.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="IO\PatternMetadata.cs" />
<Compile Include="IO\RleDecoder.cs" />
<Compile Include="Entities\Projections.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<EmbeddedResource Include="CommonPatterns.csv" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
+176
View File
@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GameOfLife.Entities;
namespace GameOfLife
{
public abstract class LifeBase : ILife
{
public int Generation { get; protected set; }
public int Population { get; protected set; }
public abstract bool IsCellAlive(Cell cell);
public virtual bool IsCellAlive(short x, short y) => IsCellAlive(new Cell(x,y));
public bool AreAllCellsAlive(IEnumerable<Cell> cells) => cells.All(IsCellAlive);
public bool AreAllCellsDead(IEnumerable<Cell> cells) => cells.All(c => !IsCellAlive(c));
public bool ToggleCell(Cell cell)
{
var r = ToggleCell_Internal(cell);
Population += r ? 1 : -1;
return r;
}
protected abstract bool ToggleCell_Internal(Cell cell);
public void IncrementGeneration()
{
Population = IncrementGeneration_Internal();
Generation++;
}
protected abstract int IncrementGeneration_Internal();
public abstract IEnumerable<Cell> LivingCells { get; }
public static readonly short[,] NeighborOffsets =
{
{ -1, -1 }, { -1, 0 }, { -1, 1 },
{ 0, -1 }, { 0, 1 },
{ 1, -1 }, { 1, 0 }, { 1, 1 },
};
public byte GetNeighborCount(short x, short y)
{
byte numberOfNeighbors = 0;
for (var i = 0; i < 8; i++)
{
numberOfNeighbors +=
IsCellAlive((short)(x + NeighborOffsets[i,0]), (short)(y + NeighborOffsets[i,1]))
? (byte)1
: (byte)0;
}
return numberOfNeighbors;
}
protected bool ShouldLive(short x, short y)
{
var numberOfNeighbors = GetNeighborCount(x, y);
var isAlive = IsCellAlive(x, y);
return GameRule(isAlive, numberOfNeighbors);
}
protected bool GameRule(bool isAlive, int neighborCount)
{
switch (isAlive)
{
case false
when neighborCount == (byte)3:
case true
when neighborCount == (byte)2
|| neighborCount == (byte)3:
return true;
default:
return false;
}
}
public Tuple<Cell, short>[] GetNeighborField() => GetNeighborField(LivingCells);
public static Tuple<Cell, short>[] GetNeighborField(IEnumerable<Cell> world)
{
var neighborCount = new Dictionary<Cell, short>();
foreach (var cell in world)
{
for (var i = 0; i < 8; i++)
{
var xOffset = LifeBase.NeighborOffsets[i, 0];
var yOffset = LifeBase.NeighborOffsets[i, 1];
var neighbor =
new Cell(
(cell.Item1 + xOffset),
(cell.Item2 + yOffset));
if (!neighborCount.ContainsKey(neighbor))
neighborCount[neighbor] = 1;
else
neighborCount[neighbor] += 1;
}
}
return neighborCount.Select(pair => Tuple.Create(pair.Key, pair.Value)).ToArray();
}
protected static Tuple<Cell, short>[] GetNeighborField_Parallel(IEnumerable<Cell> world)
{
//var neighborCount = new Dictionary<Tuple<int, int>, int>();
var neighbors = new List<Cell>();
Parallel.ForEach(
world,
() => new List<Cell>(),
(cell, loop, localNeighbors) =>
{
for (var i = 0; i < 8; i++)
{
var neighbor =
new Cell(
(cell.Item1 + LifeBase.NeighborOffsets[i, 0]),
(cell.Item2 + LifeBase.NeighborOffsets[i, 1]));
localNeighbors.Add(neighbor);
}
return localNeighbors;
},
finalNeighbors =>
{
lock(neighbors)
neighbors.AddRange(finalNeighbors);
});
var neighborCount =
neighbors
.GroupBy(n => n)
.Select(n => Tuple.Create(n.Key, (short) n.Count()))
.ToArray();
return neighborCount;
}
public IEnumerable<Pattern> FindPattern(Pattern pattern)
{
var variations = new Variations(pattern);
var livingCells = LivingCells.ToList();
foreach (var patternVar in variations)
{
var foundsCells = new List<Cell>();
foreach (var livingCell in livingCells)
{
var patternProjection
= patternVar.Select(c => livingCell + c).ToArray();
var patternMatch = AreAllCellsAlive(patternProjection);
if (!patternMatch)
continue;
// check if surrounding cells are not alive
var boundaryProjection
= variations.BoundaryCells[patternVar].Select(c => livingCell + c).ToArray();
var boundaryMatch = AreAllCellsDead(boundaryProjection);
if (boundaryMatch)
yield return patternProjection.ToPattern();
foundsCells.AddRange(patternProjection);
}
livingCells.RemoveAll(c => foundsCells.Contains(c));
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using GameOfLife.Entities;
namespace GameOfLife
{
public interface ILife
{
int Generation { get; }
int Population { get; }
bool IsCellAlive(Cell cell);
bool AllAlive(IEnumerable<Cell> cells);
bool AllDead(IEnumerable<Cell> cells);
bool ToggleCell(Cell cell);
void IncrementGeneration();
IEnumerable<Cell> LivingCells { get; }
}
}
+70
View File
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using GameOfLife.Entities;
namespace GameOfLife.IO
{
// https://conwaylife.com/wiki/Apgcode
public class ApgcodeDecoder
{
private static readonly Regex PrefixRegex = new Regex(@"(?<header>.+(?=_))?_?(?<code>.+)");
private static readonly Regex ExpansionRegex = new Regex(@"y(\w+)");
public Pattern Pattern { get; }
public PatternMetadata Metadata { get; }
public ApgcodeDecoder(string apgcode, string name = null)
{
var match = PrefixRegex.Match(apgcode);
if (!match.Success)
throw new ArgumentException("Invalid apgcode");
var header = match.Groups["header"].Value;
var code = match.Groups["code"].Value;
if (!string.IsNullOrEmpty(header))
Metadata = new ApgcodePatternMetaData(header,name);
var expanded = ExpandAbbreviations(code);
Pattern = DecodeCells(expanded).ToPattern();
}
private IEnumerable<Cell> DecodeCells(string expanded)
{
var rowNum = 0;
foreach (var strip in expanded.Split('z'))
{
var colBits = strip.Select(ConvertToUint).ToArray();
for (var i = 0; i < 5; i++)
{
var colNum = 0;
foreach (var colBit in colBits)
{
if ((colBit >> i & 0b_1) == 1)
yield return new Cell(colNum, rowNum);
colNum++;
}
rowNum++;
}
}
}
public static string ExpandAbbreviations(string apgcode)
{
apgcode = apgcode.Replace("w", "00");
apgcode = apgcode.Replace("x", "000");
apgcode = ExpansionRegex.Replace(apgcode, m =>
new string('0', 4 + Convert.ToInt32(ConvertToUint(m.Groups[1].Value[0]))));
return apgcode;
}
public static uint ConvertToUint(char c)
{
if (c >= '0' && c <= '9')
return (uint)(c - '0');
if (c >= 'a' && c <= 'z')
return (uint)(c - 'a' + 10);
throw new Exception("Cannot convert char " + c.ToString());
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Text.RegularExpressions;
using GameOfLife.Entities;
namespace GameOfLife.IO
{
public class ApgcodePatternMetaData : PatternMetadata
{
private static readonly Regex HeaderRegex = new Regex(@"(?<prefix>\w{2})(?<suffix>.*)");
public ApgcodePatternMetaData(string header, string name=null)
{
if (name != null)
Name = name;
var match = HeaderRegex.Match(header);
Type = Prefix(match.Groups["prefix"].Value);
Comments = new [] { match.Groups["suffix"].Value };
}
private PatternType Prefix(string prefix)
{
switch (prefix)
{
case "xs": return PatternType.StillLife;
case "xp": return PatternType.Oscillator;
case "xq": return PatternType.Spaceship;
case "yl": return PatternType.Periodic;
case "methuselah" : return PatternType.Methuselah;
case "messless" : return PatternType.Diehard;
case "megasized" : return PatternType.Megasized;
default:
throw new ArgumentException($"Unknown prefix: {prefix}");
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
using GameOfLife.Entities;
namespace GameOfLife.IO
{
public abstract class PatternMetadata
{
public string Name { protected set; get; }
public PatternType Type { get; protected set; }
public string Rules { get; protected set; }
public string[] Comments { get; protected set; }
public int Width { get; protected set; }
public int Height { get; protected set; }
}
}
+82
View File
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using GameOfLife.Entities;
namespace GameOfLife.IO
{
public class RleDecoder
{
public Pattern Pattern { get; }
public PatternMetadata Metadata { get; }
public RleDecoder(string filename) : this(new FileInfo(filename)) { }
public RleDecoder(FileSystemInfo fi) : this(GetLines(fi)) { }
public RleDecoder(IEnumerable<string> encodedRle)
{
var groups =
(from line in encodedRle
let trimmed = line.Trim()
where
!string.IsNullOrEmpty(trimmed)
group line by line.StartsWith("#") into g
select g
).ToArray();
var comments = groups.First(g => g.Key);
var nonComments = groups.First(g => !g.Key).ToArray();
Metadata = new RlePatternMetaData(nonComments.First(), comments);
var rleData = string.Join("", nonComments.Skip(1));
Pattern = Pattern.Extract(ExpandRle(rleData).ToArray());
}
/// <summary>
/// Returns RLE encoded string into decoded expansion, breaking lines into an enumeration
/// </summary>
private static IEnumerable<string> ExpandRle(string rleData)
{
if (!rleData.Contains("!"))
throw new ArgumentException("RLE pattern did not contain terminating character '!'");
var encodedRle
= rleData.Substring(0, rleData.IndexOf("!", StringComparison.Ordinal));
var rleDecodingRegex = new Regex(@"(?<count>\d*)(?<char>[a-z$])");
var matches = rleDecodingRegex.Matches(encodedRle);
var decoded = string.Empty;
foreach (Match match in matches)
{
var countStr = match.Groups["count"].Value;
var count = string.IsNullOrEmpty(countStr) ? 1 : int.Parse(countStr);
var c = match.Groups["char"].Value.First();
for (var i = 0; i < count; i++)
{
switch (c)
{
case '$':
yield return decoded;
decoded = string.Empty;
break;
default:
decoded += c;
break;
}
}
}
yield return decoded;
}
private static IEnumerable<string> GetLines(FileSystemInfo fi)
{
if (!fi.Exists)
throw new FileNotFoundException(fi.FullName);
return File.ReadAllLines(fi.FullName);
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace GameOfLife.IO
{
public class RlePatternMetaData : PatternMetadata
{
public RlePatternMetaData(string line, IEnumerable<string> comments)
{
Comments = comments.ToArray();
var mdLine = Regex.Replace(line, @"\s+", "");
var props = mdLine.Split(',');
if (props.Length < 2)
throw new ArgumentException("Line does not contain two properties");
var mp = new Regex(@"(\w+)=(.*)");
foreach (var prop in props)
{
var capture = mp.Match(prop).Groups;
var name = capture[1].Value;
var val = capture[2].Value;
switch (name)
{
case "x":
Width = int.Parse(val);
break;
case "y":
Height = int.Parse(val);
break;
// TODO: decode the rules
case "rules":
Rules = val;
break;
}
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using GameOfLife.Entities;
namespace GameOfLife
{
public class LifeArray : LifeBase
{
protected bool[,] World;
private bool[,] _nextGeneration;
//private Task _processTask;
public int SizeX { get; private set; }
public int SizeY { get; private set; }
public override bool IsCellAlive(Cell cell)
{
return IsCellAlive(cell.Item1, cell.Item2);
}
public override bool IsCellAlive(short x, short y)
{
// first check boundaries
if (x < 0 || x >= SizeX || y < 0 || y >= SizeY)
return false;
return IsCellAlive_NoBoundaryCheck(x, y);
}
private bool IsCellAlive_NoBoundaryCheck(short x, short y) => World[x, y];
public LifeArray(Pattern pattern)
: this(pattern.GetBoundaryMax().Item1, pattern.GetBoundaryMax().Item2)
{
foreach (var cell in pattern)
{
ToggleCell(cell);
}
}
public LifeArray(Cell sizes) : this (sizes.Item1, sizes.Item2) { }
public LifeArray(short sizeX, short sizeY)
{
if (sizeX <= 0 || sizeY <= 0) throw new ArgumentOutOfRangeException("sizeX", "Size must be greater than zero");
SizeX = sizeX;
SizeY = sizeY;
World = new bool[sizeX, sizeY];
_nextGeneration = new bool[sizeX, sizeY];
}
protected override bool ToggleCell_Internal(Cell cell)
{
return World[cell.Item1, cell.Item2] = !World[cell.Item1, cell.Item2];
}
protected override int IncrementGeneration_Internal()
{
for (short x = 0; x < SizeX; x++)
for (short y = 0; y < SizeY; y++)
{
var shouldLive = ShouldLive(x, y);
_nextGeneration[x, y] = shouldLive;
}
// now flip the back buffer so we can start processing on the next generation
var flip = _nextGeneration;
_nextGeneration = World;
World = flip;
return Population;
}
public override IEnumerable<Cell> LivingCells
{
get
{
for (short y=0; y<SizeY;y++)
for (short x =0; x<SizeX;x++)
if (IsCellAlive_NoBoundaryCheck(x, y))
yield return new Cell(x, y);
}
}
}
}
+96
View File
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameOfLife.Entities;
namespace GameOfLife
{
public abstract class LifeBase : ILife
{
public int Generation { get; protected set; }
public int Population { get; protected set; }
public abstract bool IsCellAlive(Cell cell);
public virtual bool IsCellAlive(short x, short y) => IsCellAlive(new Cell(x,y));
public bool AllAlive(IEnumerable<Cell> cells) => cells.All(IsCellAlive);
public bool AllDead(IEnumerable<Cell> cells) => cells.All(c => !IsCellAlive(c));
private IReadOnlyList<Tuple<Cell, short>> _neighborhoodField;
public IReadOnlyList<Tuple<Cell, short>> NeighborhoodField =>
// cache values this expensive operation
_neighborhoodField ?? (_neighborhoodField = Neighborhood.GetNeighborField(LivingCells).ToList());
private Pattern _livingCellPattern;
public Pattern LivingCellPattern =>
_livingCellPattern ?? (_livingCellPattern = LivingCells.ToPattern());
public bool ToggleCell(Cell cell)
{
var alive = ToggleCell_Internal(cell);
Population += alive ? 1 : -1;
return alive;
}
protected abstract bool ToggleCell_Internal(Cell cell);
public void IncrementGeneration()
{
_neighborhoodField = null;
_livingCellPattern = null;
Population = IncrementGeneration_Internal();
Generation++;
}
protected abstract int IncrementGeneration_Internal();
public abstract IEnumerable<Cell> LivingCells { get; }
public static readonly short[,] NeighborOffsets =
{
{ -1, -1 }, { -1, 0 }, { -1, 1 },
{ 0, -1 }, { 0, 1 },
{ 1, -1 }, { 1, 0 }, { 1, 1 },
};
public byte GetNeighborCount(short x, short y)
{
byte numberOfNeighbors = 0;
for (var i = 0; i < 8; i++)
{
numberOfNeighbors +=
IsCellAlive((short)(x + NeighborOffsets[i,0]), (short)(y + NeighborOffsets[i,1]))
? (byte)1
: (byte)0;
}
return numberOfNeighbors;
}
protected bool ShouldLive(short x, short y)
{
var numberOfNeighbors = GetNeighborCount(x, y);
var isAlive = IsCellAlive(x, y);
return LifeRule(isAlive, numberOfNeighbors);
}
protected bool LifeRule(bool isAlive, int neighborCount)
{
switch (isAlive)
{
case false
when neighborCount == 3:
case true
when neighborCount == 2
|| neighborCount == 3:
return true;
default:
return false;
}
}
}
}
+83
View File
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GameOfLife.Entities;
namespace GameOfLife
{
public class LifeHashSet : LifeBase
{
protected HashSet<Cell> World;
public LifeHashSet()
{
World = new HashSet<Cell>();
}
public LifeHashSet(IEnumerable<Cell> cells) : this()
{
foreach (var cell in cells)
World.Add(cell);
}
public override bool IsCellAlive(Cell cell) => World.Contains(cell);
protected override bool ToggleCell_Internal(Cell cell)
{
if (IsCellAlive(cell))
{
World.Remove(cell);
return false;
}
World.Add(cell);
return true;
}
protected override int IncrementGeneration_Internal()
{
World = GetNextGeneration_Parallel(NeighborhoodField);
return World.Count;
}
private HashSet<Cell> GetNextGeneration_Serial(IEnumerable<Tuple<Cell, short>> field)
{
return
new HashSet<Cell>(
field.Where(
cellNeighborCount =>
LifeRule(World.Contains(cellNeighborCount.Item1), cellNeighborCount.Item2))
.Select(c => c.Item1));
}
private HashSet<Cell> GetNextGeneration_Parallel(IEnumerable<Tuple<Cell, short>> field)
{
var nextGeneration = new HashSet<Cell>();
Parallel.ForEach(
field,
() => new HashSet<Cell>(),
(cellNeighborCount, loop, localNextGen) =>
{
var cell = cellNeighborCount.Item1;
var neighborCount = cellNeighborCount.Item2;
if (LifeRule(World.Contains(cell), neighborCount))
localNextGen.Add(cell);
return localNextGen;
},
globalNextGen =>
{
lock (nextGeneration)
foreach (var cell in globalNextGen)
nextGeneration.Add(cell);
}
);
return nextGeneration;
}
public override IEnumerable<Cell> LivingCells => World;
}
}
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GameOfLife.Entities;
namespace GameOfLife
{
public static class Neighborhood
{
public static Tuple<Cell, short>[] GetNeighborField(IEnumerable<Cell> world)
{
var neighbors = new List<Cell>();
Parallel.ForEach(
world,
() => new List<Cell>(),
(cell, loop, localNeighbors) =>
{
localNeighbors.AddRange(cell.NeighborCells);
return localNeighbors;
},
globalNeighbors =>
{
lock(neighbors)
neighbors.AddRange(globalNeighbors);
});
var neighborCount =
neighbors
.GroupBy(n => n)
.Select(n => Tuple.Create(n.Key, (short)n.Count()))
.ToArray();
return neighborCount;
}
public static Tuple<Cell, short>[] GetNeighborField_Serial(IEnumerable<Cell> world)
{
var neighborCount = new Dictionary<Cell, short>();
foreach (var cell in world)
{
foreach (var neighbor in cell.NeighborCells)
{
if (!neighborCount.ContainsKey(neighbor))
neighborCount[neighbor] = 1;
else
neighborCount[neighbor] += 1;
}
}
return neighborCount.Select(pair => Tuple.Create(pair.Key, pair.Value)).ToArray();
}
}
}
+89
View File
@@ -0,0 +1,89 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using GameOfLife.Entities;
using GameOfLife.IO;
namespace GameOfLife
{
public class PatternLibrary
{
public const int PatternLoadLimit = 100;
public const int OscillationMaxPeriod = 50;
public IReadOnlyList<Tuple<PatternMetadata, Projections>> PatternProjections;
public IReadOnlyList<Tuple<PatternMetadata, Pattern>> Patterns;
public IReadOnlyDictionary<Pattern, PatternMetadata> PatternToMetadata;
public PatternLibrary()
{
Patterns = ExtractPatterns(GetPatternFileLines()).AsReadOnly();
PatternProjections = ExtractOscillationsAndProjections(Patterns);
PatternToMetadata = Patterns.ToDictionary(t => t.Item2, t => t.Item1);
}
private static List<Tuple<PatternMetadata, Projections>> ExtractOscillationsAndProjections(IEnumerable<Tuple<PatternMetadata, Pattern>> patterns)
{
var results =
from pattern in patterns.AsParallel()
let life = new LifeHashSet(pattern.Item2)
let oscillation = life.FindOscillation(OscillationMaxPeriod)
let variations = new Projections(oscillation)
select Tuple.Create(pattern.Item1, variations);
return results.ToList();
}
private static List<Tuple<PatternMetadata, Pattern>> ExtractPatterns(IEnumerable<string> fileLines)
{
// first line is the header
var pattern =
from line in fileLines.Skip(1).Take(PatternLoadLimit)
select line.Split(',')
into split
let name = split[0].Trim()
let apgCode = split[1].Trim()
select new ApgcodeDecoder(apgCode, name)
into decoder
select Tuple.Create(decoder.Metadata, decoder.Pattern.Normalize());
var extractPatterns = pattern.ToList();
// ensure this patterns are all unique
var duplicates =
string.Join(", ",
extractPatterns
.GroupBy(tuple => tuple)
.Where(tuples => tuples.Count() > 1)
.Select(t => t.Key.Item1.Name));
if (duplicates.Length > 0)
throw new Exception($"Duplicate patterns: " + duplicates);
return extractPatterns;
}
private static IEnumerable<string> GetPatternFileLines()
{
var assembly = Assembly.GetExecutingAssembly();
//Getting names of all embedded resources
var allResourceNames = assembly.GetManifestResourceNames();
var resourceName = allResourceNames.First(n => n.Contains("CommonPatterns.csv"));
var fileLines = new List<string>();
using (var stream = assembly.GetManifestResourceStream(resourceName))
if (stream != null)
using (var tx = new StreamReader(stream))
{
while (!tx.EndOfStream)
fileLines.Add(tx.ReadLine());
}
return fileLines;
}
}
}
+36
View File
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("GameOfLife")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("GameOfLife")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("0bea14a8-8e11-4a18-bd7d-628d3f1561b0")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Threading.Tasks;
using GameOfLife.Entities;
using GameOfLife.IO;
namespace GameOfLife.Search
{
public class LibrarySearcher
{
private readonly PatternLibrary _patternLibrary;
private Dictionary<Cell, List<Pattern>> _cellPatterns;
public LibrarySearcher(PatternLibrary patternLibrary)
{
_patternLibrary = patternLibrary;
var cellPatterns =
from patternProjections in _patternLibrary.PatternProjections
from projection in patternProjections.Item2
from cell in projection
group projection by cell into cellGroups
select cellGroups;
_cellPatterns = cellPatterns.ToDictionary(t => t.Key, t => t.ToList());
}
//public IEnumerable<Tuple<PatternMetadata, Pattern[]>> MatchLibraryPatternsLookup(IEnumerable<Cell> cells)
//{
// var pattern = cells.ToPattern();
//}
public IEnumerable<Tuple<PatternMetadata, Pattern[]>> MatchLibraryPatterns(IEnumerable<Cell> cells)
{
var inputCells = cells.ToPattern();
var matches = new ConcurrentBag<Tuple<PatternMetadata, Pattern[]>>();
Parallel.ForEach(_patternLibrary.PatternProjections, tuple =>
{
var patternMetadata = tuple.Item1;
var projections = tuple.Item2;
var foundPatterns = inputCells.FindPatterns_Serial(projections).ToArray();
if (foundPatterns.Any())
matches.Add(Tuple.Create(patternMetadata, foundPatterns.ToArray()));
}
);
return matches;
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using GameOfLife.Entities;
namespace GameOfLife
{
public static class Util
{
public static Pattern ToPattern(this IEnumerable<Cell> cells)
{
return new Pattern(cells.ToList());
}
//https://stackoverflow.com/a/63820524
public static IEnumerable<string> SplitByLine(this string str)
{
return Regex
.Split(str, @"((\r)+)?(\n)+((\r)+)?")
.Select(i => i.Trim())
.Where(i => !string.IsNullOrEmpty(i));
}
public static IEnumerable<Pattern> FindOscillation(this ILife gol, int maxPeriod = 50)
{
var list = new List<Pattern>();
while (gol.Generation < maxPeriod)
{
var currentPattern = gol.LivingCells.ToPattern().Normalize();
if (list.Contains(currentPattern))
return list;
list.Add(currentPattern);
gol.IncrementGeneration();
}
return list.Take(1); // didn't find any oscillations, return the first pattern
}
}
}
+3
View File
@@ -0,0 +1,3 @@
#Beehive
x = 4, y = 3, rule = B3/S23
b2o$o2bo$b2o!