Help us understand the problem. What is going on with this article?

【1時間チャレンジ】Angularでオセロを1時間で作ってみる

More than 1 year has passed since last update.

自分がプログラマーになろうと思った動機の一つに、以下の動画を見て感動したというのがあります。

【プログラミング】テトリスを1時間強で作ってみた【実況解説】
【プログラミング】オセロを1時間で作ってみた【実況解説】

ちょうどAngularを勉強しよっかなーって思っていたところで、上記動画のことを思い出して
1時間でオセロを作れるかチャレンジしてみました。

記録

45988c2 [2018/10/28 22:27:16]  initial commit
3d114ef [2018/10/28 22:29:38]  add ng-cli-pug-loader
4e4edc7 [2018/10/28 22:32:00]  template changes to pug
27d83e2 [2018/10/28 22:52:44]  create board
3fbbdb7 [2018/10/28 23:12:38]  declare style white and black
20fe7c1 [2018/10/28 23:18:03]  init board
edd03af [2018/10/28 23:24:48]  implements when on click cell, put my-trun cell
6a074a4 [2018/10/28 23:52:51]  implements flip logic
f8057d1 [2018/10/28 23:53:16]  show current turn which color
50f534c [2018/10/29 00:02:39]  show war-situation
146d652 [2018/10/29 00:09:56]  implemntns game over

1時間40分ほどかかってますね :joy:
(※コミットメッセージが適当すぎるのはご愛嬌ということで・・・)

実はタイマーも横においていて、 ng new ng-othello を実行した瞬間から測り初めていたのですが、
途中で電話がかかってきて訳わからなくなってしまったので、初回コミットからの時間ということにしています。
(どっちみち1時間以内では終わらなかった)

実装内容

せっかくなので実装内容を振り返っていきたいと思います

initial commit

まずはプロジェクト作成からです。
尊敬すべき元動画では完全まっさらな状態からスタートしていたりしたのですが、
今回はエディタやNode.js, angular-cliはインストール済みの状態で初めています

$ ng new ng-othello
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? Stylus [ http://stylus-lang.com ]

CSSのプリプロセッサはstylusを使っていきます。

add ng-cli-pug-loader

今回はHTMLのテンプレートエンジンとしてpugを使いたいので、 ng-cli-pug-loaderを追加します。

$ ng add ng-cli-pug-loader

ng add がどういう動きをするのか正直よくわかっていないので、
また調べてみないとだめですね :sweat_drops:

template changes to pug

ng-cli-pug-loader が追加できたら、 app.component.pug を作成し、
動作確認してみます。

src/app/app.component.pug
p Hello, World!!
src/app/app.component.ts
 @Component({
   selector: 'app-root',
-  templateUrl: './app.component.html',
+  templateUrl: './app.component.pug',
   styleUrls: ['./app.component.styl']
 })
 export class AppComponent {
$ npm run start

Hello world が表示されていれば大丈夫ですね!
ここからはローカルで起動したまま実装を進めていきます

create board

まずはオセロの盤面を作っていきます。
ちょうど先日社内勉強会で CSS Gridのことを教えてもらい、いい機会なのでこれを使ってみました。

ちなみに GRID GARDEN というゲームでCSS Gridについて完全に理解した状態で挑みました。

src/app/app.component.pug
-p Hello, World!!
+.board
+  .row(*ngFor="let i of [1,2,3,4,5,6,7,8]")
+    .cell(*ngFor="let j of [1,2,3,4,5,6,7,8]")

div.cellを64個作って行きます。
8回繰り返すというのが *ngFor でどうやるかわからなかったので、
とりあえず適当な配列を作っています。

src/app/app.component.styl
.board
  display grid
  grid-template-rows repeat(8, 64px)
  position: absolute
  top 32px
  left 32px
  width 64px * 8
  height 64px * 8
  border-style solid
  border-width 3px
  border-color black

  .row
    display grid
    grid-template-columns repeat(8, 64px)

    .cell
      border-style solid
      border-width 1px
      border-color black
      background-color green

.boardには64pxの高さのrowを8つ、
.rowに64pxの幅をもつcolumnを8つといった感じで配置しています。
(最初 .board に両方定義してうまく行かずに悩んでしまいました)

色などは細かく考える余裕がなかったため、適当です

CSS Grid について微塵も理解できてなかったことがわかりました

declare style white and black

白駒と黒駒を作っていきます。
画像を用意しようかと思ったのですが、疑似要素作ればいけるんじゃないかと思って
CSSだけでやっています

src/app/app.component.styl
    .cell
      border-style solid
      border-width 1px
      border-color black
      background-color green

+    .cell.white
+      &:before
+        content ""
+        position relative
+        background-color white
+        border-radius 50%
+        height 64px
+        width 64px
+        position absolute
+
+    .cell.black
+      &:before
+        content ""
+        position relative
+        background-color black
+        border-radius 50%
+        height 64px
+        width 64px
+        position absolute

border-radius 50%で丸を作っています。
.white と .black で色以外が同じなのに、DRYになってないのはいけてないですね。
stylusの記法をつかってなんとかDRYにしたいところです

init board

盤面と駒のスタイルが設定できたので、ロジックの方を作っていきます。

src/app/app.component.ts
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';

 @Component({
   selector: 'app-root',
   templateUrl: './app.component.pug',
   styleUrls: ['./app.component.styl']
 })
-export class AppComponent {
-  title = 'ng-othello';
+export class AppComponent implements OnInit {
+
+  board = [
+    [0, 0, 0, 0, 0, 0, 0, 0],
+    [0, 0, 0, 0, 0, 0, 0, 0],
+    [0, 0, 0, 0, 0, 0, 0, 0],
+    [0, 0, 0, 1, 2, 0, 0, 0],
+    [0, 0, 0, 2, 1, 0, 0, 0],
+    [0, 0, 0, 0, 0, 0, 0, 0],
+    [0, 0, 0, 0, 0, 0, 0, 0],
+    [0, 0, 0, 0, 0, 0, 0, 0]
+  ]
+
+  ngOnInit() {
+  }
+
+  getCellClass(cell: number) {
+    const cellClass = {
+      0: "",
+      1: "white",
+      2: "black"
+    }
+
+    return cellClass[cell]
+  }
src/app/app.component.pug
 .board
-  .row(*ngFor="let i of [1,2,3,4,5,6,7,8]")
-    .cell(*ngFor="let j of [1,2,3,4,5,6,7,8]")
+  .row(*ngFor="let row of board")
+    .cell(*ngFor="let cell of row", [ngClass]="getCellClass(cell)")

駒と値の関係を以下のように設定して、board変数を初期化します

  • 0: 空
  • 1: 白駒
  • 2: 黒駒

TypeScriptを使っているので、ここらへんは型をバッチリ決めたいところですね
getCellClassメソッド生やしているのも微妙です・・・

implements when on click cell, put my-turn cell

src/app/app.component.pug
 .board
-  .row(*ngFor="let row of board")
-    .cell(*ngFor="let cell of row", [ngClass]="getCellClass(cell)")
+  .row(*ngFor="let row of board, index as i")
+    .cell(*ngFor="let cell of row, index as j",
+          [ngClass]="getCellClass(cell)",
+          (click)="onClickCell(i, j)")
src/app/app.component.ts
     [0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0]
   ]
+
+  // white: 1, black: 2
+  turn = 1

...

     return cellClass[cell]
   }
+
+  onClickCell(i: number, j: number) {
+    let currCell = this.board[i][j]
+
+    if (currCell !== 0) {
+      return
+    }
+    
+    this.board[i][j] = this.turn
+
+    this.turn = 3 - this.turn

まずは簡単に空いているセルをクリックすると次ターンの色の駒を置くという処理を書いています。
ひっくり返すことができるかどうかの判定は次で実装していきます。

ちなみに以下の相手ターンにする処理は動画で学んだ書き方です
最初見たときにすっげーって感動したので、なんとなく覚えていました :sparkles:

this.turn = 3 - this.turn

implements flip logic

ひっくり返す処理を実装していきます。
オセロで一番重要なロジックですね!
なんだかんだでやはりここが一番苦労しました :sweat:

src/app/app.component.ts
    if (this.flip(i, j, this.turn)) {
      this.board[i][j] = this.turn
      this.turn = 3 - this.turn 
    }
  }

  private flip(y: number, x: number, myColor: number) {
    let canFlip = false

    for (let i = -1; i <= 1; i++) {
    for (let j = -1; j <= 1; j++) {
      if (i == 0 && j == 0) {
        continue
      }

      let _y = y + i
      let _x = x + j

      if (_y < 0 || _y >= 8 || _x < 0 || _x >= 8) {
        continue
      }

      if (this.board[_y][_x] !== 3 - myColor) {
        continue
      }

      if (this.checkFlip(_y, _x, i, j, myColor)) {
        canFlip = true
        while(this.board[_y][_x] == 3 - myColor) {
          this.board[_y][_x] = myColor

          _y += i
          _x += j
        }
      }
    }
    }

    return canFlip
  }

  private checkFlip(y: number, x: number, i: number, j: number, myColor: number) {

    if (y < 0 || y >= 8 || x < 0 || x >= 8) {
      return false
    }

    if (this.board[y][x] == 0) {
      return false
    }
    if (this.board[y][x] == myColor) {
      return true
    }
    if (this.board[y][x] == 3 - myColor) {
      return this.checkFlip(y + i, x + j, i, j, myColor)
    }
  }

クリックしたセルの8方向を調べて、相手の駒があったらその先まで調べにいくという感じですね。
以下のコードとか美しくないので、もうちょっとどうにかしたいですね :sweat:

  if (_y < 0 || _y >= 8 || _x < 0 || _x >= 8) {
    continue
  }

show current turn which color

最低限オセロができるようになったのですが、現在どちらのターンかわからないので
表示するようにしました

src/app/app.component.pug
+.info
+  .turn-info
+    span CURRENT TURN IS {{turn == 1 ? "WHITE" : "BLACK"}}
+
 .board
   .row(*ngFor="let row of board, index as i")
     .cell(*ngFor="let cell of row, index as j",

ん〜手抜き・・・

show war-situation

src/app/app.component.pug
 .info
   .turn-info
     span CURRENT TURN IS {{turn == 1 ? "WHITE" : "BLACK"}}
+  .war-situation
+    | WHITE: {{whiteNum}} vs BLACK {{blackNum}}
src/app/app.component.ts
 export class AppComponent implements OnInit {

  blackNum = 0
  whiteNum = 0

  ngOnInit() {
    this.updateNumInfo()
  }

...

     if (this.flip(i, j, this.turn)) {
       this.board[i][j] = this.turn
       this.turn = 3 - this.turn

      this.updateNumInfo()
     }
   }

...

  updateNumInfo() {
    this.whiteNum = [].concat(...this.board)
      .filter(x => x === 1)
      .length
    this.blackNum = [].concat(...this.board)
      .filter(x => x === 2)
      .length
  }

updateNumInfoというメソッドを追加して、白と黒の駒の数をそれぞれカウントしています
whiteNumとblackNumというプロパティを追加して、そこにカウントした結果を格納しています。

[].concat(...this.board)

上記のように書くことで、配列のflattenを行えるようなので、それでカウントしています。
もっといいやり方あれば教えてくださいmm

参考: JavaScript で flatten

implements game over

最後にゲーム終了の実装をしていきます。

src/app/app.component.ts
       this.turn = 3 - this.turn

       this.updateNumInfo()
+
+      this.checkGameOver()
     }
   }
...
  checkGameOver() {
    let emptyCellNum = [].concat(...this.board)
      .filter(x => x == 0)
      .length

    if (emptyCellNum == 0) {
      let winColor = this.whiteNum > this.blackNum ? "WHITE" : "BLACK"
      alert(`${winColor} is WIN !!`)
    }
  }

空のセルを数えて、ゼロだったら終了としています。
またどちらが勝利したがalertで表示しています。

(眠くて雑な実装になっていまいました。。。)

感想

(1時間を超えてしまいましたが)時間を区切ることで集中してできたので、
タイムアタックで実装してみるのは意外にいいかもと思いました。
時間があるので、ダラダラせずにある程度割り切って先に進めるので
どんどん動くものが出来上がり、楽しく実装することができました!

次はオセロにリベンジするか、テトリスに挑戦したいですね :sparkles:

コードはいかにアップしています
https://github.com/tamanugi/ng-othello

tamanugi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away