[recovery mode] С LINQом по «Жизни»
Знаменитая игра Джона Конвея«Жизнь», благодаря своей простоте, занимательности и поучительность, реализовывалась программистами так много раз, что уступает вероятно только пресловутой сортировке «пузырьком».
Приведу, тем не менее, еще один вариант исполнения этой замечательной игры, целиком основанный на технологии LINQ в среде .NET — простой, компактный, без циклов и многомерных массивов.
Сразу оговорюсь, что задача достижения максимальной эффективности не ставилась.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace LinqLife
{
public struct Cell
{
public readonly int X, Y;
public Cell(int x, int y) { X = x; Y = y; }
}
public class Life : IEnumerable
{
private List _cells = new List();
private static readonly int[] Liveables = { 2, 3 };
public bool Next()
{
var died = _cells
.Where(cell => !Liveables.Contains(Count(cell))) // Условие смерти
.ToArray();
var born = _cells
.SelectMany(Ambit) // Все окружающие ячейки
.Distinct() // ...без дубликатов
.Except(_cells) // ...пустые
.Where(cell => Count(cell) == 3) // Условие рождения
.ToArray();
if (died.Length == 0 && born.Length == 0)
return false; // Нет изменений
_cells = _cells
.Except(died)
.Concat(born)
.ToList();
return _cells.Any(); // Не все еще умерли?
}
private int Count(Cell cell)
{
return Ambit(cell)
.Intersect(_cells)
.Count();
}
private static IEnumerable Ambit(Cell cell)
{
return Enumerable.Range(-1, 3)
.SelectMany(x => Enumerable.Range(-1, 3)
.Where(y => x != 0 || y != 0) // Кроме центральной клетки
.Select(y => new Cell(cell.X + x, cell.Y + y)));
}
public override string ToString()
{
if (_cells.Count == 0)
return string.Empty;
var xmin = _cells.Min(cell => cell.X);
var xmax = _cells.Max(cell => cell.X);
var ymin = _cells.Min(cell => cell.Y);
var ymax = _cells.Max(cell => cell.Y);
var matrix = Enumerable.Range(xmin, xmax - xmin + 1)
.Select(x => Enumerable.Range(ymin, ymax - ymin + 1)
.Select(y => _cells.Contains(new Cell(x, y))));
return string.Join(Environment.NewLine,
matrix.Select(row =>
string.Join("",
row.Select(b => b ? "X" : "."))));
}
public IEnumerator GetEnumerator()
{
return _cells.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(int x, int y)
{
_cells.Add(new Cell(x, y));
}
}
class Program
{
static void Main(string[] args)
{
var life = new Life { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 1, 2 }, { 1, 3 } };
var i = 0;
do
{
Console.WriteLine("#{0}\r\n{1}", i++, life);
Console.ReadLine();
} while (life.Next());
Console.WriteLine(life.Any() ? "* Stagnation!" : "* Extinction!");
}
}
}
| | | | |
Замечания к коду:
- Текущее состояние игры — список координат живых клеток, обновляемый на каждом шаге. В других реализациях обычно используется представление игрового поля в виде двумерного массива.
- Использование операций над множествами: объединение, пересечение, вычитание.
- Ни единого явного цикла (в классе Life).
- Использование struct для Cell позволяет не заботиться о сравнении ячеек и обеспечивает эффективное управление памятью (.NET нативно поддерживает ValueType в типизированных коллекциях).
- Практически неограниченный размер игрового поля. Причем «бесплатно».
- Возможна простая оптимизация: замена тип _cell с List на HashSet и вызовы Except ()/Concat () в Next () на циклы с Remove ()/Add () даст значительный прирост скорости (правда, в ущерб элегантности кода).
- Возможна простая многопоточная оптимизация: AsParallel ().
- Наследование от IEnumerable и метод Add () добавлены исключительно ради изящества инициализации.
Полагаю, мало кто сомневается, что LINQ позволяет легко писать компактный, выразительный и эффективный код. Пусть сей нехитрый пример послужит еще одним свидетельством тому.