ダーツボード再現記（Ｗｅｂ版）

### まえがき
うちにダーツボードが転生してきたのですが、計算機能がないので自作します。
転生される民のごとき知識なので、もっといい方法があるかもしれません。

タイトル by
[なろうパロディ☆タイトルジェネレータ](https://aqueous-wildwood-63896.herokuapp.com/)

成果物
<p class="codepen" data-height="800" data-theme-id="light" data-default-tab="result" data-user="obq777" data-slug-hash="MWwoEXW" style="height: 800px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Darts board">
  <span>See the Pen <a href="https://codepen.io/obq777/pen/MWwoEXW">
  Darts board</a> by Oba Takeshi (<a href="https://codepen.io/obq777">@obq777</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

### 作成したい仕様
* ダーツボードを押したら点を入れたい
* 01やクリケットなど各種ゲームに対応 (現状: 501のみ)

### CSSでダーツボードを再現する
ダーツボード自体は円形ですが、部分的には扇型の集合です。
が、CSSで90°以外の扇型の再現は難しく、JavaScriptのCanvasを使う手もありますが、得点計算時の当たり判定のとき苦労しそうなので、三角形の寄せ集めでダーツボードを今回再現することにします。

方針としては

* 半径が同じ二等辺三角形を20個作成し、ボトムを基点に18°(=360°/20)ずつ回転(ループで作成)
* 交互に色が変わるため、ループのインデックスに応じて色を指定
* 純粋に部品の見た目を再現するのは難しいため、中心に頂点のある三角形で基本的に再現し、z-indexを用いて内側を隠すことで再現

![スクリーンショット 2020-03-07 23.58.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180925/ec02dee4-5b69-7792-d3e3-573f40065f71.png)

```scss
$points: 20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5;

@each $i in $points {
  .single-inner-#{$i} {
    top: 50%; left: 50%;
    position: absolute;
    height: 0; width: 0;
    @if (index($points, $i)-1) % 2 == 0 {
      border-top: black 140px solid;
    } @else {
      border-top: white 140px solid;
    }
    border-right: 22px solid transparent;
    border-bottom: 0px solid transparent;
    border-left: 22px solid transparent;
    transform: translate(-50%, -100%) rotate(#{18*(index($points, $i)-1)}deg);
    transform-origin: bottom;
    z-index: 5;
  }
}
```
ひとつの部品をつくり、18°(=360°/20)ずつ回転し、色を変えます。

<details><summary>SCSS (長いし同じものが続きます)</summary><div>
```scss
.darts-board {
  position: relative;
  height: 800px;
  width: 800px;
  margin: 0 auto;
}

.inner-bull {
  position: absolute;
  top: 50%; left: 50%;
  height: 5%; width: 5%;
  transform: translate(-50%, -50%);
  border-radius: 100%;
  background: black;
  z-index: 7;
}

.outer-bull {
  position: absolute;
  top: 50%; left: 50%;
  height: 10%; width: 10%;
  transform: translate(-50%, -50%);
  border-radius: 100%;
  background: red;
  z-index: 6;
}

$points: 20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5;

@each $i in $points {
  .single-inner-#{$i} {
    top: 50%; left: 50%;
    position: absolute;
    height: 0; width: 0;
    @if (index($points, $i)-1) % 2 == 0 {
      border-top: black 140px solid;
    } @else {
      border-top: white 140px solid;
    }
    border-right: 22px solid transparent;
    border-bottom: 0px solid transparent;
    border-left: 22px solid transparent;
    transform: translate(-50%, -100%) rotate(#{18*(index($points, $i)-1)}deg);
    transform-origin: bottom;
    z-index: 5;
  }
}

@each $i in $points {
  .triple-#{$i} {
    top: 50%; left: 50%;
    position: absolute;
    height: 0; width: 0;
    @if (index($points, $i)-1) % 2 == 0 {
      border-top: red 160px solid;
    } @else {
      border-top: blue 160px solid;
    }
    border-right: 25.14px solid transparent;
    border-bottom: 0px solid transparent;
    border-left: 25.14px solid transparent;
    transform: translate(-50%, -100%) rotate(#{18*(index($points, $i)-1)}deg);
    transform-origin: bottom;
    z-index: 4;
  }
}

@each $i in $points {
  .single-outer-#{$i} {
    top: 50%; left: 50%;
    position: absolute;
    height: 0; width: 0;
    @if (index($points, $i)-1) % 2 == 0 {
      border-top: black 280px solid;
    } @else {
      border-top: white 280px solid;
    }
    border-right: 44px solid transparent;
    border-bottom: 0px solid transparent;
    border-left: 44px solid transparent;
    transform: translate(-50%, -100%) rotate(#{18*(index($points, $i)-1)}deg);
    transform-origin: bottom;
    z-index: 3;
  }
}

@each $i in $points {
  .double-#{$i} {
    top: 50%; left: 50%;
    position: absolute;
    height: 0; width: 0;
    @if (index($points, $i)-1) % 2 == 0 {
      border-top: red 300px solid;
    } @else {
      border-top: blue 300px solid;
    }
    border-right: 47.14px solid transparent;
    border-bottom: 0px solid transparent;
    border-left: 47.14px solid transparent;
    transform: translate(-50%, -100%) rotate(#{18*(index($points, $i)-1)}deg);
    transform-origin: bottom;
    z-index: 2;
  }
}
.out {
  position: absolute;
  top: 50%; left: 50%;
  height: 670px; width: 670px;
  transform: translate(-50%, -50%);
  border-radius: 100%;
  background: black;
  z-index: 1;
}
```
</div></details>
HTMLもループできればいいのですが、変わるところがそんなにないのでコピペでどんどん増やしていきます。

<details><summary>HTML (長いし同じものが続きます)</summary><div>

```html
<div class="darts-board">
  <div class="inner-bull" onclick="hit(this.className)"></div>
  <div class="outer-bull" onclick="hit('outer-bull')"></div>
  
  <div class="single-inner-1" onclick="hit(this.className)"></div>
  <div class="single-inner-2" onclick="hit(this.className)"></div>
  <div class="single-inner-3" onclick="hit(this.className)"></div>
  <div class="single-inner-4" onclick="hit(this.className)"></div>
  <div class="single-inner-5" onclick="hit(this.className)"></div>
  <div class="single-inner-6" onclick="hit(this.className)"></div>
  <div class="single-inner-7" onclick="hit(this.className)"></div>
  <div class="single-inner-8" onclick="hit(this.className)"></div>
  <div class="single-inner-9" onclick="hit(this.className)"></div>
  <div class="single-inner-10" onclick="hit(this.className)"></div>
  <div class="single-inner-11" onclick="hit(this.className)"></div>
  <div class="single-inner-12" onclick="hit(this.className)"></div>
  <div class="single-inner-13" onclick="hit(this.className)"></div>
  <div class="single-inner-14" onclick="hit(this.className)"></div>
  <div class="single-inner-15" onclick="hit(this.className)"></div>
  <div class="single-inner-16" onclick="hit(this.className)"></div>
  <div class="single-inner-17" onclick="hit(this.className)"></div>
  <div class="single-inner-18" onclick="hit(this.className)"></div>
  <div class="single-inner-19" onclick="hit(this.className)"></div>
  <div class="single-inner-20" onclick="hit(this.className)"></div>
  
  <div class="triple-1" onclick="hit(this.className)"></div>
  <div class="triple-2" onclick="hit(this.className)"></div>
  <div class="triple-3" onclick="hit(this.className)"></div>
  <div class="triple-4" onclick="hit(this.className)"></div>
  <div class="triple-5" onclick="hit(this.className)"></div>
  <div class="triple-6" onclick="hit(this.className)"></div>
  <div class="triple-7" onclick="hit(this.className)"></div>
  <div class="triple-8" onclick="hit(this.className)"></div>
  <div class="triple-9" onclick="hit(this.className)"></div>
  <div class="triple-10" onclick="hit(this.className)"></div>
  <div class="triple-11" onclick="hit(this.className)"></div>
  <div class="triple-12" onclick="hit(this.className)"></div>
  <div class="triple-13" onclick="hit(this.className)"></div>
  <div class="triple-14" onclick="hit(this.className)"></div>
  <div class="triple-15" onclick="hit(this.className)"></div>
  <div class="triple-16" onclick="hit(this.className)"></div>
  <div class="triple-17" onclick="hit(this.className)"></div>
  <div class="triple-18" onclick="hit(this.className)"></div>
  <div class="triple-19" onclick="hit(this.className)"></div>
  <div class="triple-20" onclick="hit(this.className)"></div>
  
  <div class="single-outer-1" onclick="hit(this.className)"></div>
  <div class="single-outer-2" onclick="hit(this.className)"></div>
  <div class="single-outer-3" onclick="hit(this.className)"></div>
  <div class="single-outer-4" onclick="hit(this.className)"></div>
  <div class="single-outer-5" onclick="hit(this.className)"></div>
  <div class="single-outer-6" onclick="hit(this.className)"></div>
  <div class="single-outer-7" onclick="hit(this.className)"></div>
  <div class="single-outer-8" onclick="hit(this.className)"></div>
  <div class="single-outer-9" onclick="hit(this.className)"></div>
  <div class="single-outer-10" onclick="hit(this.className)"></div>
  <div class="single-outer-11" onclick="hit(this.className)"></div>
  <div class="single-outer-12" onclick="hit(this.className)"></div>
  <div class="single-outer-13" onclick="hit(this.className)"></div>
  <div class="single-outer-14" onclick="hit(this.className)"></div>
  <div class="single-outer-15" onclick="hit(this.className)"></div>
  <div class="single-outer-16" onclick="hit(this.className)"></div>
  <div class="single-outer-17" onclick="hit(this.className)"></div>
  <div class="single-outer-18" onclick="hit(this.className)"></div>
  <div class="single-outer-19" onclick="hit(this.className)"></div>
  <div class="single-outer-20" onclick="hit(this.className)"></div>
  
  <div class="double-1" onclick="hit(this.className)"></div>
  <div class="double-2" onclick="hit(this.className)"></div>
  <div class="double-3" onclick="hit(this.className)"></div>
  <div class="double-4" onclick="hit(this.className)"></div>
  <div class="double-5" onclick="hit(this.className)"></div>
  <div class="double-6" onclick="hit(this.className)"></div>
  <div class="double-7" onclick="hit(this.className)"></div>
  <div class="double-8" onclick="hit(this.className)"></div>
  <div class="double-9" onclick="hit(this.className)"></div>
  <div class="double-10" onclick="hit(this.className)"></div>
  <div class="double-11" onclick="hit(this.className)"></div>
  <div class="double-12" onclick="hit(this.className)"></div>
  <div class="double-13" onclick="hit(this.className)"></div>
  <div class="double-14" onclick="hit(this.className)"></div>
  <div class="double-15" onclick="hit(this.className)"></div>
  <div class="double-16" onclick="hit(this.className)"></div>
  <div class="double-17" onclick="hit(this.className)"></div>
  <div class="double-18" onclick="hit(this.className)"></div>
  <div class="double-19" onclick="hit(this.className)"></div>
  <div class="double-20" onclick="hit(this.className)"></div>
  <div class="out" onclick="hit(this.className)"></div>
</div>
```
</div></details>

### JavaScriptで当たり判定+得点計算
各HTMLがクリックされてときに`onclick`が発火し、クラス名をパースし当たった場所に応じて得点を入れます。

~~もっといい方法がある気がしますが、HTMLを最小限のコピペで書いたためこれでいきます。~~

```html
<div class="double-20" onclick="hit(this.className)"></div>
```

```javascript
// hit()からname_to_point()が呼ばれる.
// クラス名からパースを行う関数.
function name_to_point(name) {
  if (name === "out") {
    return 0;
  } else if (name === "inner-bull" | name === "outer-bull") {
    return 50;
  } else {
    let p = name.split('-');
    let basic_point = parseInt(p[p.length-1], 10);
    if (p[0] === "triple") {
      return basic_point * 3;
    } else if (p[0] === "double") {
      return basic_point * 2;
    } else {
      return basic_point
    }
  }
}
```

あとは、デバッグ用と各得点を足すコードです。
~~長いし面白くないので特に取りあげません。~~

<details><summary>JavaScript (長い)</summary><div>

```javascript
const max_point = 501
let round = 1;
let now_throw_player = 1;
let now_throw_count = 1;
let players = [[]];
// player2 = {[]};


let table = document.createElement('table');
var tr = document.createElement('tr');
var tr1 = document.createElement('tr');
append_cell(["", "player1"]);
append_cell(["point", max_point]);
table.appendChild(tr);
table.appendChild(tr1);
document.getElementById('score-table').appendChild(table);

function hit(name) {
  let p = name_to_point(name);
  players[round-1].push(p);
  let total_score = calc_total_score(players);
  table.rows[1].cells[1].firstChild.data = max_point - total_score;
  
  if (total_score == max_point) {
    alert('finish');
  } else if (total_score > max_point) {
    alert('bust');
    players[round-1] = [0, 0, 0];
    now_throw_count = 10;
  }
  
  document.getElementById("notification-click-event").innerText = round + " " + now_throw_count + " " + p + " " + total_score + " " + players;
  
  now_throw_count += 1;
  if (now_throw_count > 3) {
    let round_sum = function(arr) {
      let sum = 0;
      for (var i=0; i<arr.length; i++) {
        sum += arr[i];
      }
      return sum;
    }
    append_cell(["R"+round, round_sum(players[round-1])]);
    now_throw_count = 1;
    round += 1;
    players.push([]);
  }
};


function name_to_point(name) {
  if (name === "out") {
    return 0;
  } else if (name === "inner-bull" | name === "outer-bull") {
    return 50;
  } else {
    let p = name.split('-');
    let basic_point = parseInt(p[p.length-1], 10);
    if (p[0] === "triple") {
      return basic_point * 3;
    } else if (p[0] === "double") {
      return basic_point * 2;
    } else {
      return basic_point
    }
  }
}

function calc_total_score(scores) {
  let total = 0;
  for (let i=0; i<scores.length; i++) {
    for (let l=0; l<scores[i].length; l++) {
      total += scores[i][l];
    }
  }
  return total;
}

function append_cell(data) {
  var th = document.createElement('th');
  th.textContent = data[0];
  tr.appendChild(th);
  var td = document.createElement('td');
  td.textContent = data[1];
  tr1.appendChild(td);
}
```
</div></details>

### まとめ
CSSで簡単に表現できるだろうと思ったダーツボードでしたが、扇型など簡単に表現できるものではなく、意外と詰まりました。

今後の課題としては、

* クリケットなど他ゲームへの対応
* ダーツボードのサイズが固定なのでスマホなどに対応するため、可変にする

などです。

Bustなど対応したらJavaScript部分が長く複雑になってゆきました。
ゲームルールなどをプログラムで表現しても複雑になりにくい書き方があったら知りたい…
なんとなくVueで作れば良かったかなとも思いますが以上です。