Минимальная игра жизни в C #

Я хочу научиться писать чистый код с самого начала. Это мой первый проект - реализация «Жизнеспособного продукта», разработанного Конвеем в C #. В основном я хочу знать, доступен ли мой код для чтения, очистки, понимания и т. Д.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

//The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, alive or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
//Any live cell with fewer than two live neighbours dies, as if caused by under-population.
//Any live cell with two or three live neighbours lives on to the next generation.
//Any live cell with more than three live neighbours dies, as if by overcrowding.
//Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
//The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed—births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a tick (in other words, each generation is a pure function of the preceding one). The rules continue to be applied repeatedly to create further generations.

namespace Life
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

        }
        // Some parameters to use throughout the program
        // I'll basically use an array to store data and buttons to represent the cells
        static int columns = 30;    // Columns the Grid will have
        static int rows = 20;       // Rows the Grid will have
        static int depth = 3;       // Depth of the Grid
        int cellWidth = 20;         // With of the cells which will also be used to determine positions
        int cellHeight = 20;        // Height of the cells which will also be used to determine position
        string[, ,] cellGrid = new string[columns, rows, depth]; // This is the array that will hold the cell's information
        Panel Panel1 = new Panel(); // A panel where the cells will be laid out



        // Upon Loading the Form
        //Add the Panel and Populate with the cells
        public void Form1_Load(object sender, EventArgs e)
        {
            this.Controls.Add(Panel1);
            Panel1.Location = new Point(0, 0);
            Panel1.Size = new Size(cellWidth * columns, cellHeight * rows);
            Panel1.Visible = true;



            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < columns; j++)
                {
                    Button cell = new Button();
                    cellGrid[j, i, 0] = "dead"; // All cells start dead
                    cell.Location = new Point((j * cellWidth), (i * cellHeight)); // Possition is assigned to cell
                    cell.Size = new Size(cellWidth, cellHeight);// Size is Assigned to cell
                    cell.Click += button_Click;
                    cell.FlatStyle = FlatStyle.Flat; // Style
                    cell.Margin.All.Equals(0); // Margins
                    cell.BackColor = Color.White; // Color
                    Panel1.Controls.Add(cell); // Add to Panel



                }

            }

        }
        // When clicking on a cell it will switch between being alive and dead
        private void button_Click(object sender, EventArgs e)
        {
            Button thisButton = ((Button)sender);
            // Get the index in cellGrid using the cell's position
            int xIndex = thisButton.Location.X / cellWidth;
            int yIndex = thisButton.Location.Y / cellHeight;
            if (thisButton.BackColor == Color.White) // If the BackColor is white, it means it's dead so
            {
                thisButton.BackColor = Color.Black; // Change the color to Black
                cellGrid[xIndex, yIndex, 0] = "Alive"; // Change the cell to "Alive" in the Array

            }

            else // Otherwise it's alive so:
            {
                thisButton.BackColor = Color.White; // Change color to White
                cellGrid[xIndex, yIndex, 0] = "Dead"; // Change to Dead in the array

            }

        }

        // This will determine how many Neighbours or live cells each space has
        void Neighbours()
        {
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < columns; j++)
                {
                    int neighbours = 0;

                    for (int k = i - 1; k < i + 2; k++)
                    {
                        for (int l = j - 1; l < j + 2; l++)
                        {
                            try
                            {
                                if (k == i && l == j) { neighbours += 0; }
                                else if (cellGrid[l, k, 0] == "Alive") { neighbours += 1; }
                            }

                            catch (Exception e) { neighbours += 0; }

                        }

                    }
                    cellGrid[j, i, 1] = neighbours.ToString();


                }


            }
        }


        // Switches the grid to the next generation killing and reviving cells following the rules.
        public void UpdateGrid()

        {
            foreach (Control cell in Panel1.Controls)
            {

                int xIndex = cell.Left / cellWidth;
                int yIndex = cell.Top / cellHeight;
                int neighbours = Convert.ToInt32(cellGrid[xIndex, yIndex, 1]);

                if (neighbours < 2 | neighbours > 3)
                {
                    cellGrid[xIndex, yIndex, 0] = "Dead";
                    cell.BackColor = Color.White;

                }
                else if (neighbours == 3)
                {
                    cellGrid[xIndex, yIndex, 0] = "Alive";
                    cell.BackColor = Color.Black;

                }


             }

            }

        // Each generation that passes updates the grid following the rules
        public void NextGen()
        {
            Neighbours();
            UpdateGrid();
        }





        // Each tick of the timer will be a generation
        private void timer1_Tick(object sender, EventArgs e)
        {
            timer1.Stop();
            NextGen();
            timer1.Start();


        }

        // When pressing the Start button, generations will start passing automatically
        private void StartBtn_Click(object sender, EventArgs e)
        {
            if (StartBtn.Text == "Start" | StartBtn.Text == "Resume")
            {
                timer1.Start();
                StartBtn.Text = "Pause";

            }
            else
            {
                timer1.Stop();
                StartBtn.Text = "Resume";

            }

        }

        // Pressing Reset, resets the grid
        private void ResetBttn_Click(object sender, EventArgs e)
        {
            timer1.Stop();
            StartBtn.Text = "Start";
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < columns; j++)
                {
                    cellGrid[j, i, 0] = "dead"; // Kill all cells

                }


            }

            NextGen(); //Makes one generation go by

            }

        // You can pass generations manually by pressing Next Gen Button
        private void NextGenBttn_Click(object sender, EventArgs e)
        {
            NextGen(); //Hace que transcurra una generación
        }

        // Control how many generations in each second by changing the value
        private void GenXSec_ValueChanged(object sender, EventArgs e)
        {
            timer1.Interval = Convert.ToInt32(1000 / GenXSec.Value);
        }


    }
}
11 голосов | спросил Santiago Gil 26 Jam1000000amSun, 26 Jan 2014 02:13:46 +040014 2014, 02:13:46

5 ответов


11

Вы определили глубину как 3, но вы используете только 2 строки (один для живого или мертвого, а второй для подсчета соседей).

Вместо сохранения состояния ячейки в виде массива строк определите класс (или, возможно, структуру):

enum State
{
    Alive,
    Dead
}
class CellState
{
    internal State State { get; set; }
    internal int Neighbours { get; set; }
    internal CellState() { this.State = State.StateDead; Neighbours = 0; }
}

... также ...

CellState[,] cellGrid = new CellState[columns, rows];

cellGrid[j, i] = new CellState(); // All cells start dead

cellGrid[xIndex, yIndex].State = State.Alive; // Change the cell to "Alive" in the Array

Или (если есть только два состояния), используйте вместо буфера логическое выражение:

class CellState
{
    internal bool IsAlive { get; set; }
    internal int Neighbours { get; set; }
    internal CellState() { this.IsAlive = false; Neighbours = 0; }
}

Создание 400 кнопок управления становится дорогим. Если у вас была гораздо большая сетка (например, 1000x1000), то вы (т. Е. Ваша машина) не смогли бы управлять ею. Вместо элементов управления Button вы можете создать пользовательский элемент управления, на котором вы сами нарисовываете ячейки, и обрабатывать события мыши для хит-тестов.


Это сбивало с первого взгляда:

        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < columns; j++)
            {
                int neighbours = 0;

                for (int k = i - 1; k < i + 2; k++)
                {
                    for (int l = j - 1; l < j + 2; l++)

Вы можете назвать переменные:

  • i для строки
  • j для col
  • k to i
  • l to j

Вы не должны не должны использовать исключения для обычных событий, например, для достижения края экрана.

catch (Exception e) { neighbours += 0; }

Вместо этого не вызывать исключения, например:

for (int k = i - 1; k < i + 2; k++)
{
    if ((k < 0) || (k == rows))
    {
        // beyond edge of screen: not a neighbour
        continue;
    }

Кроме того, он чист, опрятен, понятен.

Есть вертикальные пустые /пробельные строки, которые вы должны удалить.

Строковое значение «Пуск» существует более чем в одном методе (может быть определено как константа или переменная в одном месте, например, если вы хотите загрузить его из многоязычного файла ресурсов).

Я был удивлен, увидев, что вы вызываете «Стоп» и «Старт» в методе timer1_Tick: обычно такой метод оставит тикание таймера. Возможно, вы это сделаете, потому что NexGen занимает много времени, поэтому вы хотите сбросить таймер, чтобы увидеть изменение перед следующим тиком.

Комментарий, который описывает реализуемый алгоритм (например, модифицированная версия Игра Конуэй Life Rules ) может помочь кому-то еще в будущем, кому необходимо поддерживать ваше программное обеспечение. Они могут читать программное обеспечение, чтобы увидеть, что он делает; комментарии помогают им знать, что он должен делать (например, если он не делает то, что он должен делать).


Это может быть вызов идеи Обновить вместе с SuspendDrawing и ResumeDrawing в вашем методе UpdateGrid. Изменение экземпляров кнопок (заставляя их перерисовывать) может (я не знаю) быть дорогим).


Вы упомянули «MVP»: означает ли это Model-View-ведущий ?

Если это так, я не вижу шаблон MVP в вашем коде: вместо этого у вас есть один класс с событиями пользовательского интерфейса, привязанными к событиям состояния данных.

Например, как бы вы изменили это (и сколько вам нужно было бы изменить), если бы вы захотели реализовать версии этой программы в консоли и WPF, а также версию Windows Forms?


  

MVP означает минимальный жизнеспособный продукт. Это скудный смысл разговора. Что-то, что работает с минимальным количеством функций и дизайна.

В этом случае я предлагаю следующие минимальные изменения.

Заменить это утверждение ...

string[, ,] cellGrid = new string[columns, rows, depth]

... с ...

Tuple<bool,int>[,] cellGrid = new Tuple<bool,int>[columns, rows];

Это дает вам безопасность типа: используйте вместо bool вместо «Alive» и «Dead» вместо целых экспатий вместо выражений типа neighbours.ToString() и Convert.ToInt32(cellGrid[xIndex, yIndex, 1]).

Заменить это утверждение ...

for (int k = i - 1; k < i + 2; k++)

... с ...

for (int k = Math.Max(i - 1, 0); k < Math.Min(i + 2, rows); k++)

... и соответствующее изменение в вашем диапазоне l. Бросание 50Исключения из расчета на мой взгляд ужасны; однако я должен признать, что они не так плохи, как я думал, они были .

ответил ChrisW 26 Jam1000000amSun, 26 Jan 2014 04:09:43 +040014 2014, 04:09:43
5

Идти на короткое время, ниже мое усилие (он абсолютно не выиграет никаких призов за производительность!). syb0rg предложил добавить несколько слов относительно того, как я думаю, что это улучшение. Здесь:

  • Простое использование массивов с целыми числами 2D репрезентативно легче.

  • Я использую абстракцию (Rg, Sum, Do над действиями и т. д.), чтобы фактор из общих паттернов.

  • Я использую коллекции коротких функций, чтобы делать то, что хочу; каждая функция должен быть понятен сам по себе.

  • Перейдя к краткости, я думаю, что улучшил ясность кода.

-

void Main()
{
    var rows = 10; // Including a 1-cell border o'death.
    var cols = 10;
    var rnd = new Random();
    var cell = new int[rows, cols];
    Do(cell, rows, cols, (r, c) => { cell[r, c] = (int)Math.Round(rnd.NextDouble()); });
    while (true) {
        WriteCells(cell, rows, cols);
        Console.WriteLine("--------");
        Console.ReadLine();
        cell = Gen(cell, rows, cols);
    }
}

static IEnumerable<int> Rg = Enumerable.Range(-1, 3);

static int Nbrs(int[,] cell, int r, int c) {
    return Rg.Sum(dr => Rg.Sum(dc => cell[r + dr, c + dc]));
}

static int Next(int[,] cell, int r, int c) {
    var nbrs = Nbrs(cell, r, c) - cell[r, c];
    return (cell[r, c] == 0 && nbrs == 3 || cell[r, c] == 1 && (nbrs == 2 || nbrs == 3)) ? 1 : 0;
}

static void Do(int[,] cell, int rows, int cols, Action<int, int> a) {
    for (var r = 1; r < rows - 1; r++) {
        for (var c = 1; c < cols - 1; c++) {
            a(r, c);
        }
    }
}

static int[,] Gen(int[,] cell, int rows, int cols) {
    var nxt = new int[rows, cols];
    Do(cell, rows, cols, (r, c) => { nxt[r, c] = Next(cell, r, c); });
    return nxt;
}

static void WriteCells(int[,] cell, int rows, int cols) {
    Do(cell, rows, cols, (r, c) => {
        Console.Write(cell[r, c] == 0 ? ' ' : '*');
        if (c == cols - 2) Console.WriteLine();
    });
}
ответил Rafe 4 FebruaryEurope/MoscowbTue, 04 Feb 2014 06:24:17 +0400000000amTue, 04 Feb 2014 06:24:17 +040014 2014, 06:24:17
3

Всего несколько случайных заметок:

  1. Вместо i и j переменные можно было бы назвать rowNumber и columnNumber.

    Обновление комментария Томаса: код чтения (и понимания) обычно занимает много времени, но обычно это не из-за длинных имен переменных или методов. Я не буду оптимизировать номер символа. Обсуждаются также темы этой темы на Programmer.SE . Моим любимым ответом является ники, один из которых , потому что он упомянул короткую 7-слотовую память (+ -2).

  2. cellGrid[j, i, 0]
    

    0 вот волшебное число. Именованная константа будет лучше.

  3. Комментарии, подобные этому, не нужны:

    cell.BackColor = Color.White; // Color
    

    Это говорит не что иное, как код уже делает, это довольно шум. ( Чистый код Роберт С. Мартин : Глава 4: Комментарии, комментарии к шуму )

  4. Более короткие строки, без горизонтальной прокрутки, будут легче читать.

ответил palacsint 26 Jam1000000amSun, 26 Jan 2014 03:03:21 +040014 2014, 03:03:21
3

Я знаю немного о «инженерной инженерии», но в интересах разделения проблем я был бы склонен перегонять проект в его основные элементы.

например

internal interface GameOfLifeRenderer
{
    GameOfLife GameOfLife { set; }

    int CellWidth { get; }
    int CellHeight { get; }

    void HighlightCell(int x, int y);
    void DeHighlightCell(int x, int y);

    void Initialize();
    void Update();

}

internal interface GameOfLife
{
    int Columns { get; }
    int Rows { get; }

    void Initialize();
    void Update();
    GridCell GetCell(int x, int y);
}

internal interface GridCell
{
    Boolean Alive { get; set; }
    GridCell[] GetNeighbours();

}

С учетом этого вы можете легко получить форму «Вид /рендеринг» и создать презентатор для ее хранения и объекта GameOfLife.

Тогда у вас будет чистое количество развязанных вызовов, если вы затем решите использовать линию для создания рендерера без winform или даже другого визуализатора, такого как пользовательский графический объект.

ответил apieceoffruit 27 Jpm1000000pmMon, 27 Jan 2014 16:39:47 +040014 2014, 16:39:47
3

Вот что нужно сделать, чтобы просто очистить существующий код.

Помимо приведенных ниже изменений, я также настоятельно рекомендую отделить логику пользовательского интерфейса от логики игры.

Одна ошибка, которую я нашел:

cell.Margin.All.Equals(0); // Margins

ничего не делает. Я думаю, вы имеете в виду:

var margin = cell.Margin;
margin.All = 0;
cell.Margin = margin;

Итак, без дальнейших церемоний:

namespace Life
{
    using System;
    using System.Drawing;
    using System.Globalization;
    using System.Windows.Forms;

    /// <summary>
    /// The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, alive or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
    /// Any live cell with fewer than two live neighbours dies, as if caused by under-population.
    /// Any live cell with two or three live neighbours lives on to the next generation.
    /// Any live cell with more than three live neighbours dies, as if by overcrowding.
    /// Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
    /// The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed—births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a tick (in other words, each generation is a pure function of the preceding one). The rules continue to be applied repeatedly to create further generations.
    /// </summary>
    public partial class Form1 : Form
    {
        //// Some parameters to use throughout the program
        //// I'll basically use an array to store data and buttons to represent the cells

        private const string Dead = "Dead";

        private const string Alive = "Alive";

        /// <summary>
        /// Columns the Grid will have
        /// </summary>
        private const int Columns = 30;

        /// <summary>
        /// Rows the Grid will have
        /// </summary>
        private const int Rows = 20;

        /// <summary>
        /// Depth of the Grid
        /// </summary>
        private const int Depth = 2;

        private const int DeadOrAliveIndex = 0;

        private const int NeighborCountIndex = 1;

        /// <summary>
        /// With of the cells which will also be used to determine positions
        /// </summary>
        private const int CellWidth = 20;

        /// <summary>
        /// Height of the cells which will also be used to determine position
        /// </summary>
        private const int CellHeight = 20;

        /// <summary>
        /// This is the array that will hold the cell's information
        /// </summary>
        private readonly string[,,] cellGrid = new string[Columns, Rows, Depth];

        /// <summary>
        /// A panel where the cells will be laid out
        /// </summary>
        private readonly Panel panel1 = new Panel();

        public Form1()
        {
            this.InitializeComponent();
        }

        // Upon Loading the Form
        // Add the Panel and Populate with the cells
        public void Form1_Load(object sender, EventArgs e)
        {
            this.Controls.Add(this.panel1);
            this.panel1.Location = new Point(0, 0);
            this.panel1.Size = new Size(CellWidth * Columns, CellHeight * Rows);
            this.panel1.Visible = true;

            for (var row = 0; row < Rows; row++)
            {
                for (var column = 0; column < Columns; column++)
                {
                    var cell = new Button();

                    this.cellGrid[column, row, DeadOrAliveIndex] = Dead;
                    cell.Location = new Point(CellWidth * column, CellHeight * row);
                    cell.Size = new Size(CellWidth, CellHeight);
                    cell.Click += this.ButtonClick;
                    cell.FlatStyle = FlatStyle.Flat;
                    var margin = cell.Margin;
                    margin.All = 0;
                    cell.Margin = margin;
                    cell.BackColor = Color.White;
                    this.panel1.Controls.Add(cell);
                }
            }
        }

        // When clicking on a cell it will switch between being alive and dead
        private void ButtonClick(object sender, EventArgs e)
        {
            var thisButton = sender as Button;

            if (thisButton == null)
            {
                return;
            }

            // Get the index in cellGrid using the cell's position
            var row = thisButton.Location.Y / CellHeight;
            var column = thisButton.Location.X / CellWidth;


            // If the BackColor is white, it means it's dead so
            var isDead = thisButton.BackColor == Color.White;

            thisButton.BackColor = isDead ? Color.Black : Color.White;
            this.cellGrid[column, row, DeadOrAliveIndex] = isDead ? Alive : Dead;
        }

        // This will determine how many Neighbours or live cells each space has
        private void Neighbours()
        {
            for (var row = 0; row < Rows; row++)
            {
                for (var column = 0; column < Columns; column++)
                {
                    var neighbours = 0;

                    for (var i = row - 1; i < row + 2; i++)
                    {
                        if (i < 0 || i >= Rows)
                        {
                            continue;
                        }

                        for (var j = column - 1; j < column + 2; j++)
                        {
                            if (j < 0 || j >= Columns)
                            {
                                continue;
                            }

                            if (this.cellGrid[j, i, DeadOrAliveIndex] == Alive)
                            {
                                neighbours++;
                            }
                        }
                    }

                    this.cellGrid[column, row, NeighborCountIndex] = neighbours.ToString(CultureInfo.InvariantCulture);
                }
            }
        }


        // Switches the grid to the next generation killing and reviving cells following the rules.
        public void UpdateGrid()
        {
            foreach (Control cell in this.panel1.Controls)
            {
                var row = cell.Top / CellHeight;
                var column = cell.Left / CellWidth;
                var neighbours = Convert.ToInt32(this.cellGrid[column, row, NeighborCountIndex]);

                if (neighbours < 2 || neighbours > 3)
                {
                    this.cellGrid[column, row, DeadOrAliveIndex] = Dead;
                    cell.BackColor = Color.White;
                }
                else if (neighbours == 3)
                {
                    this.cellGrid[column, row, DeadOrAliveIndex] = Alive;
                    cell.BackColor = Color.Black;
                }
            }
        }

        // Each generation that passes updates the grid following the rules
        public void NextGen()
        {
            this.Neighbours();
            this.UpdateGrid();
        }

        // Each tick of the timer will be a generation
        private void timer1_Tick(object sender, EventArgs e)
        {
            this.timer1.Stop();
            this.NextGen();
            this.timer1.Start();
        }

        // When pressing the Start button, generations will start passing automatically
        private void StartBtn_Click(object sender, EventArgs e)
        {
            if (this.StartBtn.Text == "Start" || this.StartBtn.Text == "Resume")
            {
                this.timer1.Start();
                this.StartBtn.Text = "Pause";
            }
            else
            {
                this.timer1.Stop();
                this.StartBtn.Text = "Resume";
            }
        }

        // Pressing Reset, resets the grid
        private void ResetBttn_Click(object sender, EventArgs e)
        {
            this.timer1.Stop();
            this.StartBtn.Text = "Start";

            // Kill all cells
            for (var row = 0; row < Rows; row++)
            {
                for (var column = 0; column < Columns; column++)
                {
                    this.cellGrid[column, row, DeadOrAliveIndex] = Dead;
                }
            }

            this.NextGen(); // Makes one generation go by
        }

        // You can pass generations manually by pressing Next Gen Button
        private void NextGenBttn_Click(object sender, EventArgs e)
        {
            this.NextGen(); // Hace que transcurra una generación
        }

        // Control how many generations in each second by changing the value
        private void GenXSec_ValueChanged(object sender, EventArgs e)
        {
            this.timer1.Interval = Convert.ToInt32(1000 / this.GenXSec.Value);
        }
    }
}
ответил Jesse C. Slicer 28 Jam1000000amTue, 28 Jan 2014 03:05:48 +040014 2014, 03:05:48

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132