【2019年4月版】JavaScriptのconsoleでスネークゲームをワンライナー！

# 元ネタ

先週の記事「[JavaScriptのconsoleがすごいことになってた。](https://qiita.com/koinori/items/83f119cb2d82c0ca2c1e)」に触発されて弊社の @prononami 氏がコンソール専用のスネークゲームを作ってくれました。

- [console.log()でスネークゲームを出力する](https://qiita.com/prononami/items/7571ce9bb382981a56bb)

でもやっぱりスネークゲームといえば~~N88-BASICの時から~~ワンライナーに挑戦してみるってのが定番ですよね？！

そんなわけで**スネークゲームワンライナー**に挑戦してみました！

## リファクタリング、その１

[元の実装はこちら](https://qiita.com/prononami/items/7571ce9bb382981a56bb#%E5%AE%9F%E8%A3%85)をご覧いただくとしてさっそくガシガシ、リファクタリングしていきたいと思います。

方針としては、

1. 一度しか使用していない変数・メソッドの除去
2. 複数回使用している変数・メソッドは名前を短く
3. マップ、リストで置き換えられる変換メソッドはマップ・リスト化
4. 結果が固定できるものは先に固定
5. できるだけ`if` → `?:`演算子に置換

とりあえずこんな感じでロジックはそのまま(?)でコードを圧縮した結果が以下です。

```javascript
const c = console,
  mt = Math,
  ar = Array,
  // game info
  w = 20,
  h = 16,
  br = JSON.stringify(ar.from(new ar(h), () => new ar(w).fill(0))),
  mp = ar.from(new ar(h), () => [...new ar(w).fill('%c '), '%c\n']).join(''),
  // 枠の外か判定
  gg = p => p[1] < 0 || p[1] >= w || p[0] < 0 || p[0] >= h,
  // エサをランダムな位置に出現させる
  sf = () => [mt.floor(mt.random() * h), mt.floor(mt.random() * w)],
  // 座標用配列の同一判定
  eq = (a1, a2) => a1[0] === a2[0] && a1[1] === a2[1],
  d = document,
  stBtn = d.getElementById('start'),
  //ゲームの開始終了を変更
  chgGmSt = flag => {
    isPlay = flag;
    stBtn.disabled = flag;
  },
  css_b =
    'padding-left:20px;padding-top:10px;line-height:10px;font-size:0px;background-color:',
  // draw Map to Log
  drawMap = f => {
    css = [];
    f.forEach(l => {
      css = css.concat([
        ...l.map(e => css_b + { 0: 'black;', 1: 'yellow;', 2: 'red;' }[e]),
        ''
      ]);
    });
    c.log(mp, ...css);
  };
var mVec = [0, 0],
  // control game
  isPlay = false;
//キー検知
d.onkeydown = event => {
  if (!isPlay) return;
  mVec = {
    ArrowRight: [0, 1],
    ArrowLeft: [0, -1],
    ArrowUp: [-1, 0],
    ArrowDown: [1, 0]
  }[event.key];
};
c.log('%cPress Click Start Button', 'font-size:20px;');
// Game start
function onStart() {
  // init game
  var m = [[10, 8], [10, 9], [10, 10]];
  mVec = [0, -1];
  chgGmSt(true);
  var fd = sf();
  var fr = 0;
  // game main
  var tm;
  tm = setInterval(() => {
    var f = JSON.parse(br);
    const pre = m[0];
    // sneakを動かす
    m.pop();
    m.unshift([pre[0] + mVec[0], pre[1] + mVec[1]]);
    const h = m[0];
    // sneakの衝突判定
    if (gg(h) || m.filter(e => eq(e, h)).length > 1) {
      // ゲームオーバー処理＆表示
      clearInterval(tm);
      chgGmSt(false);
      c.log('%cGame Over Score: %d', 'font-size:20px;', m.length - 3);
      return;
    }
    // get food
    if (eq(h, fd)) {
      m.push([h[0], h[1]]);
      fd = sf();
    }
    // sneak全長をフィールドにセット
    m.forEach(p => {
      f[p[0]][p[1]] = 1;
    });
    // エサをフィールドにセット
    f[fd[0]][fd[1]] = 2;
    // ログの数による描画速度低下を防ぐため定期的にリフレッシュ
    fr++;
    if (fr > 300) {
      fr = 0;
      console.clear();
    }
    drawMap(f);
  }, 1000 / 3);
}
```

一度初期化して作った配列を再利用する=ディープコピーを作成するために `JSON.stringify` を使用しています。
またロジックを見やすくするためにいったん `const` などを使っています。
`function`で定義されているものはメインの`onStart`以外は`()=>`を使って短くしています。

## リファクタリング、その２

だいぶスリムになりました。~~すでにがっつり変わってますが。。。~~また、プログラムの全体像もよくわかるようになってきました。

素のリファクタリングはこれが限界でしょうかね。。。？

ちょっとロジックの組みなおしにチャレンジします。

まずは`drawMap`で実行時に CSS の文字列をロジックで書いている箇所、もっと事前に決めておけませんかね・・・？
`'padding-left: 20px; padding-top: 10px; line-height: 10px; font-size:0px;background-color: '`の箇所はそのままですし、結局、CSS の文字列とセットで空文字列`''`を送りたいだけなわけです。
んじゃ、あらかじめ `black`にしておきますか？

それと座標の判定、もっと言えば座標値を配列で持ってますが js はタプルとか`Point`クラスみたいなものがないので数値で保持するメリット薄いんですよね。むしろ`push`とか`unshift`の時、シャローコピーにならないように配列を生成しとかないと副作用があったり手間なんですよね。。一次元配列に直して取り扱ってしまいましょう。。。

ロジックそのもののリファクタリングに着手した結果が以下です。

```javascript
const c = console,
  mt = Math,
  ar = Array,
  // game info
  wd = 20,
  ht = 16,
  p = (x, y) => y * wd + x,
  q = p => [p % wd, mt.floor(p / wd)],
  // br = JSON.stringify(ar.from(new ar(h*w), 'black')),
  mp = ar
    .from(new ar(ht), () => [...new ar(wd).fill('%c '), '%c\n'])
    .flat()
    .join(''),
  // 枠の外か判定
  gg = p => p[0] < 0 || p[0] >= wd || p[1] < 0 || p[1] >= ht,
  // エサをランダムな位置に出現させる
  sf = () => mt.floor(mt.random() * ht * wd),
  d = document,
  stBtn = d.getElementById('start'),
  //ゲームの開始終了を変更
  chgGmSt = flag => {
    isPlay = flag;
    stBtn.disabled = flag;
  },
  cs =
    'padding-left:20px;padding-top:10px;line-height:10px;font-size:0px;background-color:';
var mVec = [0, 0],
  // control game
  isPlay = false;
//キー検知
d.onkeydown = event => {
  if (!isPlay) return;
  mVec = {
    ArrowRight: [1, 0],
    ArrowLeft: [-1, 0],
    ArrowUp: [0, -1],
    ArrowDown: [0, 1]
  }[event.key];
};
c.log('%cPress Click Start Button', 'font-size:20px;');

// Game start
function onStart() {
  // init game
  mVec = [0, -1];
  var m = [p(10, 8), p(10, 9), p(10, 10)],
    fd = sf(),
    fr = 0,
    // game main
    tm;
  chgGmSt(true);
  tm = setInterval(() => {
    // sneakを動かす
    const pre = q(m[0]),
      t = [pre[0] + mVec[0], pre[1] + mVec[1]],
      pt = p(t[0], t[1]);
    // sneakの衝突判定
    if (gg(t) || m.some(e => e === pt)) {
      // ゲームオーバー処理＆表示
      clearInterval(tm);
      chgGmSt(false);
      c.log('%cGame Over Score: %d', 'font-size:20px;', m.length - 3);
      return;
    }
    m.pop();
    m.unshift(pt);
    const h = m[0];
    // get food
    if (h === fd) {
      m.push(h);
      fd = sf();
    }
    // ログの数による描画速度低下を防ぐため定期的にリフレッシュ
    fr++;
    if (fr > 300) {
      fr = 0;
      console.clear();
    }
    // エサをフィールドにセット&sneak全長をフィールドにセット
    c.log(
      mp,
      ...ar
        .from(new ar(ht * wd), (l, i) =>
          i == fd ? 'red' : m.indexOf(i) >= 0 ? 'yellow' : 'black'
        )
        .map((l, i) => ((i + 1) % wd ? cs + l : [cs + l, '']))
        .flat()
    );
  }, 1000 / 3);
}
```
うーん、ひどいｗ
やっぱり長いですね。。。

## リファクタリング、その３

これより、さらに短くするために`onStart`の引数を利用し~~汚い手を使って~~初期設定的なものは以下のように呼び出す引数側に回してしまいましょう。。。

以下がボタンのクリックイベント側。

```html
onclick="onStart(20, 16, [0, -1], [[10, 8], [10, 9], [10, 10]], this, 0)"
```
以下のように`onStart`内で使われる変数は引数に持っていきます。引数だからと言って容赦なくミュータブルに使っています。良い子はまねしないように！

```javascript
function onStart(wd, ht, mv, m, bt, fr) {
  const c = console,
    mt = Math,
    ar = Array,
    p = (x, y) => y * wd + x,
    q = p => [p % wd, mt.floor(p / wd)],
    mp = ar
      .from(new ar(ht), () => [...new ar(wd).fill('%c '), '%c\n'])
      .flat()
      .join(''),
    gg = p => p[0] < 0 || p[0] >= wd || p[1] < 0 || p[1] >= ht,
    sf = () => mt.floor(mt.random() * ht * wd),
    d = document,
    cs =
      'padding-left:20px;padding-top:10px;line-height:10px;font-size:0px;background-color:';
  d.onkeydown = event => {
    mv = {
      ArrowRight: [1, 0],
      ArrowLeft: [-1, 0],
      ArrowUp: [0, -1],
      ArrowDown: [0, 1]
    }[event.key];
  };
  var fd = sf(),
    tm;
  bt.disabled = true;
  m = m.map(e => p(e[0], e[1]));
  tm = setInterval(() => {
    const pre = q(m[0]),
      t = [pre[0] + mv[0], pre[1] + mv[1]],
      pt = p(t[0], t[1]);
    if (gg(t) || m.some(e => e === pt)) {
      clearInterval(tm);
      bt.disabled = false;
      c.log('%cGame Over Score: %d', 'font-size:20px;', m.length - 3);
      return;
    }
    m.pop();
    m.unshift(pt);
    const h = m[0];
    if (h === fd) {
      m.push(h);
      fd = sf();
    }
    if (fr++ % 300 === 0) {
      console.clear();
    }
    c.log(
      mp,
      ...ar
        .from(new ar(ht * wd), (l, i) =>
          i == fd ? 'red' : m.indexOf(i) >= 0 ? 'yellow' : 'black'
        )
        .map((l, i) => ((i + 1) % wd ? cs + l : [cs + l, '']))
        .flat()
    );
  }, 1000 / 3);
}
```
変数まわりもかなりすっきりしましたね？！

しかし、いや～、長いっすね。。。でも、1 行にしてしまいましょう。。。

## ワンライナー最終形態

不要な`var`や`const`、`}`の前の`;`も除去。
あと `Math.floor`や`Array.from` などのプロトタイプメソッドは `this` がずれても大丈夫でしょう？短い名前に代入してしまいます。(`console.log`はインスタンスメソッドなので`this`がズレると危ういのです。)

そんな感じでいろいろ刈り込んでいった結果を元の `click` イベントハンドラにどーんと載せます。

```html
onclick="((wd,ht,mv,m,bt,fr)=>{c=console,mt=Math,flr=mt.floor,ar=Array,frm=ar.from,p=(x,y)=>y*wd+x,q=p=>[p%wd,flr(p/wd)],fz='font-size:',mp=frm(new ar(ht),()=>[...new ar(wd).fill('%c '),'%c\n']).flat().join(''),sf=()=>flr(mt.random()*ht*wd),d=document,cs='padding-left:20px;padding-top:10px;line-height:10px;'+fz+'0px;background-color:';d.onkeydown=event=>{mv={ArrowRight:[1,0],ArrowLeft:[-1,0],ArrowUp:[0,-1],ArrowDown:[0,1]}[event.key]};fd=sf();var tm;bt.disabled=true;m=m.map(e=>p(e[0],e[1]));tm=setInterval(()=>{pr=q(m[0]),t=[pr[0]+mv[0],pr[1]+mv[1]],pt=p(t[0],t[1]);if(t[0]<0||t[0]>=wd||t[1]<0||t[1]>=ht||m.some(e=>e===pt)){clearInterval(tm);bt.disabled=false;c.log('%cGameOverScore:%d',fz+'20px;',m.length-3);return}m.pop();m.unshift(pt);h=m[0];if(h===fd){m.push(h);fd=sf()}if(fr++%300===0){c.clear()}c.log(mp,...frm(new ar(ht*wd),(l,i)=>i===fd?'red':m.indexOf(i)>=0?'yellow':'black;').map((l,i)=>((i+1)%wd?cs+l:[cs+l,''])).flat())},1000/3)})(20,16,[0,-1],[[10,8],[10,9],[10,10]],this,0)"
```

onclick の中は現在 `984` 文字ですｗ

## アドバイス頂きました！

アドバイス頂いた点を反映してさらに刈り込んでいきます！
一行にする直前は以下のコードになりました。

```javascript
((wd, ht, mv, m, bt, fr) => {
  c = console,
  ar = Array,
  p = (x, y) => y * wd + x,
  q = p => [p % wd, (p / wd) | 0],
  fz = 'font-size:',
  mp = ar.from(ar(ht), () => [...ar(wd).fill('%c '), '%c\n'])
      .flat()
      .join(''),
  sf = () => (Math.random() * ht * wd) | 0,
  d = document,
  cs =
      'padding:10px 0 0 20px;line-height:10px;' + fz + '0px;background-color:';
  d.onkeydown = event => {
    mv = {
      ArrowRight: [1, 0],
      ArrowLeft: [-1, 0],
      ArrowUp: [0, -1],
      ArrowDown: [0, 1]
    }[event.key];
  };
  fd = sf();
  var tm;
  bt.disabled = !0;
  m = m.map(e => p(...e));
  tm = setInterval(() => {
    pr = q(m[0]),
    t = [pr[0] + mv[0], pr[1] + mv[1]],
    pt = p(...t);
    if (t[0] < 0 || t[0] >= wd || t[1] < 0 || t[1] >= ht || m.includes(pt)) {
      clearInterval(tm);
      bt.disabled = !1;
      c.log('%cGameOverScore:%d', fz + '20px;', m.length - 3);
      return;
    }
    m.pop();
    m.unshift(pt);
    h = m[0];
    if (h === fd) {
      m.push(h);
      fd = sf();
    }
    if (fr++ % 300 === 0) {
      c.clear();
    }
    c.log(
      mp,
      ...ar.from(ar(ht * wd), (l, i) =>
        i === fd ? 'red' : m.includes(i) ? 'yellow' : 'black;'
      )
        .map((l, i) => ((i + 1) % wd ? cs + l : [cs + l, '']))
        .flat()
    );
  }, 1000 / 3);
})(20, 16, [0, -1], [[10, 8], [10, 9], [10, 10]], this, 0)
```
上記コードから不要な空白文字と`}`前のセミコロンを除去して以下のワンライナーになります！

```html
onclick="((wd,ht,mv,m,bt,fr)=>{c=console,ar=Array,p=(x,y)=>y*wd+x,q=p=>[p%wd,(p/wd)|0],fz='font-size:',mp=ar.from(ar(ht),()=>[...ar(wd).fill('%c '),'%c\n']).flat().join(''),sf=()=>(Math.random()*ht*wd)|0,d=document,cs='padding:10px 0 0 20px;line-height:10px;'+fz+'0px;background-color:';d.onkeydown=event=>{mv={ArrowRight:[1,0],ArrowLeft:[-1,0],ArrowUp:[0,-1],ArrowDown:[0,1]}[event.key]};fd=sf();var tm;bt.disabled=!0;m=m.map(e=>p(...e));tm=setInterval(()=>{pr=q(m[0]),t=[pr[0]+mv[0],pr[1]+mv[1]],pt=p(...t);if(t[0]<0||t[0]>=wd||t[1]<0||t[1]>=ht||m.includes(pt)){clearInterval(tm);bt.disabled=!1;c.log('%cGameOverScore:%d',fz+'20px;',m.length-3);return}m.pop();m.unshift(pt);h=m[0];if(h===fd){m.push(h);fd=sf()}if(fr++%300===0){c.clear()}c.log(mp,...ar.from(ar(ht*wd),(l,i)=>i===fd?'red':m.includes(i)?'yellow':'black;').map((l,i)=>((i+1)%wd?cs+l:[cs+l,''])).flat())},1000/3)})(20,16,[0,-1],[[10,8],[10,9],[10,10]],this,0)"
```
onclick の中は現在 `914` 文字！70文字も刈り込めました！！

@htsign さんありがとうございます！

## さらにアドバイス頂きました！(追記:2019/4/21)

コメントにて追加のアドバイス頂きました！
そして`onclick`に渡す直前のコードは以下のようになります。

```javascript
((wd, ht, mv, m, bt, fr) => {
  c = console,
  mt = Math,
  flr = mt.floor,
  ar = Array,
  p = (x, y) => y * wd + x,
  q = p => [p % wd, flr(p / wd)],
  fz = 'font-size:',
  mp = ar
      .from(ar(ht), () => [...ar(wd).fill('%c '), '%c\n'])
      .flat()
      .join(''),
  sf = () => mt.random() * ht * wd | 0,
  d = document,
  cs = `padding:10px 0 0 20px;line-height:10px;${fz}0px;background:`,
  d.onkeydown = event => {
    mv = [[-1, 0], [0, -1], [1, 0], [0, 1]][event.keyCode - 37]
  },
  fd = sf(),
  bt.disabled = 1,
  m = m.map(e => p(...e)),
  tm = setInterval(() => {
    pr = q(m[0]),
    t = [pr[0] + mv[0], pr[1] + mv[1]],
    pt = p(...t);
    if (t[0] < 0 || t[0] >= wd || t[1] < 0 || t[1] >= ht || m.includes(pt)) {
      clearInterval(tm);
      bt.disabled = 0;
      c.log('%cGameOverScore:%d', fz + '20px', m.length - 3);
      return
    }
    m.pop();
    m.unshift(pt);
    if (pt === fd) {
      m.push(pt);
      fd = sf();
    }
    fr++ % 300 === 0 && c.clear();
    c.log(
      mp,
      ...ar
        .from(ar(ht * wd), (l, i) =>
          i === fd ? 'red' : m.includes(i) ? 'yellow' : 'black'
        )
        .flatMap((l, i) => (i + 1) % wd ? cs + l : [cs + l, ''])
      )
    }, 1000 / 3))
})(20, 16, [0, -1], [[10, 8], [10, 9], [10, 10]], this, 0)
```

そして不要な空白文字を削除すると`onclick`は以下のようになります。

```html
  onclick="((wd,ht,mv,m,bt,fr)=>{c=console,mt=Math,flr=mt.floor,ar=Array,p=(x,y)=>y*wd+x,q=p=>[p%wd,flr(p/wd)],fz='font-size:',mp=ar.from(ar(ht),()=>[...ar(wd).fill('%c '),'%c\n']).flat().join(''),sf=()=>mt.random()*ht*wd|0,d=document,cs=`padding:10px 0 0 20px;line-height:10px;${fz}0px;background:`,d.onkeydown=event=>{mv=[[-1,0],[0,-1],[1,0],[0,1]][event.keyCode-37]},fd=sf(),bt.disabled=1,m=m.map(e=>p(...e)),tm=setInterval(()=>{pr=q(m[0]),t=[pr[0]+mv[0],pr[1]+mv[1]],pt=p(...t);if(t[0]<0||t[0]>=wd||t[1]<0||t[1]>=ht||m.includes(pt)){clearInterval(tm);bt.disabled=0;c.log('%cGameOverScore:%d',fz+'20px',m.length-3);return}m.pop();m.unshift(pt);if(pt===fd){m.push(pt);fd=sf()}fr++%300===0&&c.clear();c.log(mp,...ar.from(ar(ht*wd),(l,i)=>i===fd?'red':m.includes(i)?'yellow':'black').flatMap((l,i)=>(i+1)%wd?cs+l:[cs+l,'']))},1000/3)})(20,16,[0,-1],[[10,8],[10,9],[10,10]],this,0)"
```

`onclick`の中は現在、`869`文字！

@htsign さん、追加ありがとうございます～！

# HTML 全体

HTMLは全体として以下のようになっております。

```html
<html>
  <header></header>
  <body>
    F12
    <button
      id="start"
      onclick="((wd,ht,mv,m,bt,fr)=>{c=console,mt=Math,flr=mt.floor,ar=Array,p=(x,y)=>y*wd+x,q=p=>[p%wd,flr(p/wd)],fz='font-size:',mp=ar.from(ar(ht),()=>[...ar(wd).fill('%c '),'%c\n']).flat().join(''),sf=()=>mt.random()*ht*wd|0,d=document,cs=`padding:10px 0 0 20px;line-height:10px;${fz}0px;background:`,d.onkeydown=event=>{mv=[[-1,0],[0,-1],[1,0],[0,1]][event.keyCode-37]},fd=sf(),bt.disabled=1,m=m.map(e=>p(...e)),tm=setInterval(()=>{pr=q(m[0]),t=[pr[0]+mv[0],pr[1]+mv[1]],pt=p(...t);if(t[0]<0||t[0]>=wd||t[1]<0||t[1]>=ht||m.includes(pt)){clearInterval(tm);bt.disabled=0;c.log('%cGameOverScore:%d',fz+'20px',m.length-3);return}m.pop();m.unshift(pt);if(pt===fd){m.push(pt);fd=sf()}fr++%300===0&&c.clear();c.log(mp,...ar.from(ar(ht*wd),(l,i)=>i===fd?'red':m.includes(i)?'yellow':'black').flatMap((l,i)=>(i+1)%wd?cs+l:[cs+l,'']))},1000/3)})(20,16,[0,-1],[[10,8],[10,9],[10,10]],this,0)"
    >
      start</button
    ><br />
    <br />
    操作方法<br />
    ・ArrowKey: 上下左右の方向転換<br />
    注：この画面にフォーカスが当たっていないと操作不可！<br />
    <script>
      console.log('%cPress Click Start Button', 'font-size:20px;');
    </script>
  </body>
</html>
```
初期表示のメッセージはこのままで勘弁してください。。。

~~１ｋ切ったということでちょっと満足してしまいました。まだ座標とCSSの吐きだしロジックで改善点あると思いますが今夜はこれくらいにしておこうと思いますｗ~~
~~(これ絶対、超絶ロジックがベーマガに載ってたと思う)~~