○×ゲーム作ってみた

アイキャチ画像
2022/06/25
2022/06/25
9E95Pu9p

今回はプログラミング挑戦企画ということで、『○×ゲーム』を作ってみようと思います!

というわけで前回の『パスワード自動生成アプリ』に引き続きチャレンジ企画をやっていきます。

今回は第2弾ということで『○×ゲーム』に挑戦していきたいと思います。

では早速今回の成果物からみてみましょう!

出来上がったもの

下のリンクボタンから今回作成したプログラムを実行することができます。

○×ゲームで遊んでみる

注意事項

まずはじめに、この記事はコードの綺麗さや簡潔さなどは一切考えていません。

もちろんそれゆえコード自体のパフォーマンスに関してもちゃんと考えていないのであらかじめご了承ください。

そして今回ボクが書いたコードは準備ができしだいgithubで公開しているのでそちらをご覧ください。

githubでソースコードを確認する

プログラミング方針

今回使う言語は下記3つです。

  • TypeScript
  • Pug
  • Sass

今回も前回と同じくオブジェクト指向でコーディングしていきます。

ソースコード

ペンタ

まずは今回のソースコード全体を見てみよう!

コードの全体像

○×ゲームを実装するためのTypeScriptファイル

/**
* HTMLを配列と連動させるクラス (コントローラー)を作成して
* ○×ゲームの実装を行っていく。
*/
export default class model{
    private base:[number[],number[],number[]]
    private turn:number
    // 初期化処理
    constructor(){
        this.base = [[null,null,null],[null,null,null],[null,null,null]]
        this.turn = 0
    }
    // 描画処理
    private init():void{
        for (let i = 0; i < this.base.length; i++) {
            for (let j = 0; j < this.base[i].length; j++) {
                if (this.base[i][j] != null) {
                    if (this.base[i][j] === 1) {
                        document.getElementById(i + "." + j).innerText = "×"
                    }else{
                        document.getElementById(i + "." + j).innerText = "○"
                    }
                    }
            }
        }
        this.game()
    }
    // イベントリスナーを設置処理
    public set_listener():void{
        for (let i = 0; i < this.base.length; i++) {
            for (let j = 0; j < this.base[i].length; j++) {
                document.getElementById(i + "." + j).addEventListener("click",()=>{
                    if(this.game()){return}
                    if (this.base[i][j] === null) {
                        this.base[i][j] = this.change_base()
                        this.init()
                    }
                })
            }
        }
    }
    // ○と×を交互にする処理
    private change_base():number{
        if (this.turn === 0) {
            this.turn = 1
            document.getElementById("turn").innerText = "次は ○のターン"
        }else{
            this.turn = 0
            document.getElementById("turn").innerText = "次は ×のターン"
        }
        return this.turn
    }
    // ゲーム判定処理
    private game():boolean{
        for (let i = 0; i < 3; i++) {
            if (this.base[i][0] != null && this.base[i][1] === this.base[i][0] && this.base[i][2] === this.base[i][0]) {
                this.winner_test()
                return true
            }
        }
        for (let i = 0; i < 3; i++) {
            if (this.base[0][i] != null && this.base[1][i] === this.base[0][i] && this.base[2][i] === this.base[0][i]) {
                this.winner_test()
                return true
            }
        }
        if(this.base[0][0] != null && this.base[1][1] === this.base[0][0] && this.base[2][2] === this.base[0][0]){
            this.winner_test()
            return true
        }
        if(this.base[0][2] != null && this.base[1][1] === this.base[0][2] && this.base[2][0] === this.base[0][2]){
            this.winner_test()
            return true
        }
        if(this.draw()){
            document.getElementById("turn").innerText = "引き分け!!"
            return true
        }
        return false
    }
    // 勝敗をDOMに反映させる
    private winner_test():void{
        if (this.turn === 0) {
            document.getElementById("turn").innerText = "○の勝利!!"
        }else{
            document.getElementById("turn").innerText = "×の勝利!!"
        }
    }
    // 引き分け判定
    private draw():boolean{
        let null_counter:number = 0
        for (let i = 0; i < this.base.length; i++) {
            for (let j = 0; j < this.base[i].length; j++) {
                if(this.base[i][j] === null){null_counter += 1}
            }
        }
        if(null_counter === 0){return true}
        return false
    }
    // リセット処理
    public reset():void{
        this.base = [[null,null,null],[null,null,null],[null,null,null]]
        for (let i = 0; i < this.base.length; i++) {
            for (let j = 0; j < this.base[i].length; j++) {
                document.getElementById(i + "." + j).innerHTML= ""
            }
        }
        this.turn = 0
        document.getElementById("turn").innerText = "×のターン"
    }
}

ゲームのビュー画面を作るためのPugファイル

doctype html
html(lang="ja")
    head
        meta(charset="utf-8")
        title ○×ゲーム by さくらぼ.com
    body
        h1 ○×ゲーム
        .game-controller
            span(id="turn") ×のターン
            button(id="reset") reset
        .game-board
            - for(var y = 0; y < 3; y++)
                .raw
                    - for(var x = 0; x < 3; x++)
                        .cell
                            span(id= y + "." + x) 
            small © さくらぼ.com 
        .rulus
            h2 使用方法
            p 
                ol
                    li ×のターンから開始されます
                    li 先に縦・横・斜めにそろえた方が勝利
                    li 勝者が決まり次第ゲームは終了します
                |※再度ゲームを楽しみたい場合は画面のリロードをしてください

スタイル指定のためのSASSファイル

.game-board 
    margin: 0 auto
    width: 300px
    height: 300px
    box-sizing: border-box
    border: black solid 1px
    .raw
        display: flex
        .cell
            width: 100px
            height: 100px
            box-sizing: border-box
            border: black solid 1px
            span
                display: table-cell
                vertical-align: middle
                text-align: center
                font-size: 60px
                width: 100px
                height: 100px
.game-controller
    display: flex
    width: 300px
    margin: 0 auto
    margin-bottom: 5px
    justify-content: space-between

コードの全体像はこんな感じです。

今回のコードの概要を説明すると2次元配列と取得した要素を連動させ、毎ターン縦横斜めで○か×が3つ揃っているかチェックをするながれになっています。

modelクラスの定義

まず今回のプログラムの肝であるmodelクラスの変数について確認してみましょう。

クラスの定義部分は下記部分です。

export default class model{
    private base:[number[],number[],number[]]
    private turn:number
    // 初期化処理
    constructor(){
        this.base = [[null,null,null],[null,null,null],[null,null,null]]
        this.turn = 0
    }
}

この部分は2つの役割に分けて変数を定義しています。

this.base
  • ○と×の位置を数値で格納
  • 0が○、1が×とします
  • 初期値は空を意味するnullにしておく
  • 配列は2次元配列を定義
this.turn
  • ゲームのターンを数値で格納
  • 1が○、0が×とします
  • 初期値は0

DOMと連動させる

では次にDOMと連動させるための処理です。

下記部分が連携部分になります。

public set_listener():void{
    for (let i = 0; i < this.base.length; i++) {
        for (let j = 0; j < this.base[i].length; j++) {
            document.getElementById(i + "." + j).addEventListener("click",()=>{
                if(this.game()){return}
                if (this.base[i][j] === null) {
                    this.base[i][j] = this.change_base()
                    this.init()
                }
            })
        }
    }
}

ここでの処理は下記順番で実行されています

  1. getElementByIdで要素を取得
  2. 取得した要素にイベントリスナーをつける
  3. gameメソッドで勝敗を判定し勝者が決まっていなければ次の処理へ
  4. 指定したクラス内の配列が空(null)なら現在のターンに対応する数値を配列に格納する
  5. 最後にDOMへの○と×の書き込み

getElementByIdメソッドは○×ゲームが3×3の盤面なので、for分で回して9つすべての要素を取得するようにしました。

取得した要素は後にイベントリスナーを設置しクリックされたときにクラスの配列を変更する処理、ターンの変更処理を実行するように設定します。

次にここで実行されている3つのメソッド

  • change_base()
  • init()
  • game()

この3つについてを見てみましょう。

change_baseメソッド

現在のターンが○と×どちらのターンか判定しターンを入れ替えるメソッドになっています!

今がどっちのターンかif文で判定をしターンを反転させています。

画面につぎの手番を表示させる処理もここで実行していますがご自身でコードを書くときは必須ではないのでなくしても大丈夫だと思います。

private change_base():number{
    if (this.turn === 0) {
        // ターンを反転(0 → 1)
        this.turn = 1
        // 手番の表示を切り替え
        document.getElementById("turn").innerText = "次は ○のターン"
    }else{
        // ターンを反転(1 → 0)
        this.turn = 0
        // 手番の表示を切り替え
        document.getElementById("turn").innerText = "次は ×のターン"
    }
    // 最後に今どちらのターンになっているかを返す
    return this.turn
}

※後ほどの処理の際にどちらのターンになっているか必要になるため返り値としてthis.trunを返しておきます。

initメソッド

クラスの配列を元にそれぞれの要素にに○と×を書き込む処理が書いてあります!

処理の流れはこんな感じです。

  1. 配列の中身がnullかどうか確認をし,nullじゃなければ次の処理へ
  2. 配列の中身が0の場合は対応する要素に○を書き込む
  3. 配列の中身が1の場合は対応する要素に×を書き込む

以上を踏まえコードも一緒に見てみましょう!

private init():void{
    for (let i = 0; i < this.base.length; i++) {
        for (let j = 0; j < this.base[i].length; j++) {
            // 配列の中身がnullかどうか確認
            if (this.base[i][j] != null) {
                if (this.base[i][j] === 1) {
                    // 配列の中身が1なら要素のテキストを×にする
                    document.getElementById(i + "." + j).innerText = "×"
                }else{
                    // それ以外の場合は要素のテキストを○にする
                    document.getElementById(i + "." + j).innerText = "○"
                }
            }
        }
    }
    /**
    * 【ここでのgameメソッドの役割】
    * 最後にgameメソッドを実行しDOMのテキストを変更し
    * 次のターンがどちらかかプレイヤーに知らせる為
    */ 
    this.game()
}

gameメソッド

クラス内の配列をチェックし○と×が縦・横・斜めで3つ揃っていないか確認する処理

このgameメソッドがこの○×ゲームのロジック部分になります。

ここで念の為○×ゲームのルールを確認しておきましょう。

  • 縦・横・斜めに同じマークを先に揃えた方が勝ち
  • 盤面は3×3
  • 交互に○と×を配置していく
  • 一度○か×を配置するとそのマスは変更できない

『一度○か×を配置するとそのマスは変更できない』に関してはset_listenerメソッドの下記部分ですでに実装しています。

if (this.base[i][j] === null) {...} 

つまりgameメソッドでは縦・横・斜めに同じマークがそろっているか確認できればOKですね。

今回はクラス内部に2次元配列を定義しているので、その配列の中身をif文でチェックし処理を進めるものにします。

横方向に揃っているか確認する処理

横方向の確認処理の流れはこんな感じです。

  1. 配列の1番目の数値を取得
  2. 1で取得した数値が配列の2・3番目の配列と同じならwinnder_testメソッドを実行+trueを返す
  3. 配列は全部で3つあるのでfor文で処理を回す

では実際のコードも見てみましょう。

for (let i = 0; i < 3; i++) {
    // ① n番目の配列の1番めの数値を取得
    if (this.base[i][0] != null 
        // ② 先程取得した数値とn番目の配列の2番めの数値が等しいか確認
        && this.base[i][1] === this.base[i][0] 
        // ③ 先程取得した数値とn番目の配列の3番めの数値が等しいか確認
        && this.base[i][2] === this.base[i][0]
        ) {
        // ①②③すべての条件を満たす時処理を実行
        this.winner_test()
        return true
    }
}

これで横方向に○もしくは×が揃っているか確認をするメソッドを実装することができました。

次は縦方向の処理を見てみましょう。

縦方向に揃っていうか確認する処理

縦方向の処理は各配列のn番目の数値が全て等しいか確認することで実装することができます。

  1. 1番目の配列のn番目の数値を取得
  2. 1で取得した数値が2・3番目の配列のn番目と同じならwinnder_testメソッドを実行+trueを返す
  3. 配列は全部で3つあるのでfor文で処理を回す
for (let i = 0; i < 3; i++) {
    // ① 1番目の配列のn番目の数値を取得
    if (this.base[0][i] != null 
        // ② 2番目の配列のn番目の数値が1番目の配列のn番目の数値と等しいか確認
        && this.base[1][i] === this.base[0][i] 
        // ③ 3番目の配列のn番目の数値が1番目の配列のn番目の数値と等しいか確認
        && this.base[2][i] === this.base[0][i]) {
        // ①②③すべての条件を満たす時処理を実行
        this.winner_test()
        return true
    }
}

これで縦方向に○か×が3つ揃っているか確認するメソッドを実装することができました。

次は斜め方向の処理を見ていきましょう。

斜め方向に揃っていうか確認する処理

斜め方向は全部で2パターンなのでそれぞれのパターンで処理を実装していきます。

// 左上から右下
if(this.base[0][0] != null && this.base[1][1] === this.base[0][0] && this.base[2][2] === this.base[0][0]){
    this.winner_test()
    return true
}
// 右上から左下
if(this.base[0][2] != null && this.base[1][1] === this.base[0][2] && this.base[2][0] === this.base[0][2]){
    this.winner_test()
    return true
}

縦・横の処理で仕組みを理解できていれば斜めの処理は簡単に理解できるはずです!

この時点でよくわからなくなってしまった人は縦・横処理の概念がわかるまで考えてみてください。

ここまでくればほとんど○×ゲームが完成しているようなものです。

ペンタ

後は細かい仕様を実装していきましょう!

その他の細かい処理

ここからの処理はゲーム性をもたせるための処理なので○×ゲームを作るだけの目的であれば実装不要です。

勝敗結果をテキストにしてDOMに反映させる

private winner_test():void{
    if (this.turn === 0) {
        document.getElementById("turn").innerText = "○の勝利!!"
    }else{
        document.getElementById("turn").innerText = "×の勝利!!"
    }
}

このメソッドはgemeメソッド内部で実行されるものなので現在のターンを取得し、取得結果に応じてDOMを書き換えるだけの処理になっています。

引き分けの場合にテキストにしてDOMに反映させる

○×ゲームにおいて『引き分け』の状態は盤面が埋まっているにもかかわらず縦・横・斜めに同じマークが揃っていない状態です。

マークが揃っているかはgameメソッドで実装済みなので、gameメソッド内部で配列のnullが0になった時にturuを返すメソッドを作りました。

private draw():boolean{
    // カウンター用の変数
    let null_counter:number = 0
    // 配列内すべてのnullの数を確認しカウンター変数に個数を反映
    for (let i = 0; i < this.base.length; i++) {
        for (let j = 0; j < this.base[i].length; j++) {
            if(this.base[i][j] === null){null_counter += 1}
        }
    }
    // nullが0になったときにtureを返り値にする
    if(null_counter === 0){return true}
    return false
}

あとはgameメソッド内部に以下の処理を実装すれば引き分け判定を追加できる

if(this.draw()){
    document.getElementById("turn").innerText = "引き分け!!"
    return true
}

ゲームリセット処理を実装する

ゲームのリセット処理はクラスの配列、ターンの変数を元通りにするのとDOMのテキストを空にすることで実装できる。

public reset():void{
    // 配列を初期化
    this.base = [[null,null,null],[null,null,null],[null,null,null]]

    // DOMのテキストを初期化
    for (let i = 0; i < this.base.length; i++) {
        for (let j = 0; j < this.base[i].length; j++) {
            document.getElementById(i + "." + j).innerHTML= ""
        }
    }
    document.getElementById("turn").innerText = "×のターン"

    // ターンを初期化
    this.turn = 0
}

まとめ

チュンすけ

いかがでしたでしょうか?

今回はプログラミングチャレンジ企画第2弾ということでボク自身も0からだったので結構手こずりました。

コードの作成は開始から大体1時間半くらいで完成させることができました。

やってみてプログラミング初学者には○×ゲーム作成はおすすめかなと感じました。

理由は全部でつあります。

  • 配列の理解を深めることができる
  • for文・if文などプログラムにおける基礎の理解を深めることができる
  • ゲームを作るので何より楽しく作れる

プログラミング学習したてって『覚えたけどどう使うの?』とか『こんな処理使いみちあるの?』とか色々感じることがあると思います。

大事なのは覚えたことで何かを作ってみる、その過程でプログラミングの本質的な部分である論理的思考力やたのしさが芽生えてくるものです。

もしあなたが学習につまずいていたり、モチベーションがなかなか上がらない状況なら、プログラミング学習の一環で○×ゲーム作ってみてはいかがでしょうか?

とういわけで今回の記事はチャレンジ企画でした!

今後もこのようなチャレンジ企画をいくつかやる予定でいるのでお楽しみに!