オセロ作ってみた|パート3〜オセロの構造を定義する〜

アイキャチ画像
2022/09/07
2022/09/07
gAbL8V8S

今回はチャレンジ企画第3弾『オセロを作る』のパート3です。TypeScriptを使ってオセロの骨組みである行やマス、石を表現していきましょう。

みなさんこんにちは!

この記事は『オセロをつくる』チャレンジ企画の第2回目となっています。

この記事のテーマは

  • オセロの構造を理解する
  • TypeScriptで構築する
  • オブジェクト指向で定義する

というテーマで進めていきます。

まずは今回オセロの構造を定義するファイルを作成しましょう。

作成するファイル

ではさっそくファイルを作成していきたいので、srcフォルダの中にmodelフォルダを作成しましょう。

次にmodelフォルダ内にreverse.tsというTypeScriptファイルを作成しましょう。

ちなみに今回オセロの構造、ゲームのロジックは基本的にこのreverse.tsに書いていきます。

reverse.tsが作成できたらベースとなるクラスとenumを定義していきます。

boardクラス
  • 盤面の一番大枠となるクラス。8つのrowクラスを持っている。
rowクラス
  • 盤面の行を定義するクラス。全部で8つのSquaresクラスを持っている。
squaresクラス
  • 盤面のマス目を定義するクラス。石の座標、石の状態を持っている。
Pointクラス
  • 座標を表現するクラス

クラスを定義する

export class board {}
export class row {}
export class squares {}
export class Point {}

enumを定義する

enumイナムとは列挙型と呼ばれていて次のような特徴があります。

  • 使うことができる複数個の定数を明確にできる
  • 使い回しがきく

ただし現在のTypeScript公式ドキュメントではenumの利用が非推奨となっています。

理由は主に3つ

  1. enumにアクセスするとき本来意図しない方法でアクセスできてしまう場合がある
  2. バンドル時、enumの部分がデッドコードになる可能性がある。(Tree-shakingが効かない)
  3. `TypeScriptのコンパイルオプション--isolatedModules`を利用するとコンパイルエラーになる。(そのためファイルごとに単一のモジュールとしてコンパイルを実行できない)

TypeScriptという言語の性質上この問題に関しては仕方がない部分があります。

ですが今回はenumイナムを使用します。

理由は、

  1. enumの定義をするときにキーを明示的に割り当てる
  2. enumがデッドコードにならないよう開発を考えてる
  3. `TypeScriptのコンパイルオプション--isolatedModules`は使用しない
  4. 開発する時の利便性を重視

のような感じにしようと考えているためです。

※心配な方はUnion型で定義する方法がTypeScriptでは推奨されているのでそのやり方で開発しましょう。

定義するenumについて

enumイナムで定義するSquaresStateの役割はマスの状態を定義することです。

マスの状態は全部で3パターン考えられます。

  • なにもない(初期状態)
  • 白い石が置かれている
  • 黒い石が置かれている

これを定数になおすと、

  • None→なにもない
  • White→白い石
  • Black→黒い石

のようになりました。

これらの要素、コーディングの方針(キーの明示的な割り当て)をコードとして表現すると次のようになります。

export enum SquaresState {
    White = "white", // 白い石が置かれている
    Black = "black", // 黒い石が置かれている
    None = "node", // 何もない
}

ここまでできたら下のような状態になっていると思います。

export class board {}
export class row {}
export class squares {}
export class Point {}
export enum SquaresState {
    White = "white", 
    Black = "black", 
    None = "none", 
}

ここからはそれぞれのクラスへ処理の実装をしていきましょう。

boardクラス

ではまずは1番大枠となるクラス、boardクラスからです。

boardクラスはこの後定義するRowクラスを全部で8コもっています。

まずはrowクラスを格納するためのメンバー変数を用意しましょう。

export class board {
    public row:row[]
}

次にコンストラクターを定義します。

コンストラクターにはインスタンスが生成された時にrowクラスを8コ配列に格納する初期化処理をまずは定義しておきましょう。

初期化処理はこんな感じです。

export class board {
    public row:row[]
    constructor(){
        this.row = [...Array(8).keys()].map((i) => new row(i));
    }
}

[...Array(n).keys()].map(…)について

ちょっとこの部分は複雑なので解説します。

というのもこの処理他のソースコードでもたまに出てくるやり方で、指定した数(n)の要素をもつ配列を自分の欲しい形式で作り出すことができます。

まずこのまま実行するとどうなるかというと、

/* 
[...Array(8).keys()].map((i) => new row(i));
の実行結果
*/
[
    rowクラス,
    rowクラス,
    rowクラス,
    rowクラス,
    rowクラス,
    rowクラス,
    rowクラス,
    rowクラス,
]

こんな状態になります。

今回は8個のrowクラスを持つ配列を作りたかったのでこれでOKです。

ではどのような処理の結果こうなったか、簡単に解説します。

まずrowクラスを格納するための8つ要素をもつ配列を用意します。

Array(8)
// 実行結果
[
    null,
    null,
    null,
    null,
    null,
    null,
    null,
    null,
]

この時Arrayメソッドは指定された長さの配列を返します。

次にkeysメソッドを使います。

keysメソッドは配列の各要素のキー名を順に出力するイテレータを返すようになっています。

Array(8).keys()
// 実行結果
{}

JavaScriptのイテレータはスプレッド構文で展開することができ、keysメソッドのイテレータを展開すると配列のキーを取得できる。

...Array(8).keys()
// 実行結果
0,1,2,3,4,5,6,7

このままでは次に使うmapメソッドを使えないので配列にしておきます。

[...Array(8).keys()]
// 実行結果
[0,1,2,3,4,5,6,7]

ではここからmapメソッドを使って配列の値全てに処理を実行します。

[...Array(8).keys()].map()

mapメソッドは()内に関数を定義することでその処理を配列の全ての要素に実行し新たな配列を作ってくれます。

[...Array(8).keys()].map(()=> new row())

一旦はこのままでもいいのですが、今回は元々入っている値をrowクラスに渡してそのまま行番号にするようrowクラス内部で処理をする予定なので引数を渡しておきます。

[...Array(8).keys()].map((i)=> new row(i))

出来上がった配列をクラス変数の初期化のために代入します。

this.row = [...Array(8).keys()].map((i) => new row(i));

これでboardクラスにrowクラス8個を配列として持たせることができました。

今の時点でこんな感じになっていればOKです。

export class board {
    public row:row[]
    constructor(){
        this.row = [...Array(8).keys()].map((i) => new row(i));
    }
}
export class row {}
export class squares {}
export class Point {}
export enum SquaresState {
    White = "white", 
    Black = "black", 
    None = "none", 
}

rowクラス

ではここからはrowクラスを実装していきます。

ここで一旦rowクラスの役割をおさらいします。

rowクラスの役割

  • 盤面の行を定義するクラス
  • 全部で8つのSquaresクラスを持っている
  • (行番号がある)※後から追加

rowクラスはboardクラスで解説したやり方で、Squaresクラスを8個持つ配列を作ります。

ちなみにこの後実装予定のSquaresクラスも引数として座標を受け取るようにしたいのでここで値を渡しておきます。

rowクラスの引数はそのまま行番号としたいので

export class row {
    public Squares:squares[]
    public RowNumber:number
    constructor(num:number){
        this.RowNumber = num;
        this.Squares = [...Array(8).keys()].map((i) => new squares(i,num));
    }
}

メンバー変数(RowNumber)に格納しておきます。

rowクラスはメインの役割が行の表現なので、rowクラスとしての実装はこれで完成です。

ここまでの作業で下のような状態になっていれば大丈夫です。

export class board {
    public row:row[]
    constructor(){
        this.row = [...Array(8).keys()].map((i) => new row(i));
    }
}
export class row {
    public Squares:squares[]
    public RowNumber:number
    constructor(num:number){
        this.RowNumber = num;
        this.Squares = [...Array(8).keys()].map((i) => new squares(i,num));
    }
}
export class squares {}
export class Point {}
export enum SquaresState {
    White = "white", 
    Black = "black", 
    None = "none", 
}

Squaresクラス

次はSquaresクラスを実装していきます。

このクラスの役割は

  • マス目の座標を持っている
  • 石の状態を持っている

この2つの要素を持っていることになります。

まずは座標をクラスに持たせましょう。

持たせる座標は引数をベースにします。

export class squares {
    public x: number;
    public y: number;
    constructor(x: number,y: number){
        this.x = x;
        this.y = y;
    }
}

そして石の状態を持たせるわけですが、前半に定義したenumをここで使います。

状態はメンバー変数(squarestate)に格納します。

型はSquarestateとし、石の状態を代入するための変数にします。

public squarestate: Squarestate;

オセロでの初期値はなにもない状態なので、それを意味するSquarestateで初期化しておきます。

export class squares {
    public x: number;
    public y: number;
    public squarestate: Squarestate;
    constructor(x: number,y: number){
        this.x = x;
        this.y = y;
        this.squarestate = Squarestate.None;
    }
}

これでマス目に座標と石の状態を持たせることができました。

Pointクラス

最後に座標の表現や管理を簡略化するためのPointクラスを作ります。

このクラスの役割は

  • X座標をもつ
  • Y座標をもつ

一旦これだけとします。

コードはこんな感じです。

export class Point {
    public x: number;
    public y: number;
    constructor(x: number,y: number){
        this.x = x;
        this.y = y;
    }
}

引数を受け取ってそれをメンバー変数に代入するシンプルな処理になってます。

今回の記事ではここまでになります。

一旦コード全体を確認してみましょう。

export class board {
    public row: row[];
    constructor(){
        this.row = [...Array(8).keys()].map((i) => new row(i));
    }
}
export class row {
    public Squares: squares[];
    public RowNumber: number;
    constructor(num: number){
        this.RowNumber = num;
        this.Squares = [...Array(8).keys()].map((i) => new squares(i,num));
    }
}
export class squares {
    public x: number;
    public y: number;
    public squarestate: Squarestate;
    constructor(x: number,y: number){
        this.x = x;
        this.y = y;
        this.squarestate = Squarestate.None;
    }
}
export class Point {
    public x: number;
    public y: number;
    constructor(x: number,y: number){
        this.x = x;
        this.y = y;
    }
}
export enum SquaresState {
    White = "white", 
    Black = "black", 
    None = "none", 
}

ここまでだとオセロの構造だけの状態なので何もできませんが、次回以降の章で動かすためのコードを書いていきます。

まとめ

今回の記事はオセロ企画の第3弾で『構造を定義する』パートでした。

まだ骨組み状態ですがこれからオセロに必要な機能を追加していくので楽しみにしていてください。

またこの第3弾からは皆様と一緒にボク自身も知らなかったことを知り、活用できるようにしたいという裏テーマを作りました。

記事内で解説できなかった細かい概念や仕様部分は別記事にて解説する予定です。

ちなみにこの記事で解説しきれなかった部分は

  • イテレータ
  • クラス
  • メンバー変数

だと思います。

こういった細かい部分がわからないと場合によっては内容が頭に入ってこなかったり、なかなか読み進められなかったりするので大事ですよね。

今後こういった記事も増やしていく予定です。

では次回は今回作ったreverse.tsをvueに連携させるパートになります。

ペンタ

ここまで読んでくれてありがとう!

オセロシリーズ記事

参考になりそうな関連記事