×

[PR]この広告は3ヶ月以上更新がないため表示されています。
ホームページを更新後24時間以内に表示されなくなります。

WPF:LifeGame (ライフゲーム)   

WPFでライフゲームを書いてみました。ライフゲームについては、ちょっと長めですがWikipediaより抜粋させていただきます。

ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。
...
ライフゲームでは初期状態のみでその後の状態が決定される。碁盤のような格子があり、一つの格子はセル(細胞)と呼ばれる。各セルには8つの近傍のセルがある (ムーア近傍) 。各セルには「生」と「死」の2つの状態があり、あるセルの次のステップ(世代)の状態は周囲の8つのセルの今の世代における状態により決定される。

セルの生死は次のルールに従う。

誕生   死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存   生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎   生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密   生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
 
つまり、ひとつのセルに注目した場合、そのセルの周りの8つのセルの状態によって、自分自身のセルの生死が決まるという単純なルールによるシミュレーションです。
セルの配置により、同じパターンを繰り返したり、形を変えながら移動していったりと、セルの生き死にの様子を眺めているだけで楽しかったりします。
是非、Wikipediaに掲載されている初期状態パターンで動かしてみてください。




主要なクラスは、以下の4つです。

■Cellクラス

ひとつのセル上の生物を表すクラス。
Surviceメソッドが、引数で周囲の「生きているセル」の数を受け取り、その数により次の世代の状態を決めます。このメソッドを呼び出しただけでは、実際の状態は更新されません。NextStageを呼び出されることで、Surviveで得られた状態に更新されます。

使い方として、すべてのCellに対してSurviveを呼び出した後に、すべてのCellに対してNextStageを呼び出すことになります。

■LifeBoard

ライフゲームの空間(盤)を表すクラスで、Boardクラスを継承しています。
このクラスは複数のCellオブジェクトを管理します。一つの升目にひとつのCellオブジェクトが割り当てられます。
初期値はすべてのCellが死んだ状態です。

このクラスにもSurviveメソッドがあります。このメソッドは、すべてのCellオブジェクトに対して、周りの生存数をカウントし、その値を引数にして、Cell.Survive メソッドを呼び出した後に、CellごとにNextStageを呼び出しています。
なお、状態が変化した場合、変化したCellの数分、Changedイベントが発行されます。

これにより、UI上にCellが変化したことを通知し、描画をさせています。

■Drawerクラス

描画を担当するクラス。BoardCanvasを継承しています。
DrawPieceメソッドは、Cellの生死の状態を描画するメソッドです。

生きている状態の時には、Grayの四角(Rectangle)を描画します。
死んでいるときには、このRectangleオブジェクトを消去しています。

■MainWindoweクラス

MainWindow.xamlのコードビハインドクラス。LifeBoardオブジェクトを保持し、プログラムを統括しています。

世代を進めるために、DispatcherTimerを利用しています。0.5秒ごとに、LifeBoard.Surviveメソッドを呼び出し、世代を進めています。

なお、ゲームの初期状態を設定するのに、canvas1_MouseLeftButtonDown、canvas1_MouseLeftButtonUp、canvas1_MouseMoveイベントハンドラを定義しています。

マウスダウンで、マウスが指すCellの状態が反転します。また、マウスをドラッグさせることで、次々とCellの状態を反転させるようにしています。

このプログラムを作成するのに、これまで掲載してきた「ハイパークィーン問題」「8クィーン・ゲーム」などのボード系のプログラム同様、Board, BoardCanvas クラスを利用しています。
ただし、Silverlight用のコードはそのまま利用できなかったため、WPFに書き換えています。

このプログラムにとっては、Boardおよび BoardCanvasクラスは、機能が過剰なのですが、再利用したほうが早く作れるので、これを利用することとしました。 書き換えたソースコードは、当ページの最後に掲載しています。

以下に、XAMLとC#のコードを示します。ライフゲームにしては長めのソースコードとなってしまいましたが、簡単なコメントもつけましたし、ひとつひとつのメソッドはかなり短いので、理解するのもそれほ難しくはないと思います。



■MainWindow.xaml
<Window x:Class="LifeGame.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="400" Width="525">
    <Grid>
        <Canvas Margin="15,15,100,15" Name="canvas1" Loaded="canvas1_Loaded" 
                MouseLeftButtonDown="canvas1_MouseLeftButtonDown" 
                MouseLeftButtonUp="canvas1_MouseLeftButtonUp"
                MouseMove="canvas1_MouseMove"
                Background="White" />
        <Button Content="Start" Height="23" HorizontalAlignment="Right" 
                Margin="310,10,10,0" Name="buttonStart" VerticalAlignment="Top" 
                Width="75" Click="buttonStart_Click" />
        <Button Content="Stop" Height="23" HorizontalAlignment="Right" 
                Margin="310,40,10,0" Name="buttonStop" VerticalAlignment="Top" Width="75" 
                Click="buttonStop_Click" />
        <Button Content="Clear" Height="23" HorizontalAlignment="Right" Margin="310,70,10,0" 
                Name="buttonClear" VerticalAlignment="Top" Width="75" Click="buttonClear_Click" />
        <Button Content="Random" Height="23" HorizontalAlignment="Right" Margin="310,100,10,0" 
                Name="buttonRandom" VerticalAlignment="Top" Width="75" Click="buttonRandom_Click"  />
    </Grid>
</Window>



■MainWindow.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using Gushwell.Etude;

namespace LifeGame {
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }
        private DispatcherTimer timer;
        private LifeBoard board;
        private Drawer drawer;

        // Canvasがロードされた。初期化をする。
        private void canvas1_Loaded(object sender, RoutedEventArgs e) {
            timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromMilliseconds(500);
            timer.Tick += new EventHandler(timer_Tick);
            board = new LifeBoard(30);
            drawer = new Drawer(canvas1, board);
            drawer.DrawRuledLines(BoardType.Chess);
        }

        // Startボタンが押された
        private void buttonStart_Click(object sender, RoutedEventArgs e) {
            Start();
        }

        // Stopボタンが押された
        private void buttonStop_Click(object sender, RoutedEventArgs e) {
            Stop();
        }

        // Clearボタンが押された
        private void buttonClear_Click(object sender, RoutedEventArgs e) {
            board.ClearAll();
        }


        private bool _mouseDown = false;
        private Location _prevLoc;

        // マウスが押された(Cellの状態を反転させる)
        private void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
            Location loc = drawer.ToLocation(e.GetPosition(this.canvas1 as UIElement));
            board.Reverse(loc);
            _mouseDown = true;
            _prevLoc = loc;
        }

        // ドラッグ終了
        private void canvas1_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
            _mouseDown = false;
        }

        // マウスがドラッグされた (Cellの状態を反転させる)
        private void canvas1_MouseMove(object sender, MouseEventArgs e) {
            if (_mouseDown) {
                Location loc = drawer.ToLocation(e.GetPosition(this.canvas1 as UIElement));
                if (_prevLoc != null && (_prevLoc.X != loc.X || _prevLoc.Y != loc.Y)) {
                    board.Reverse(loc);
                    _prevLoc = loc;
                }
            }
        }

        // Randomボタンが押された
        private void buttonRandom_Click(object sender, RoutedEventArgs e) {
            SetRandom();
        }

        // 世代を進める。(一定間隔で呼び出される)
        void timer_Tick(object sender, EventArgs e) {
            int count = board.Survive();
            if (count == 0)
                Stop();
        }

        // 開始
        private void Start() {
            timer.Start();
            buttonStart.IsEnabled = false;
            buttonClear.IsEnabled = false;
        }

        // 終了
        private void Stop() {
            timer.Stop();
            buttonStart.IsEnabled = true;
            buttonClear.IsEnabled = true;
        }

        // Random配置
        private void SetRandom() {
            Random rnd = new Random();
            int count = rnd.Next(100, 150);
            while (count > 0) {
                int x = rnd.Next(5, board.XSize - 5);
                int y = rnd.Next(5, board.YSize - 5);
                var loc = new Location(x, y);
                board.Reverse(loc);
                count--;
            }
        }
    }
}


■Cell.cs
using Gushwell.Etude;

namespace LifeGame {
    // Cell (ひとつの四角の領域)を表す。
    // IPeiseはマーカーインターフェース(実装すべきメソッド等は無い)
    public class Cell : IPiece {
        public bool IsAlive { get; private set; }
        private bool _nextStatus;

        // コンストラクタ
        public Cell() {
            IsAlive = false;
        }

        // 生死を反転する
        public void Toggle() {
            IsAlive = !IsAlive;
        }

        // trueならば生、falseならば死
        private bool Judge(int count) {
            if (IsAlive)
                return (count == 2 || count == 3);
            else
                return (count == 3);
        }

        // 次の世代の状態を決める。変化があるとtrueが返る。
        public bool Survive(int around) {
            _nextStatus = Judge(around);
            return _nextStatus != IsAlive;
        }

        // 次の状態にする
        public bool NextStage() {
            var old = IsAlive;
            IsAlive = _nextStatus;
            return IsAlive != old;
        }
    }
}


■LifeBoard.cs
using Gushwell.Etude;

namespace LifeGame {
    // Cellを管理する
    public class LifeBoard : Board{
        // コンストラクタ
        public LifeBoard(int size) : base(size,size) {
            foreach (var loc in this.GetValidLocations())
                this[loc] = new Cell();
        }

        // 反転する
        public void Reverse(Location loc) {
            Cell cell = this[loc] as Cell;
            cell.Toggle();
            OnChanged(loc, cell);
        }

        public override void ClearAll() {
            base.ClearAll();
            foreach (var loc in this.GetValidLocations())
                this[loc] = new Cell();
        }

        // 周りの生存者の数を数える
        protected int CountAround(Location loc) {
            int[] directions = { -this.XSize - 3, -this.XSize -2, -this.XSize - 1,
                                   -1, +1,
                                   this.XSize + 1, this.XSize + 2, this.XSize +3 };
            int count = 0;
            int index = ToIndex(loc);
            foreach (var d in directions) {
                var nix = (index + d);
                var loc2 = this.ToLocation(index + d);
                Cell cell = this[loc2] as Cell;
                if (cell != null && cell.IsAlive)
                    count++;
            }
            return count;
        }

        // 生死を決める
        public int Survive() {
            int count = 0;
            foreach (var loc in this.GetValidLocations()) {
                Cell cell = this[loc] as Cell;
                if (cell.Survive(CountAround(loc)))
                    count++;
            }
            if (count > 0) {
                foreach (var loc in this.GetValidLocations()) {
                    Cell cell = this[loc] as Cell;
                    if (cell.NextStage()) {
                        OnChanged(loc, cell);
                    }
                }
            }
            return count;
        }
    }
}


■Drawer.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Gushwell.Etude;

namespace LifeGame {
    // 描画を受け持つクラス
    public class Drawer : BoardCanvas {
        public Drawer(Panel panel, Board board)
            : base(panel, board) {
        }

        // ひとつのセルをその状態によって描画
        public override void DrawPiece(Location loc, IPiece piece) {
            Cell cell = piece as Cell;
            if (cell.IsAlive) {
                DrawLife(loc, Colors.Gray);
            } else {
                RemovePiece(loc);
            }
        }

        // 四角形を生成する
        private void DrawLife(Location loc, Color color) {
            Point p1 = ToPoint(loc);
            Point p2 = new Point(p1.X + this.CellWidth - 1, p1.Y + this.CellHeight - 1);
            var rect = new Rectangle() {
                Name = this.PieceName(loc),
                Stroke = new SolidColorBrush(color),
                Fill = new SolidColorBrush(color),
                Margin = new Thickness(p1.X, p1.Y, p2.X, p2.Y),
                Width = p2.X - p1.X,
                Height = p2.Y - p1.Y,
            };
            this.Panel.Children.Add(rect);
            if (this.Panel.FindName(rect.Name) != null)
                this.Panel.UnregisterName(rect.Name);
            this.Panel.RegisterName(rect.Name, rect);
        }
    }
}


■Board.cs
using System;
using System.Linq;
using System.Collections.Generic;
using System.Windows.Media;

// WindowsFormsで利用する場合は、System.Windows.Media の代わりに、
// System.Drawing を using する。

namespace Gushwell.Etude
{
    // Changeイベントで使われる EventArgs
    public class BoardChangedEventArgs : EventArgs
    {
        public Location Location { get; internal set; }
        public IPiece Piece { get; internal set; }
    }

    // 盤データクラス
    public class Board
    {
        // 駒が配置される1次元配列 (周辺には番兵が置かれる)
        private IPiece[] _pieces;

        // 番兵以外の有効な位置(1次元のインデックス)が格納される
        private readonly int[] _validIndexes;

        // 盤の行(縦方向)数
        public int YSize { get; private set; }
        // 盤のカラム(横方向)数
        public int XSize { get; private set; }

        // _pieces配列に変更があるとChangeイベントが発生する。
        public event EventHandler<BoardChangedEventArgs> Changed;

        // コンストラクタ
        public Board(int xsize, int ysize)
        {
            this.YSize = xsize;
            this.XSize = ysize;
            // 盤データの初期化 (周りは番兵(Guard)をセットしておく)
            _pieces = new IPiece[(xsize + 2) * (ysize + 2)];
            for (int i = 0; i < _pieces.Length; i++)
            {
                if (IsOnBoard(ToLocation(i)))    // この時点でIsOnBoard(int index) は利用できない
                    _pieces[i] = Pieces.Empty;
                else
                    _pieces[i] = Pieces.Guard;
            }
            // 毎回求めるのは無駄なので最初に求めておく
            _validIndexes = Enumerable.Range(0, _pieces.Length)
                                     .Where(ix => _pieces[ix] == Pieces.Empty)
                                     .ToArray();

        }

        // コンストラクタ (Cloneとしても利用できる)
        public Board(Board board)
        {
            this.YSize = board.YSize;
            this.XSize = board.XSize;
            this._validIndexes = board._validIndexes.ToArray();
            this._pieces = board._pieces.ToArray();
        }

        // イベント発行 
        protected void OnChanged(Location loc, IPiece piece)
        {
            if (Changed != null)
            {
                var args = new BoardChangedEventArgs
                {
                    Location = loc,
                    Piece = piece
                };
                Changed(this, args);
            }
        }

        // (x,y) から、_pieceへのインデックスを求める
        public int ToIndex(int x, int y)
        {
            return x + y * (XSize + 2);
        }

        // Location から _pieceのIndexを求める
        public int ToIndex(Location loc)
        {
            return ToIndex(loc.X, loc.Y);
        }

        // IndexからLocationを求める
        public Location ToLocation(int index)
        {
            return new Location(index % (XSize + 2), index / (YSize + 2));
        }

        // 本来のボード上の位置かどうかを調べる
        protected bool IsOnBoard(Location loc)
        {
            int x = loc.X;
            int y = loc.Y;
            return ((1 <= x && x <= XSize) &&
                   (1 <= y && y <= YSize));
        }

        // 本来のボード上の位置(index)かどうかを調べる
        protected bool IsOnBoard(int index)
        {
            if (0 <= index && index < _pieces.Length)
                return this[index] != Pieces.Guard;
            return false;
        }

        // Pieceを置く_piecesの要素を変更するのはこのメソッドだけ(コンストラクタは除く)。
        // override可 
        protected virtual void PutPiece(int index, IPiece piece)
        {
            if (IsOnBoard(index))
            {
                _pieces[index] = piece;
                OnChanged(ToLocation(index), piece);
            }
            else
            {
                throw new ArgumentOutOfRangeException();
            }
        }

        // インデクサ (x,y)の位置の要素へアクセスする
        public IPiece this[int index]
        {
            get { return _pieces[index]; }
            set { PutPiece(index, value); }
        }

        // インデクサ (x,y)の位置の要素へアクセスする
        public IPiece this[int x, int y]
        {
            get { return this[ToIndex(x, y)]; }
            set { this[ToIndex(x, y)] = value; }
        }

        // インデクサ locの位置の要素へアクセスする
        public IPiece this[Location loc]
        {
            get { return this[loc.X, loc.Y]; }
            set { this[loc.X, loc.Y] = value; }
        }

        // 全てのPieceをクリアする
        public virtual void ClearAll()
        {
            foreach (var ix in GetOccupiedIndexes())
                ClearPiece(ToLocation(ix));
        }

        // x,yの位置をクリアする
        public virtual void ClearPiece(Location loc)
        {
            this[loc.X, loc.Y] = Pieces.Empty;
        }

        // EmptyPiece 以外の全てのPieceを列挙する
        public IEnumerable<IPiece> GetAllPieces()
        {
            return _validIndexes.Select(i => this[i]).Where(p => p != Pieces.Empty);
        }

        // 指定したIPieceが置いてあるLocationを列挙する
        public IEnumerable<Location> GetLocations(IPiece piece)
        {
            Type type = piece.GetType();
            return GetValidLocations().Where(loc => this[loc].GetType() == type);
        }

        // 指定したIPieceがおいてあるIndexを列挙する
        public IEnumerable<int> GetIndexes(IPiece piece)
        {
            Type type = piece.GetType();
            return _validIndexes.Where(index => this[index].GetType() == type);
        }

        // 番兵部分を除いた有効なLocationを列挙する
        public IEnumerable<Location> GetValidLocations()
        {
            return _validIndexes.Select(ix => ToLocation(ix));
        }

        // 番兵部分を除いた有効なIndexを列挙する
        public IEnumerable<int> GetValidIndexes()
        {
            return _validIndexes;
        }

        // 駒が置かれているLocationを列挙する
        public IEnumerable<Location> GetOccupiedLocations()
        {
            return GetOccupiedIndexes().Select(index => ToLocation(index));
        }

        // 駒が置かれているLocationを列挙する
        public IEnumerable<int> GetOccupiedIndexes()
        {
            return _validIndexes.Where(index => this[index] != Pieces.Empty);
        }

        // 何もおかれていないLocationを列挙する
        public IEnumerable<Location> GetVacantLocations()
        {
            return _validIndexes.Select(index => ToLocation(index));
        }

        // 何もおかれていないIndexを列挙する
        public IEnumerable<int> GetVacantIndexes()
        {
            return GetIndexes(Pieces.Empty);
        }

        // 指定した方向の位置を番兵が見つかるまで取得する。
        public IEnumerable<int> GetSeriesIndexes(int index, int direction)
        {
            for (int pos = index; this[pos] != Pieces.Guard; pos += direction)
                yield return pos;
        }

        // 上方向
        public int UpDirection
        {
            get { return -(this.XSize + 2); }
        }

        // 下方向
        public int DownDirection
        {
            get { return (this.XSize + 2); }
        }

        // 左方向
        public int LeftDirection
        {
            get { return -1; }
        }

        // 右方向
        public int RightDirection
        {
            get { return 1; }
        }

        // 右上の方向
        public int UpperRightDirection
        {
            get { return UpDirection + 1; }
        }

        // 左上の方向
        public int UpperLeftDirection
        {
            get { return UpDirection - 1; }
        }

        // 右下の方向
        public int LowerRightDirection
        {
            get { return DownDirection + 1; }
        }

        // 左下の方向
        public int LowerLeftDirection
        {
            get { return DownDirection - 1; }
        }
    }

    // 駒を示すマーカーインターフェース(marker interface)
    public interface IPiece
    {
    }

    // 色を持つ駒
    public interface IColorPiece : IPiece
    {
        Color Color { get; }
    }

    // 良く利用する駒たち
    public static class Pieces
    {
        public static readonly IPiece Black = new BlackPiece();
        public static readonly IPiece White = new WhitePiece();
        public static readonly IPiece Empty = new EmptyPiece();
        public static readonly IPiece Guard = new GuardPiece();
    }

    // 黒石
    public struct BlackPiece : IColorPiece
    {
        public Color Color
        {
            get { return Color.FromArgb(255, 128, 128, 128); }
        }
    }

    // 白石
    public struct WhitePiece : IColorPiece
    {
        public Color Color
        {
            get { return Color.FromArgb(255, 255, 255, 255); }
        }
    }

    // 番兵用:他と区別するためだけなので中身は何でも良い (マーカーオブジェクト)
    public struct GuardPiece : IPiece
    {
    }

    // 何もおかれていないことを示す:いわゆる NullObject  (マーカーオブジェクト)
    public struct EmptyPiece : IPiece
    {
    }

    // ボード上の位置を示す
    public class Location
    {
        public int X { get; set; }
        public int Y { get; set; }
        public Location(int x, int y)
        {
            X = x;
            Y = y;
        }
        public override string ToString()
        {
            return string.Format("({0},{1}) ", X, Y);
        }
    }
}


■BoardCanvas.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Gushwell.Etude;

namespace Gushwell.Etude {
    // 罫線の種類
    public enum BoardType {
        Go,
        Chess
    }

    // Boardおよび駒(PIece)の表示を担当する
    // BoardオブジェクトからChangeイベントを受け取ると、変更されたセルのPieceを描画する。
    // その他Board/Pieceを描画するための各種メソッドを用意。
    // PanelにUiElementを動的に追加削除することで描画を行っている。
    public class BoardCanvas {
        protected double CellWidth { get; private set; }        // ひとつのCellの幅
        protected double CellHeight { get; private set; }       // ひとつのCellの高さ
        protected int YSize { get; private set; }            // 縦方向のCellの数
        protected int XSize { get; private set; }            // 横方向のCellの数
        protected BoardType BoardType { get; private set; }     // 罫線の種類 (碁盤かチェス盤か)
        protected Panel Panel { get; private set; }             // 対象となる Panelオブジェクト
        protected Board Board { get; private set; }             // 対象となる Boardオブジェクト

        // コンストラクタ
        public BoardCanvas(Panel panel, Board board) {
            this.Board = board;
            this.Panel = panel;
            YSize = board.YSize;
            XSize = board.XSize;

            CellWidth = (panel.ActualWidth - 1) / XSize;
            CellHeight = (panel.ActualHeight - 1) / YSize;
            foreach (var loc in board.GetValidLocations()) {
                UpdatePiece(loc, board[loc]);
            }
            this.Board.Changed += new EventHandler<BoardChangedEventArgs>(board_Changed);
            _synchronize = true;
        }

        private bool _synchronize;

        // Boardオブジェクトと同期するか否か (初期値:同期する)
        public bool Synchronize {
            get { return _synchronize; }
            set {
                if (value == true) {
                    if (!_synchronize) {
                        this.Board.Changed += new EventHandler<BoardChangedEventArgs>(board_Changed);
                        _synchronize = true;
                    }
                } else {
                    if (_synchronize) {
                        this.Board.Changed -= new EventHandler<BoardChangedEventArgs>(board_Changed);
                        _synchronize = false;
                    }
                }
            }
        }

        public void ChangeBoard(Board board) {
            this.Board = board;
        }


        // 罫線を引く
        public void DrawRuledLines(BoardType linetype) {
            this.BoardType = linetype;
            int startx = (linetype == BoardType.Chess)
                            ? 0
                            : (int)(CellHeight / 2);
            for (double i = startx; i <= Panel.ActualHeight; i += CellHeight) {
                DrawLine(0, i, Panel.ActualWidth, i);
            }
            int starty = (linetype == BoardType.Chess)
                            ? 0
                            : (int)(CellWidth / 2);
            for (double i = starty; i <= Panel.ActualWidth; i += CellWidth) {
                DrawLine(i, 0, i, Panel.ActualHeight);
            }
        }

        // 線を引く
        protected void DrawLine(double x1, double y1, double x2, double y2) {
            Line line = new Line();
            line.X1 = x1;
            line.Y1 = y1;
            line.X2 = x2;
            line.Y2 = y2;
            line.Stroke = new SolidColorBrush(Colors.LightGray);
            Panel.Children.Add(line);
        }

        // 円オブジェクトを生成する (Pieceのデフォルト表示を担当)
        public Ellipse CreateEllipse(Location loc, Color color) {
            Ellipse eli = new Ellipse();
            eli.Name = PieceName(loc);
            eli.Height = CellWidth * 0.85;
            eli.Width = CellHeight * 0.85;
            eli.Fill = new SolidColorBrush(color);
            eli.Stroke = new SolidColorBrush(Colors.DarkGray);
            Point pt = ToPoint(loc);
            Canvas.SetLeft(eli, pt.X + CellWidth / 2 - eli.Width / 2);
            Canvas.SetTop(eli, pt.Y + CellHeight / 2 - eli.Height / 2);
            return eli;
        }

        // Pieceオブジェクトの名前を生成する
        public string PieceName(Location loc) {
            return string.Format("x{0}y{1}", loc.X, loc.Y);
        }

        // 矩形を描く
        public void DrawRectangle(Point p1, Point p2, Color color) {
            this.Panel.Children.Add(CreateRectangle(p1, p2, color));
        }

        // 矩形を消去する
        public void EraseRectangles(Point p1, Point p2) {
            var name = RectangleName(p1);
            Rectangle obj = Panel.FindName(name) as Rectangle;
            if (obj != null) {
                Panel.Children.Remove(obj as UIElement);
            }
        }

        // 四角形を生成する
        public Rectangle CreateRectangle(Point p1, Point p2, Color color) {
            var rect = new Rectangle();
            rect.Name = RectangleName(p1);
            rect.Stroke = new SolidColorBrush(color);
            double x1 = Math.Min(p1.X, p2.X);
            double y1 = Math.Min(p1.Y, p2.Y);
            double x2 = Math.Max(p1.X, p2.X);
            double y2 = Math.Max(p1.Y, p2.Y);
            rect.Margin = new Thickness(x1, y1, x2, y2);
            rect.Width = x2 - x1;
            rect.Height = y2 - y1;
            return rect;
        }

        // 矩形の名前を得る
        protected string RectangleName(Point p1) {
            return string.Format("r{0}{1}", (int)p1.X, (int)p1.Y);
        }

        // Boardの内容に沿って、Pieceを描画しなおす
        // UIスレッドとは別スレッドで動作していた場合を考慮。
        public void Invalidate() {
            Panel.Dispatcher.BeginInvoke(new Action(() => {
                foreach (var loc in Board.GetValidLocations()) {
                    this.RemovePiece(loc);
                }
                foreach (var loc in Board.GetValidLocations()) {
                    IPiece piece = Board[loc];
                    UpdatePiece(loc, piece);
                }
            }));
        }

        // UIスレッドとは別スレッドで動作させている時だけ意味を持つ。
        public TimeSpan UpdateInterval { get; set; }

        // Boardオブジェクトが変更されたときに呼び出されるイベントハンドラ
        // UIスレッドとは別スレッドで動作していた場合を考慮。
        private void board_Changed(object sender, BoardChangedEventArgs e) {
            Thread.Sleep(UpdateInterval);
            Panel.Dispatcher.BeginInvoke(new Action(() => {
                UpdatePiece(e.Location, e.Piece);
            }));
        }

        // Pieceの描画を更新する
        public virtual void UpdatePiece(Location loc, IPiece piece) {
            if (piece == null || piece is EmptyPiece) {
                RemovePiece(loc);
            } else {
                var name = PieceName(loc);
                object obj = Panel.FindName(name);
                if (obj != null) {
                    Panel.Children.Remove(obj as UIElement);
                }
                if (piece is EmptyPiece || piece is GuardPiece)
                    return;
                DrawPiece(loc, piece);
            }
        }

        // Pieceを描く。 デフォルト実装は、IColorPieceのみに対応。
        // 他のPiece型は、独自に当メソッドをoverrideする必要がある。
        // なお、overrideした DrawPiece内では、DrawRectangleメソッドは利用できない。
        public virtual void DrawPiece(Location loc, IPiece piece) {
            var colorPiece = piece as IColorPiece;
            if (colorPiece != null)
                Panel.Children.Add(CreateEllipse(loc, colorPiece.Color));
        }

        // 指定した位置のPieceを取り除く
        public void RemovePiece(Location loc) {
            var name = PieceName(loc);
            object obj = Panel.FindName(name);
            if (obj != null) {
                Panel.Children.Remove(obj as UIElement);
            }
        }

        // Locationからグラフィックの座標であるPointへ変換
        public Point ToPoint(Location loc) {
            return new Point {
                X = CellWidth * (loc.X - 1),
                Y = CellHeight * (loc.Y - 1)
            };
        }

        // グラフィックの座標であるPointからLocationへ変換
        public Location ToLocation(Point pt) {
            var x = pt.X;
            var y = pt.Y;
            int a = Math.Max(0, (int)(x / CellWidth));
            if (a >= XSize)
                a = XSize - 1;
            int b = Math.Max(0, (int)(y / CellHeight));
            if (b >= YSize)
                b = YSize - 1;
            return new Location(a + 1, b + 1);
        }
    }
}