ラベル 2048 の投稿を表示しています。 すべての投稿を表示
ラベル 2048 の投稿を表示しています。 すべての投稿を表示

2014年11月14日金曜日

2048のGUIを実装する

今朝から.NET周りの非常に大きなニュースがたくさん出回っていて、いろいろとアツいですね。
その中でも、我々WPFerにとっても胸の熱いニュースが出ています。

The Roadmap for WPF

最近どうなってるのかよく分からなかったWPFですが、今後もまたさらなる発展が期待できそうな文面ですね。高まります。

というわけで、長らく放置にしてしまっていた2048の件ですが、やっと前回の続きのGUIの作成を行いました。もちろんWPF+Livetを使っての作成です。


前回の記事では、2048のマトリックスを表示する部分をサボってテキストボックスにし、そこにテキストとしてマトリックスとスコアを表示するプログラムにしていました。さすがにそれではみすぼらしすぎますね。というわけで、行列を表示するGUIを設計してみたいと思います。

行列状のGUIの配列と言ったらパッと思い浮かぶのはGridでしょうか。比較的簡単に綺麗に等間隔にGUIコンポーネントを配置できるので、普段からGUI設計では多用しているかと思います。

一方、今回の2048ではこのマトリックスのサイズを自由に設定できるような設計をしていました。
一般的に出回ってるゲームでは4x4ですが、別に4x4にしなければならない理由はどこにも無いので、そういったことにとらわれないコードを書いています。
しかし、Gridは通常設計段階でどのように分割するかがわかっているものです。そもそもXAMLがそこまでプログラムの条件に合わせて柔軟に動的に何かを生成するものでもないですしね。
となると、ListViewDataGridなどが思い浮かぶわけですが、いまいちピンときませんね。

というわけで、今回はGridベースにマトリックスを表示するコントロールを自作してみようと思います。
コードビハインドでいじってGridのRowDefinitionsなどをC#からいじれば動的に行の追加等はできます。が、ViewModelとViewのコードビハインドは連携しにくいので、というより、コードビハインドでViewに完結しない操作をするのは越権行為ですね。というわけで、マトリックスを表示するコントロールという形でその機能を分離し、View内完結に収めてやろうということにしたわけです。

<UserControl x:Class="Game2048.Views.GameTable"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" >
    <Grid Name="grid" />
</UserControl>

自作したコントロール、GameTableはこんな感じです。Gridを中に作って、あとはコードビハインドからいじれるように名前を定義してあげるだけです。

メインディッシュはこのXAMLのコードビハインドですね。3つの依存関係プロパティを用意してあげます。
あ、依存関係プロパティって何?って方もいるかもしれませんが、まあ、これを使うとXAMLからバインディングできるようになるって理解で良いと思います。多分。

/// <summary>
/// グリッドを表示するかどうか
/// </summary>
public bool ShowGridLines
{
    get { return (bool)this.GetValue(ShowGridLinesProperty); }
    set { this.SetValue(ShowGridLinesProperty, value); }
}
public static DependencyProperty ShowGridLinesProperty = DependencyProperty.Register("ShowGridLines", typeof(bool), typeof(GameTable), new UIPropertyMetadata((d, e) => {
    ((GameTable)d).grid.ShowGridLines = (bool)e.NewValue;
}));

/// <summary>
/// セルのテンプレート
/// </summary>
public DataTemplate ItemTemplate
{
    get { return (DataTemplate)this.GetValue(ItemTemplateProperty); }
    set { this.SetValue(ItemTemplateProperty, value); }
}
public static DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(GameTable));

まずはこの2つの依存関係プロパティです。比較的やっていることが軽いのでまとめて紹介します。
1つ目はグリッドを表示するかどうかです。このプロパティをそのままGridに転送しています。
2つ目はセルのテンプレートです。このままいくとマトリックスのセルをそのままユーザーコントロールのコードビハインドから生成することになってしまって、XAML側からの編集性に欠けます。というわけで、テンプレートをこのプロパティを介して渡すことで、XAML側でデザインしたセルをこちらで使えるようにしています。

/// <summary>
/// 表示するマトリックス
/// </summary>
public ObservableMatrix2D<int> Matrix
{
    get { return (ObservableMatrix2D<int>)this.GetValue(MatrixProperty); }
    set { this.SetValue(MatrixProperty, value); }
}
public static DependencyProperty MatrixProperty = DependencyProperty.Register("Matrix", typeof(ObservableMatrix2D<int>), typeof(GameTable), new UIPropertyMetadata((d, e) => {
    INotifyCollectionChanged NewValue = e.NewValue as INotifyCollectionChanged;
    INotifyCollectionChanged OldValue = e.OldValue as INotifyCollectionChanged;

    GameTable me = (GameTable)d;

    if(NewValue != null)
        NewValue.CollectionChanged += me.CollectionChanged;

    if(OldValue != null)
        OldValue.CollectionChanged -= me.CollectionChanged;

    me.MatrixSizeChanged();
}));

void CollectionChanged(object o, NotifyCollectionChangedEventArgs e)
{
    if(e.Action == NotifyCollectionChangedAction.Replace)
        MatrixContentChanged();
    else
        MatrixSizeChanged();
}

/// <summary>
/// マトリックスのサイズが変更された
/// </summary>
void MatrixSizeChanged()
{
    grid.RowDefinitions.Clear();
    grid.ColumnDefinitions.Clear();
    grid.Children.Clear();
    if(Matrix != null) {
        for(int i = 0; i < Matrix.RowCount; i++)
            grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
        for(int i = 0; i < Matrix.ColumnCount; i++)
            grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });

        for(int i = 0; i < Matrix.RowCount; i++) {
            for(int j = 0; j < Matrix.ColumnCount; j++) {
                FrameworkElement fe = (FrameworkElement)ItemTemplate.LoadContent();

                fe.DataContext = Matrix[i, j];
                fe.SetValue(Grid.RowProperty, i);
                fe.SetValue(Grid.ColumnProperty, j);

                grid.Children.Add(fe);
            }
        }
    }
}

/// <summary>
/// マトリックスの中身が変更された
/// </summary>
void MatrixContentChanged()
{
    foreach(FrameworkElement child in grid.Children) {
        int r = (int)child.GetValue(Grid.RowProperty);
        int c = (int)child.GetValue(Grid.ColumnProperty);

        child.DataContext = Matrix[r, c];
    }
}

最後に、表示するマトリックスを保管する依存関係プロパティと、それ関連のメソッドです。
マトリックスはそのままint[,]型で与えたんじゃ中身の変更に対しての通知機能が一切ないので、 INotifyCollectionChangedインターフェースを実装したObservableMatrix2D<T>クラスを用意してそれを使っています。変数の変更は依存関係プロパティの登録時に与えるUIPropertyMetadataのコンストラクタの引数に与えるデリゲートで受け取れますが、これはあくまでもこの変数の変更であって、INotifyCollectionChangedが引き起こす変更通知イベントは拾わないので、これに新たにイベントハンドラを付け加えて受信できるようにしています。
マトリックスのサイズが変更されるといった大きな変更では、Gridの行と列を新たに作り直し、その後にそのGridのそれぞれのセルにDataTemplateのインスタンスを作って設定します。DataTemplateはLoadContentメソッドでそのインスタンスを作れます。インスタントを作った後は、DataContextにマトリックスのデータを埋め込み、あとはGridの添付プロパティで行と列のインデックスを指定してやっています。
マトリックスの中身が変更された時は、逆にGridの子コントロールからその添付プロパティを抽出して座標を選択し、DataContextを更新することで対応しています。

これで、汎用的なマトリックス表示コントロールができました。

なお、上で出てきたObservableMatrix2D<T>ですが、まあ、この実装は内部で2次元配列を持ってどうこうやる陳腐なものなのであえて細かく解説はしませんが、ピンポイントで説明するとしたらここでしょう。

public T this[int row, int column]
{
    get { return data[row, column]; }
    set
    {
        if(!object.Equals(data[row, column], value)) {
            T old = data[row, column];

            data[row, column] = value;
            if(CollectionChanged != null)
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old));
        }
    }
}

マトリックスのある要素が変更された時はINotifyCollectionChangedで定義されたCollectionChangedイベントを発生させています。NotifyCollectionChangedEventArgsではどのような変化が起こったのかを示す列挙型を指定しますが、これは、NotifyCollectionChangedEventArgsのコンストラクタによって指定できる列挙型のメンバが決まっているので、MSDNをよく読んで作ってください。

さて、実際にこの作ったコントロールを使っていきます。

<v:GameTable Matrix="{Binding Matrix}" ShowGridLines="False" >
    <v:GameTable.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Rectangle Stroke="Black" StrokeThickness="1" Fill="{Binding Converter={StaticResource NumToBrushConv}}" RadiusX="10" RadiusY="10" />
                <TextBlock Text="{Binding Converter={StaticResource ZeroDontSshowConv}}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30" />
            </Grid>
        </DataTemplate>
    </v:GameTable.ItemTemplate>
</v:GameTable>

このようにMainWindow.xamlで定義します。Matrixの中身はViewModelのデータをバインディングします。今回は、セルのテンプレートでグリッド枠相当の描写をやるので、ShowGridLinesはFalseにしています。

RectangleとTextBlockでは、それぞれFillとTextのデータにそのMatrixの各要素の値がバインディングされています。
TextBlockの場合は、そのままバインディングするとゼロもそのまま表示されてしまうので、ゼロの場合は表示しないようにするようなコンバーターを実装してそれを読ませています。
Rectangleの場合は、数字の大きさによって色を変化させるようなコンバーターを実装して、それをかませることで数値を色に変換しています。

さて、これを周囲の上下左右のボタンで操作できるようにしています。

<Button DockPanel.Dock="Left" Content="←" IsEnabled="{Binding CanMoveLeft}" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MoveLeft" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

これは左の実装ですが、上下左右4種類実装してあげています。
また、キーボードの矢印キーを使っても同様の操作ができるべきなので、それも実装してあげます。

<ei:KeyTrigger Key="Left">
    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MoveLeft" />
</ei:KeyTrigger>

Livetでは、このようにKeyTriggerをi:Interaction.Triggersの中に入れてあげれば、キー入力から直接メソッドをバインディングして呼び出せるので便利です。

これで完成です。


どうです?それっぽくなりましたよね?
ですがまあまだスコアの表示の実装とか、設定、リセット等の実装ができていません。
その辺りが煮詰まってきたら、オートソルバーの実装かな?

どれだけ時間のかかることやら。

2014年9月11日木曜日

2048のロジックを実装する

私はブームを2周くらい遅れて追いかける性質があるようで、ここ最近、2048というゲームにはまっています。ググればたくさん出てきますし、オープンソースってだけあって亜種も結構あるようなのでまあリンクは張らずにおいておきます。

さて、このようにシンプルなゲーム、ある程度やり始めると「これ、自動でやらせればよくね?」って思うようになってきますよね。えっ、ならないだって?:;(∩´﹏`∩);:なってください~。昔、マインスイーパーを自動で解くプログラムを書いたことがありましたが、今回は2048を解いていきたいと思います。

といってもスマホアプリは完全に守備範囲外ですし、別にゲームをやりたいんじゃなければスマホアプリにする必要もありません。
というわけで、お得意のC#で作ろうと思いましたが、オープンソースの2048はJavascriptで作られているようです。というわけで、2048をC#で再実装してしまいました。


まずは、オートソルバーを想定してインターフェースを実装してみましょう。
要するに、2048で必要な操作や、ゲームに際して提供する必要がある操作をインターフェースで提供してやります。そうすることで、別の人が書いた2048のアプリケーションでもそのインターフェースさえ実装していればオートソルバーが使えるようになります。

public interface I2048
{
    /// <summary>
    /// 行の本数
    /// </summary>
    int RowCount { get; }

    /// <summary>
    /// 列の本数
    /// </summary>
    int ColumnCount { get; }

    /// <summary>
    /// スコア
    /// </summary>
    uint Score { get; }

    /// <summary>
    /// ゲームオーバーかどうか
    /// </summary>
    bool IsGameOver { get; }

    /// <summary>
    /// row, columnの値
    /// </summary>
    /// <param name="row">行</param>
    /// <param name="column">列</param>
    /// <returns>その値</returns>
    uint this[uint row, uint column] { get; }

    /// <summary>
    /// 指定した行を取得するメソッド
    /// </summary>
    /// <param name="row">行番号</param>
    /// <returns>行の中身</returns>
    IEnumerable<uint> GetRow(int row);

    /// <summary>
    /// 指定した列を取得するメソッド
    /// </summary>
    /// <param name="column">列番号</param>
    /// <returns>列の中身</returns>
    IEnumerable<uint> GetColumn(int column);


    /// <summary>
    /// 左に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveLeft();

    /// <summary>
    /// 左に動かせるかどうか
    /// </summary>
    bool CanMoveLeft { get; }

    /// <summary>
    /// 右に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveRight();

    /// <summary>
    /// 右に動かせるかどうか
    /// </summary>
    bool CanMoveRight { get; }

    /// <summary>
    /// 上に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveUp();

    /// <summary>
    /// 上に動かせるかどうか
    /// </summary>
    bool CanMoveUp { get; }

    /// <summary>
    /// 下に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveDown();

    /// <summary>
    /// 下に動かせるかどうか
    /// </summary>
    bool CanMoveDown { get; }
}

これくらいの機能さえあれば2048のゲームの状態を取得することも操作することもできますかね。
2048の行数、列数はともに4が一般的ですが、別に4じゃないとこのゲームができないわけではないので(ゲームバランスの問題はあるでしょうけども)任意の列・行に対応できるような仕様にしています。
インデクサーで値が取得できればGetRowとかGetColumnは必須ではないですが、このゲームは行単位や列単位でものを見ることが多いので実装を義務付けても罰は当たらないでしょう。

さて、このインターフェースを実装していきます。
C言語系の言語は変数名等の最初が数字にできないので「2048」とかいうクラスを作るわけにはいきません。なので、Boardっていうクラスにしてみました。

コンストラクタはゲームの初期化を行っています。

public Board(int row, int column)
{
    if(row < 2 || column < 2)
        throw new ArgumentOutOfRangeException("The row and the column count must be bigger than or equals 2");

    RowCount = row;
    ColumnCount = column;

    table = new uint[row, column];

    PossibilityOfTwo = 0.75;

    MakeNumberAppear();
    MakeNumberAppear();
}


1マスだけではゲームができないので、2x2以上を義務付けています。正方形である必要はなく、例えば5x3などの長方形でもOKです。
PossibilityOfTwoは新しい数字が湧き出してくるときに2が出る確率です。残りは4が出てきます。とりあえず確率75%にしてみました。
MakeNumberAppearは新しい数字を湧き出させるメソッドです。ゲームスタート時点では2ヶ所に数字を表示しますから、2回呼び出しています。

ここでMakeNumberAppearの中身を見てみましょう。

/// <summary>
/// 新しい数字を沸き上がらせるメソッド
/// </summary>
private void MakeNumberAppear()
{
    var zero = Enumerable.Range(0, RowCount).
        SelectMany(row => GetRow(row).Select((val, col) => new { Row = row, Col = col, Val = val })).
        Where(p => p.Val == 0).
        ToArray();

    if(zero.Length == 0)
        throw new InvalidOperationException("There are no free space.");

    uint cellNumber = (uint)(random.Next() % zero.Length);

    table[zero[cellNumber].Row, zero[cellNumber].Col] = (random.NextDouble() < PossibilityOfTwo) ? 2u : 4u;
}

まずは、ゼロのセルを列挙しています。
とりあえず全てのセルを列挙し、匿名クラスでその行番号、列番号とそのセルの値を表すクラスを作り、最後にWhereでセルの値がゼロのもののみを拾ってきています。
そして、そして乱数でそのゼロのセルのなかから1つを選び、さらにそこから指定した確率で2か4を入れています。


指定した行をまるごと引っ張ってくるメソッドGetRowですが、実はとても簡単に実装できます。

/// <summary>
/// 指定した行を取得するメソッド
/// </summary>
/// <param name="row">行番号</param>
/// <returns>行の中身</returns>
public IEnumerable<uint> GetRow(int row)
{
    for(int i = 0; i < ColumnCount; i++)
        yield return table[row, i];
}

IEnumerableの概念を最初に勉強した時は「使う分には便利だけど実装はクッソめんどくさいだろうなー」って思いましたが、yield文によって上記のように超簡単に実装できるんですよね。なんていうか、for文から抜けてないかのような表記なのに抜けたような挙動の構文ってことでフローがわかりにくいようにも思えますが、この構文のシンプルさは私は大好きですね。

最後に、上下左右に動かすプログラムを実装しましょう。これは案外複雑です。
  1. 動かした方向に数が入ってるすべてのセルを詰める
  2. 動かした先のほうから反対方向へ順に見て行って、その反対方向側の隣のセルに同じ値があった場合、合体する。合体した場合は、合体した次のセルから隣のセルを見ていき、合体しなかった場合は次のセルを見ていく。
  3. ゲームスコアは、合体した後の値が加算される。
1はそこまで難しくなく実装できそうです。しかし、2が面倒くさいですね。普通に「隣のセルと同じだったら合体する」みたいなロジックで組んでしまうと、例えば4 2 2って並んでいた時に右に動かしたら、1回目で2 2が合体して4 4になり、さらにここで並んでしまうのでこれが合体して8になってしまいます。そのような現象を回避するために、動かした先のほうから反対方向へ順番に見ていく必要があります。結構複雑な感じになりそうです。

あ、ここで2の動作を「縮退」と名付けました。なんか2つのセルが合体して1つのセルに縮まる感じです。量子力学とかサーバー運用等に関して言う縮退の意味を考えるとどうなのかなーって気もしなくもないですが、なんていうか、ほかに良い日本語が見つからなかったので。

さて、ここで左に動かすメソッドを見ていきます。

/// <summary>
/// 左に動かす
/// </summary>
/// <returns>動かせたかどうか</returns>
public bool MoveLeft()
{
    if(CanMoveLeft) {
        for(int i = 0; i < RowCount; i++) {
            uint score;

            SetRow(i, GetRow(i).Where(p => p > 0).Degenerate(out score).AddZero(ColumnCount));

            Score += score;
        }
        MakeNumberAppear();
    }
    return CanMoveLeft;
}

あらかじめ動かせるかを調べてから、動かせた場合に各行について処理をしています。行を取得し、ゼロのセル、すなわち何も数字が無いセルをWhereで省きます。その後、縮退のDegenerateメソッドを呼び、最後に列数になるまでゼロのセルを後ろに付けたします。これだけです。DegenerateとAddZeroは私が実装した拡張メソッドです。中身は後述します。

このDegenerateメソッドのアルゴリズム、動かせなかったら動かさないまま値を返すので一見あらかじめ動かせるかどうかを調べてから動かす必要はなさそうです。ではなぜこれが必要かというと、2か4を湧き上がらせる条件として「動かした」ということが必要だからです。動かせる時のみ動かし、新しい数字を湧き上がらせるという手順を踏んでいます。

さて、上記の例は左へ動かす場合ですが、同様に上へ動かす場合はGetRow(i)の代わりにGetColumn(i)を呼び出してあげれば問題ないことは容易く想像がつくでしょう。もちろん、AddZeroの引数とかには注意してあげる必要がありますが。
じゃあ、右や下へ動かす場合はどうするか。
簡単です。Whereの後とAddZeroの後にReverse()を入れてあげるだけです。

GetRow(i).Where(p => p > 0).Reverse().Degenerate(out score).AddZero(ColumnCount).Reverse();

行の中身の順番を入れ替えて縮退させれば現象は左右逆になりますよね。下へ動かす場合も同様です。これでだいぶシンプルに各方向へ動かすメソッドが実装できました。

ここで、Degenerate()とAddZero()の中身を見てみましょう。まずはDegenerateから。

/// <summary>
/// 行または列をくっつけて成長させるメソッド
/// </summary>
/// <param name="array">成長前の配列</param>
/// <param name="score">点数</param>
/// <returns>成長後の配列</returns>
public static IEnumerable<uint> Degenerate(this IEnumerable<uint> array, out uint score)
{
    score = 0;
    if(array.Count() <= 1)    //要素が1個以下ならそのまま
        return array;
    else if(array.First() == array.Skip(1).First()) {    //次と同じだったら足して、残りをDegenerateする
        uint add = array.First();
        IEnumerable<uint> ret = array.Take(1).Select(p => p * 2).Concat(array.Skip(2).Degenerate(out score));
        score += add;
        return ret;
    } else    //次と同じじゃなかったら、その次のからDegenerateする
        return array.Take(1).Concat(array.Skip(1).Degenerate(out score));
}

3パターンにわかれます。
まず、要素が1個以下ならば縮退は起こりえないのでそのまま返します。
次に、最初の要素と2つ目の要素が同じだったら1つ目の要素をarray.Take(1).Select(p => p * 2)で2倍にしています。さらに、この配列を2つスキップした先の要素をDegenerateで再帰呼び出しします。そして、2倍にした側にConcatで連結しています。このとき、点数計算も同時にしています。
最後のパターンは1つ目と2つ目が同じじゃなかった時、2つ目の要素以降を再帰呼び出しで縮退させています。
これで全ての動作が網羅できますよね。我ながらなかなかシンプルにまとめあげたと思っています

つづいてAddZeroです。

/// <summary>
/// 指定した個数の要素数になるまで末尾にゼロをつなげるメソッド
/// </summary>
/// <typeparam name="T">型</typeparam>
/// <param name="array">元の配列</param>
/// <param name="TotalLength">最終的な長さ</param>
/// <returns>ゼロをつなげた配列</returns>
public static IEnumerable<T> AddZero<T>(this IEnumerable<T> array, int TotalLength)
{
    int count = array.Count();

    if(count == TotalLength)
        return array;
    else if(TotalLength < count)
        throw new InvalidOperationException("TotalLength must be bigger than or equals to array length");
    else
        return array.Concat(Enumerable.Range(1, TotalLength - count).Select(p => default(T)));
}

指定した個数より元の配列のほうが長かったら例外を吐くようにしています。
Enumerable.Rangeで追加分の長さの連続した数列を作りますが、これは必要ないのでSelectで使わないで捨てています。ジェネリックメソッドにしているので、default(T)になりますね。


最後に、動かせるかどうかを示すプロパティについて見てみます。
とはいっても、動かした後のデータと動かす前のデータをSequenceEqualで比べてるだけですがね。

/// <summary>
/// 左に動かせるかどうか
/// </summary>
public bool CanMoveLeft
{
    get
    {
        for(int i = 0; i < RowCount; i++) {
            if(!GetRow(i).Where(p => p > 0).Degenerate().AddZero(ColumnCount).SequenceEqual(GetRow(i)))
                return true;
        }
        return false;
    }
}

先述した通り、このプロパティで動かせるかを判断した後、改めて実際に動かす動作をしています。2度手間であまり賢い実装じゃないかもしれませんが、まあ、まとまりはいいのでこれでいいでしょう。

これに付随して、ゲームオーバーを表すプロパティ「IsGameOver」も実装しました。ゲームオーバーしたかどうかはすなわち全方向に動かせないということですので、そうなるように実装しました。

/// <summary>
/// ゲームオーバーかどうか
/// </summary>
public bool IsGameOver
{
    get
    {
        return !CanMoveLeft && !CanMoveRight && !CanMoveUp && !CanMoveDown;
    }
}


さて、ここまで見てもらってわかると思いますが、C#でデータの集まりを操作しようとするとガチガチのLINQ実装になりました。LINQは遅延評価なので、何度も同じIEnumerable<T>を評価するような式は実効効率の意味ではあまり賢くないかもしれませんが(1度配列などに落としておくと早くなると見込める)、まあ、今の段階では特に考えなくていいでしょう。全部LINQで書いてしまったほうが綺麗ですし。


さて、これでだいたいのロジックが出そろいました。
今回のテーマが「ロジックの実装」で、最終目標が「2048のオートソルバー」なのでUIは今のところ全然本気出して実装していませんが、とりあえず動作を確認するために簡単に実装しました。
こんな感じになっています。


次回はUIの整理かな~?