『こんにちはPython』のスカッシュゲーム（壁打ちテニス）を JavaScript で写経してみた！（後編、プログラミング玄人向け）

# 『こんにちはPython』のスカッシュゲーム（壁打ちテニス）を JavaScript で写経してみた！（後編、プログラミング玄人向け）

みなさま、こんにちは。[ハーツテクノロジー]の山崎です。（この記事は、業務とは直接無関係な記事ですが、業務で得られた知見が間接的に随所に織り込まれていると思われます。）

[ハーツテクノロジー]:https://www.hearts-tech.co.jp/

この記事は、[前編]と後編の２部構成になっていて、こちらは後編です。

[前編]には、小学生でもわかるように、「ゲームとして遊べるまで」を解説しています。
後編では、小学生に解説するには少々無理がある部分を、こちらにまとめるようにしました。ですので、小学生の方は[前編]だけ読んでいただき、後編は、学校を卒業してから読むようにすると理解がすすむ、、、かもしれません。

おとなの方は、後編からでもよいですが、[前編]を軽く流し読みしていただいてからのほうが、この後編の内容がより理解しやすい思います。よろしくお願いします。

[前編]:https://qiita.com/yamazaki3104/private/b6d567e76e81ea658ff5

# こんにちはPython

元ネタはこちら。
> ゲームセンターあらしと学ぶプログラミング入門
> まんが版『こんにちはPython』
> https://www.m-sugaya.jp/manga_python/

よい本です。なんと言っても、まんがであることが最高です。しかも、「[ゲームセンターあらし]」ですよ！（[炎のコマ]は出ないけど）
当時、小学生～中学生だったわたしは、勉強が嫌いで、よく友達とゲームセンターに行って遊んでいました。
もちろん「[ゲームセンターあらし]」も読んでました。で、ゲームがめっちゃ好きになって「プログラマになりたい！」って思ったのが最初の人生の分岐点でした。

プログラマになれば、ゲームが作れるようになります。これは事実です。ただ、１人でできる規模は限られちゃいますけど。

[ゲームセンターあらし]:https://ja.wikipedia.org/wiki/%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC%E3%81%82%E3%82%89%E3%81%97

[炎のコマ]:https://www.google.com/search?q=%E7%82%8E%E3%81%AE%E3%82%B3%E3%83%9E

さて、後編は、[前編]では書けなかったおとな向けの説明になります。

主に、
１、[オブジェクト指向]
２、スマホ対応
３、音対応
の３つについて解説していきます。


## １、オブジェクト指向

いきなりですが、[class] を導入します。いわゆる[オブジェクト指向]です。戦略でいう「[分割統治]」ってやつです。

[オブジェクト指向]:https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91

[分割統治]:https://ja.wikipedia.org/wiki/%E5%88%86%E5%89%B2%E7%B5%B1%E6%B2%BB

[前編]で作成したコードは、長くは無いコードではあるのですが、サクッと理解できる行数を超えてきたので、手を加える前に整理します。

プログラムコードは放っておくとすぐに肥大化します。肥大化したコードは散らかった部屋と一緒で、どこになにがあるのかさっぱりわからなくなります。難解なコードは、理解するための時間を奪います。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これがインフラエンジニアにとってのスパゲッティコードやで！ <a href="https://t.co/Skov2IuCgx">pic.twitter.com/Skov2IuCgx</a></p>&mdash; いちごろ (@panda66104256) <a href="https://twitter.com/panda66104256/status/1259630332523020288?ref_src=twsrc%5Etfw">May 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

[オブジェクト指向]とは、（Qiitaを俳諧しているおとなの人には耳タコだと思いますが、）わたしの解釈を短めに書くと「データ構造を整理して密接に関連するアルゴリズムを近くに置くことでコードを部品化し見通しよく整理すること」です。
複雑で難解なコードを[オブジェクト指向]の恩恵に預かって、読みやすく、理解しやすくしましょう。というわけです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ネットワークエンジニアの特殊技能集 <a href="https://t.co/1PoXpo2fph">pic.twitter.com/1PoXpo2fph</a></p>&mdash; 麹@営業のち情シス (@oryzae1824) <a href="https://twitter.com/oryzae1824/status/1315269865788194817?ref_src=twsrc%5Etfw">October 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

（この Twitter の画像は「イメージ」です。プログラミングとは無関係ですが「整理されたコードの価値」を画像で的確に表現できている、と思っています。）

プログラミングに慣れたひとは、[class] ＆ object にしたほうが見やすい（読みやすい）と思います。やはり、変数や定数は、グローバルなエリアには置かず、必要とされている（関係の深い）機能に近い場所に置いて、影響範囲や思考の範囲を狭めることで、思考がシンプルになり、理解しやすくなるからだど思っています。

具体的には、表示エリア、ボール、ラケットをそれぞれ [class] にして、作成した [class] から new を使って object を作成します。
ちなみに、[class] が本領を発揮するのは、複数の object を生成するようになってからです。このコードでは 1つの [class] に対して 1つの object しか生成していないので、構造体＋関数で書くのと大きな違いは無いと言える、、、かもしれません。

[class]:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes

あと、ついでに、メッセージをランダムに選んでいるところ↓

```JS
    const mes = Math.floor( Math.random() * 5 )
    let message = ''
    if ( mes == 0 )
        message = 'うまい！'
    if ( mes == 1 )
        message = 'グッド！'
    if ( mes == 2 )
        message = 'ナイス！'
    if ( mes == 3 )
        message = 'よしッ！'
    if ( mes == 4 )
        message = 'すてき！'

```

これは小学生向けの書き方なので、おとななプログラマの方は、この処理をサクッと関数にしましょう。

```JS

function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

const message = get_random_message( [
    'うまい！', 'グッド！', 'ナイス！', 'よしッ！', 'すてき！'
] ) 

```

はい。短く、シンプルになりました。

その昔、JavaScript には [class] は無く、 function を使って [class] っぽいコードを書いていましたが、それはもう昔の話です。今(2020年)は、JavaScript で [class] が使えます。

いったんまとめると以下のようなコードになります。長くなってきたので閉じています。ここ↓「全コード」をクリックすると見られます。

<details><summary>全コード</summary><div>

```HTML

<body></body><script>
// 「こんにちは Python」のスカッシュゲーム（壁打ちテニス）を JavaScript で写経（音無し版）
// オブジェクト指向対応版

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.cv = document.createElement( 'canvas' )
        this.ctx = this.cv.getContext( '2d' )

        this.cv_w = 640
        this.cv_h = 400
        this.cv.setAttribute( 'width',  this.cv_w )
        this.cv.setAttribute( 'height', this.cv_h )
        document.body.appendChild( this.cv )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    const message = get_random_message( [
                        'うまい！', 'グッド！', 'ナイス！', 'よしッ！', 'すてき！'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点：' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ！','ミスしたね！', 'あーあ、見てられないね！'
            ] )
            div.innerText = message + ' 得点：' + point

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// メッセージ表示エリア div の確保
const div = document.createElement( 'div' )
document.body.appendChild( div )
div.innerText = "スカッシュゲーム（音無し版）：マウスクリックでスタート！"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 15, -15 )
    div.innerText = "スカッシュゲーム：スタート！"
}

// ゲームの繰り返し処理
function game_loop()
{
    area.draw()     // 画面クリア
    ball.draw()     // ボールを描く
    racket.draw()   // ラケットを描く
    ball.move()     // ボールの移動
}

// ゲームのメイン処理開始
setInterval( game_loop, 50 ) // 20fps

</script></html>

```

</div></details>


ちなみに、現状は、こういう感じ👇で動いています。

### CodePen1

<p class="codepen" data-height="630" data-theme-id="light" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="xxOOKwg" style="height: 630px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="オブジェクト指向対応版（「こんにちは Python」のスカッシュゲーム）"></p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

コードを読んで、「is_gameover とかも class にまとめられるじゃん」と思った方もおられるでしょう。いい質問ですね。それもありだとは思います。ですが、ここは、さじ加減といいますか、やりすぎないところで止めるのが、いい塩梅だと思っています（個人の見解です）。

では、このコードを足がかりにして、スマホ対応と、音を出す対応をしていきます。


## ２、スマホ対応

スマホ対応では大きく２つの対応をします。

(1) 見た目を整える→[リサイズ]（[レスポンシブ]）に対応
(2) [タッチイベント]に対応

１つ目は、見た目を整えます。
現状は、[canvas] エリアが 640x400 と固定になっています。PCのブラウザであれば、この大きさで固定に表示しても大きな問題にはなりにくいです。
しかし、スマホのブラウザでは、ちょっと遊べません。スマホの画面サイズもいろいろありますし、画面を小さくしてもたのしくありません。
ここは、[リサイズ]（[レスポンシブ]）に対応し、表示しているブラウザのサイズに合わせて、[canvas] エリアの大きさを変更するコードを書きます。

もう１つは、[タッチイベント]の対応です。
意外に思われるかもしれませんが、ブラウザでは、マウスでクリックしたイベントと、画面にタッチしたイベントは別のイベントとしてあつかわれています。
ですので、スマホのブラウザでは、マウスを動かしたイベントが発生できず、ラケットを動かすことができません。
ここも、[タッチイベント]のコードを入れていきます。

（ちなみに、ここで指している「スマホ」とは、Android端末と、iPhone の大きく２種類がありますが、動作を確認しているのは、iPhone の Chrome ブラウザになります。Androidや、Chrome以外のブラウザでは確認していないですが、おそらく、問題なく動くと思っています。動かない環境などありましたら、コメントいただけますとたすかります。）

### (1) [リサイズ]（[レスポンシブ]）に対応

まずは、[リサイズ]（[レスポンシブ]）対応から始めます。

いきなりコードを変更するのではなく、試験的に以下のような、HTMLを用意して、希望する動作をするかを確認します。特に、[レスポンシブ]対応の場合、頭で考えているとおりの動きになるかどうか、「実際に動かしてみないとわからない」ということが多いのです。

html タグ、 body タグを縦方向に 100% とし（横方向は自動的に広がるので、特に指定しなくてもオッケー👍）、body タグには margin が効いているので、これを OFF にする意味で、 0 を指定します。

[レスポンシブ]としては、ヘッダ部分にある「メッセージ」は 30px、フッター部分にある「タップエリア」は 100px 固定の高さにして。中央の「キャンバス表示エリア」は[レスポンシブ]に（ブラウザの大きさにあわせて）最大の広さになるように `display: grid;` と `grid-template-rows: 30px 1fr 100px;` を指定しています。

[リサイズ]:https://developer.mozilla.org/ja/docs/Web/API/Window/onresize

[レスポンシブ]:https://ja.wikipedia.org/wiki/%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B7%E3%83%96%E3%82%A6%E3%82%A7%E3%83%96%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3

```HTML
<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
    <div style="background-color: white;"   >メッセージ</div>
    <div style="background-color: silver;"  >キャンバス表示エリア</div>
    <div style="background-color: skyblue;" >タップエリア</div>
</div>
</body></html>
```

### CodePen2

<p class="codepen" data-height="337" data-theme-id="light" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="gOMMYRQ" style="height: 337px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="２、レスポンシブ対応（その１）"></p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

まずは、HTML だけをブラウザで表示して、希望通り[レスポンシブ]に表示されるか、ブラウザの表示エリアを変えて確認します。短いコードで、いったん動作を確認してから、メインのコードに組み込んでいきます。

この HTML の中央 div タグ「キャンバス表示エリア」に [canvas] タグをハメ込むようにコードを書き換えます。
主に、`class CanvasArea`にまとめておいたコードに、[リサイズ]（[レスポンシブ]）の変更を加えます。

```JS:リサイズ対応前のコード
// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.cv_w = 640
        this.cv_h = 480
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'width',  this.cv_w )
        this.cv.setAttribute( 'height', this.cv_h )
        document.body.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

```

↓

```JS:リサイズの処理
window.onresize = ()=>{
    this.resize()
}
```

↓

```JS:リサイズ対応後のコード
// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}
```

コードを修正する範囲は、`class CanvasArea` に収まっています。オブジェクト指向すげー！

ってか、本当は、[canvas] タグも自動でサイズ調整が効けば、こんなコードを書かなくてもすむのですがねー（きっとなにかしらの理由があるのでしょう。）

[canvas]:https://developer.mozilla.org/ja/docs/Web/HTML/Element/canvas

### (2) [タッチイベント]の対応

次は、[タッチイベント]（タップ）に対応します。

ラケットを動かす `class Racket` 内にある、 [onmousemove] イベント処理と同じ場所に、↑の HTML で用意した、水色のフッター部分に相当する、`div#touch` タグに、[タッチイベント]を追加するだけです。

[タッチイベント]:https://developer.mozilla.org/ja/docs/Web/API/Touch_events
[onmousemove]:https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/onmousemove


```JS
    // タッチイベントの処理
    const div_touch = document.querySelector( 'div#touch' )
    div_touch.ontouchstart =
    div_touch.ontouchmove = ev => {
        this.x = ev.pageX - 50
        return false // イベントを伝搬しない（親に返すと、フリックやスクロールに反応していろいろ面倒なので）
    }
```

[ontouchstart] と [ontouchmove] に同じ関数を設定しているので、ちょっと変な書き方をしていますが、想定内（許容範囲内）ですよね。

[ontouchstart]:https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/ontouchstart
[ontouchmove]:https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/ontouchmove

[タッチイベント]の実装に関しては、手前味噌ですが、このあたり↓の記事も参考にしてください。

> JavaScript タッチイベントの取得（マルチ対応） サンプルコード
> https://qiita.com/yamazaki3104/items/1f550c589b13febade82

さて、スマホ対応の全コードはこのように↓なります。ここ↓「全コード」をクリックで開きます。

<details><summary>全コード</summary><div>

```HTML
<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
<div id="mess" ></div>
<div id="area" ></div>
<div id="touch" style="background-color: skyblue;" ></div>
</div>
</body><script>
// 「こんにちは Python」のスカッシュゲーム（壁打ちテニス）を JavaScript で写経（音無し版）
// レスポンシブ対応版

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    const message = get_random_message( [
                        'うまい！', 'グッド！', 'ナイス！', 'よしッ！', 'すてき！'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点：' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ！','ミスしたね！', 'あーあ、見てられないね！'
            ] )
            div.innerText = message + ' 得点：' + point

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }

        // タッチイベントの処理
        const div_touch = document.querySelector( 'div#touch' )
        div_touch.ontouchstart =
        div_touch.ontouchmove = ev => {
            this.x = ev.pageX - 50
            return false // イベントを伝搬しない（親に返すと、フリックやスクロールに反応していろいろ面倒なので）
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// メッセージ表示エリア div の確保
const div = document.querySelector( 'div#mess' )
div.innerText = "スカッシュゲーム（音無し版）：マウスクリックでスタート！"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 15, -15 )
    div.innerText = "スカッシュゲーム：スタート！"
}

// ゲームの繰り返し処理
function game_loop()
{
    area.draw()     // 画面クリア
    ball.draw()     // ボールを描く
    racket.draw()   // ラケットを描く
    ball.move()     // ボールの移動
}

// ゲームのメイン処理開始
setInterval( game_loop, 50 ) // 20fps

</script></html>
```

</div></details>

これでブラウザの大きさを変えても、[canvas] のエリアが追従するはず、こんな感じ↓。
スマホのブラウザで開いたときに、いい感じの配置になる予定です。

### CodePen3

<p class="codepen" data-height="600" data-theme-id="light" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="BazzBvX" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="３、レスポンシブ対応版（その２）"></p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

スマホで確認してみたいかたは、こちら↓の QR コードから開いてみてください。

![qr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107592/87c061c3-90bc-ca63-7ff7-83fd479ba450.png)

動いてますか？

## ３、音を出す

今回の写経で一番苦労したのが、ここです。ブラウザで音を出すって、意外に難しかったです。

難しい理由は、大きく２つあって、１つ目は
「ブラウザで音を出す」をググると「メディア要素（`<audio>`タグ）で mp3 を鳴らす」
という記事ばかりで、ブラウザではmp3 以外に音を鳴らすことができないのではないか？と錯覚してしまうほどです。

しばらく探すと、[Web Audio API](AudioContext) というAPI が用意されていることがわかります。ただ、まだ新しい、実験中の機能のようで、使っている人も少なく、すなわち参考文献がとても少ないです。

[Web Audio API]:https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API

さて２つ目の問題は、ブラウザ特有の文化的背景の制限があります。
理解できるものの、なんとなくモヤッとする理由（メディア要素（`<video>`, `<audio>`）の自動再生におけるポリシーの変更）から、ブラウザを開いた瞬間に音は出せないようになっています。詳しくは「ユーザーのクリックアクションがキーになって音を出し始めないといけない」と決めたようです。
このヘンテコな仕様からプログラマは泣かされます。コードの書き方によって音が出たり出なかったり、ブラウザによって、動きが異なったり。どう書くのが正解なの？？！！と。

で、結局は、試行錯誤をして、動いているコードが正義！とばかりに、「なぜかよくわからないけど、こう書くと動く」というコードが氾濫しています。混乱に混乱が重なります。

これらの問題を乗り越えて、ようやく[createOscillator()]というAPI があることにたどり着けます。
このAPIを呼べば、mp3 という、基本的に巨大なファイルを用意しなくても、ピコピコ音が出せるのです。とは言え、まだドラフト（草案）レベルなので、たまたま鳴っているだけなのですがね。

[createOscillator()]:https://developer.mozilla.org/ja/docs/Web/API/AudioContext/createOscillator



```HTML
<body>
音がでます。でるはずです。たぶん。<br />
<button onclick="sine.beep()"> sine!</button>
<button onclick="square.beep()">square!</button>
<button onclick="sawtooth.beep()">sawtooth!</button>
<button onclick="triangle.beep()">triangle!</button>
</body><script>
// 音源の確保
class Beep
{
    constructor( _frq=2000, _typ='square' )
    {
        this.acx       = null   // 最初の一回だけフラグ
        this.type      = _typ
        this.beep_frq  = _frq   // 音程（周波数）
        this.beep_time = 0      // 長さ
    }

    beep( _t=5 )
    {
        this.beep_time = _t
        if ( this.acx != null ) return // 最初の一回だけ

        this.acx = new (window.AudioContext || window.webkitAudioContext)()
        this.osc = this.acx.createOscillator()
        this.vol = this.acx.createGain()
        this.osc.connect(this.vol).connect(this.acx.destination)
        this.vol.gain.value = 0     // 音量
        this.osc.type = this.type   // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type
        this.osc.frequency.value = this.beep_frq // 音程（周波数）
        this.osc.start()

        // 音の処理
        setInterval( ()=>
        {
            if ( this.beep_time <= 0 ) {
                this.vol.gain.value = 0 // 音量 OFF
                return
            }
            this.vol.gain.value = 0.2  // 音量
            this.beep_time -= 1        // 長さ
        }, 10 )
    }
}

const sine     = new Beep( 2000, 'sine' )
const square   = new Beep( 2000, 'square' )
const sawtooth = new Beep( 2000, 'sawtooth' )
const triangle = new Beep( 2000, 'triangle' )

</script>
```

### CodePen4

<p class="codepen" data-height="200" data-theme-id="light" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="MWeeagp" style="height: 200px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Beep">
</p>


今回の写経で、いちばん苦労したのがこの音の実装でした。無事に音が鳴って、よかったです。


### JavaScript全コード

というわけで、壁やラケット、そして床に落ちてゲームオーバーになった箇所に音を出すコードを埋めていきます。

全コードは以下です。これで最後なので、閉じずに載せます。

```HTML
<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
<div id="mess" ></div>
<div id="area" ></div>
<div id="touch" style="background-color: skyblue;" ></div>
</div>
</body><script>
// 「こんにちは Python」のスカッシュゲーム（壁打ちテニス）を JavaScript で写経（音あり版）

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
            pi2000.beep() // ♪
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    pi2000.beep() // ♪
                    const message = get_random_message( [
                        'うまい！', 'グッド！', 'ナイス！', 'よしッ！', 'すてき！'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点：' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ！','ミスしたね！', 'あーあ、見てられないね！'
            ] )
            div.innerText = message + ' 得点：' + point
            boo200.beep( 80 ) // ♪

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }

        // タッチイベントの処理
        const div_touch = document.querySelector( 'div#touch' )
        div_touch.ontouchstart =
        div_touch.ontouchmove = ev => {
            this.x = ev.pageX - 50
            return false // イベントを伝搬しない（親に返すと、フリックやスクロールに反応していろいろ面倒なので）
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// 音源の確保
class Beep
{
    constructor( _frq=2000, _typ='square' )
    {
        this.acx       = null   // 最初の一回だけフラグ
        this.type      = _typ
        this.beep_frq  = _frq   // 音程（周波数）
        this.beep_time = 0      // 長さ
    }

    beep( _t=5 )
    {
        this.beep_time = _t
        if ( this.acx != null ) return // 最初の一回だけ

        this.acx = new (window.AudioContext || window.webkitAudioContext)()
        this.osc = this.acx.createOscillator()
        this.vol = this.acx.createGain()
        this.osc.connect(this.vol).connect(this.acx.destination)
        this.vol.gain.value = 0     // 音量
        this.osc.type = this.type   // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type
        this.osc.frequency.value = this.beep_frq // 音程（周波数）
        this.osc.start()

        // 音の処理
        setInterval( ()=>
        {
            if ( this.beep_time <= 0 ) {
                this.vol.gain.value = 0 // 音量 OFF
                return
            }
            this.vol.gain.value = 0.2  // 音量
            this.beep_time -= 1        // 長さ
        }, 10 )
    }
}

// メッセージ表示エリア div の確保
const div = document.querySelector( 'div#mess' )
div.innerText = "スカッシュゲーム（音あり版）：マウスクリックでスタート！"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()
const pi2000 = new Beep( 2000 )
const po1320 = new Beep( 1320 )
const boo200 = new Beep(  200 )

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 15, -15 )
    div.innerText = "スカッシュゲーム：スタート！"

    // 音の初期化（最初のクリック時に鳴らす必要がある）
    pi2000.beep( 0 )    // 長さ 0 なので音は出さない
    po1320.beep( 0 )    // 長さ 0 なので音は出さない
    boo200.beep( 0 )    // 長さ 0 なので音は出さない
}

// ゲームの繰り返し処理
function game_loop()
{
    area.draw()     // 画面クリア
    ball.draw()     // ボールを描く
    racket.draw()   // ラケットを描く
    ball.move()     // ボールの移動
}

// ゲームのメイン処理開始
setInterval( game_loop, 50 ) // 20fps

</script></html>
```

### CodePen5

最終的には、このようになります。

<p class="codepen" data-height="628" data-theme-id="light" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="mdPpWzN" style="height: 628px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="こんにちは Python のスカッシュゲーム（壁打ちテニス）を JavaScript で写経（その２）">
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js">

さて、動いたでしょうか。例によって、スマホで確認してみたいかたのために、QRコードを置いておきます。

![qr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107592/676a58ee-a7b4-5cdf-e46e-3b965b3372ab.png)


# おまけ [リフレッシュレート]を超える 20fps -> 150fps

このゲームは、もとの Python のコードに合わせて 20fps で動いています。20fps とは1秒間に20回、描きなおしているということです。
20fps の速度で更新できれば、おおむねアニメーションしているように見えると言われています。
ただ、最近の PC やブラウザの描画能力はスゴイことになっていて、このゲームのような簡単な描画処理であれば、ディスプレイの表示能力（[リフレッシュレート]）を超える速度で描画できると思われます。

ディスプレイの[リフレッシュレート]とは、1秒間に何回表示を更新できるか、というディスプレイの処理性能を示しています。最近のディスプレイはおおむね 60Hz か、それを超えるもの増えています。60Hz というのは、すなわち ゲームプログラムが 60fps で描画してもその変化を漏らさず表示されるということになります（その変化を人がすべてを感じ取れるかどうかは別の話ですが、）。
しかしながら、ゲームプログラムが 120fps で描画して表示を更新できたとしても、ディスプレイが 60Hz では、その更新の半分しかディスプレイに反映されない＝見えていないことになります。ですので、表示の更新は、ディスプレイの[リフレッシュレート]に合わせるのが賢いコードになります。

[リフレッシュレート]:https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%95%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A5%E3%83%AC%E3%83%BC%E3%83%88

ブラウザには、[requestAnimationFrame] というAPIが用意されていて、このAPIは「ディスプレイのリフレッシュのタイミングで関数を呼んでくれる」という大変便利な API なのです。この API を使って描画する関数を登録し、呼ばれたときに表示を更新すると、ちょうどよい動きになる仕組みになっています。

というわけで、「ゲームの繰り返し処理」に [requestAnimationFrame] を組み込んだコードがこちらです。

[requestAnimationFrame]:https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame

```JS
// ゲームの繰り返し処理
let request_animation_frame = null
function game_loop()
{
    ball.move()     // ボールの移動

    if ( request_animation_frame != null ) return

    request_animation_frame = window.requestAnimationFrame( ev =>
    {
        area.draw()     // 画面クリア
        ball.draw()     // ボールを描く
        racket.draw()   // ラケットを描く

        request_animation_frame = null // クリア（次の描画イベントの登録を受け付ける）
    } )
}
```

ループ内で、移動処理をしたあとに描画イベントの登録をします。
ただ、描画処理が終わるくらいループ速度がゆっくりであれば問題ないのですが、描画処理よりもループ速度が早い場合は、イベントを登録しないように null を確認してスキップしています。こうすることで、ディスプレイのリフレッシュレートを気にすること無く、ループ速度を上げることができます。

ということで、最近、手に入るようになってきたゲーミング用ディスプレイのリフレッシュレート 144Hz を超えるフレームレート（ここでは 150fps ）に改造して、終わりたいと思います。

<details><summary>全コード</summary><div>

```HTML
<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
<div id="mess" ></div>
<div id="area" ></div>
<div id="touch" style="background-color: skyblue;" ></div>
</div>
</body><script>
// 「こんにちは Python」のスカッシュゲーム（壁打ちテニス）を JavaScript で写経（音あり版）

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
            pi2000.beep() // ♪
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    pi2000.beep() // ♪
                    const message = get_random_message( [
                        'うまい！', 'グッド！', 'ナイス！', 'よしッ！', 'すてき！'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点：' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ！','ミスしたね！', 'あーあ、見てられないね！'
            ] )
            div.innerText = message + ' 得点：' + point
            boo200.beep( 80 ) // ♪

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }

        // タッチイベントの処理
        const div_touch = document.querySelector( 'div#touch' )
        div_touch.ontouchstart =
        div_touch.ontouchmove = ev => {
            this.x = ev.pageX - 50
            return false // イベントを伝搬しない（親に返すと、フリックやスクロールに反応していろいろ面倒なので）
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// 音源の確保
class Beep
{
    constructor( _frq=2000, _typ='square' )
    {
        this.acx       = null   // 最初の一回だけフラグ
        this.type      = _typ
        this.beep_frq  = _frq   // 音程（周波数）
        this.beep_time = 0      // 長さ
    }

    beep( _t=5 )
    {
        this.beep_time = _t
        if ( this.acx != null ) return // 最初の一回だけ

        this.acx = new (window.AudioContext || window.webkitAudioContext)()
        this.osc = this.acx.createOscillator()
        this.vol = this.acx.createGain()
        this.osc.connect(this.vol).connect(this.acx.destination)
        this.vol.gain.value = 0     // 音量
        this.osc.type = this.type   // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type
        this.osc.frequency.value = this.beep_frq // 音程（周波数）
        this.osc.start()

        // 音の処理
        setInterval( ()=>
        {
            if ( this.beep_time <= 0 ) {
                this.vol.gain.value = 0 // 音量 OFF
                return
            }
            this.vol.gain.value = 0.2  // 音量
            this.beep_time -= 1        // 長さ
        }, 10 )
    }
}

// メッセージ表示エリア div の確保
const div = document.querySelector( 'div#mess' )
div.innerText = "スカッシュゲーム（音あり版）：マウスクリックでスタート！"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()
const pi2000 = new Beep( 2000 )
const po1320 = new Beep( 1320 )
const boo200 = new Beep(  200 )

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 2, -2 ) // 20fps -> 150fps 対応 15px -> 2px
    div.innerText = "スカッシュゲーム：スタート！"

    // 音の初期化（最初のクリック時に鳴らす必要がある）
    pi2000.beep( 0 )    // 長さ 0 なので音は出さない
    po1320.beep( 0 )    // 長さ 0 なので音は出さない
    boo200.beep( 0 )    // 長さ 0 なので音は出さない
}

// ゲームの繰り返し処理
let request_animation_frame = null
function game_loop()
{
    ball.move()     // ボールの移動

    if ( request_animation_frame != null ) return

    request_animation_frame = window.requestAnimationFrame( ev =>
    {
        area.draw()     // 画面クリア
        ball.draw()     // ボールを描く
        racket.draw()   // ラケットを描く

        request_animation_frame = null // クリア（次の描画イベントの登録を受け付ける）
    } )
}

// ゲームのメイン処理開始
setInterval( game_loop, 1000/150 ) // 150fps

</script></html>
```

</div></details>

<p class="codepen" data-height="656" data-theme-id="light" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="JjKRGao" style="height: 656px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="150fps 版"></p>

# まとめ

お疲れ様でしたー。

[前編]から書き始めて、どのくらいの月日がたったでしょうか、、、すでに、覚えていないです。

すこしでも、JavaScriptプログラミングの楽しさがお伝えできれば幸いです。

最後に、ゲームを作る楽しさを思い出させていただいた、[すがやみつる]先生に感謝とお礼をしつつ終わりたいと思います。どうもありがとうございました！

[すがやみつる]:https://twitter.com/msugaya

それでは、みなさまの快適なプログラミングライフを願いつつ終わります。

# 追記(2021-02-16)：相互リンク追加

同じような JavaScript プログラミングの記事なのに、View 数がめっちゃ偏っているので相互リンクしてみる。タイトルの問題なのかなぁ？？🤔

- [JavaScript を使った「電卓Webアプリ」の作り方を中３の息子に教えてみた！（プログラミング初心者向け）] (https://qiita.com/yamazaki3104/items/b5351f589fb6644bf910)


- [『こんにちはPython』のスカッシュゲーム（壁打ちテニス）を JavaScript に写経してみた！（前編、プログラミング初心者向け）](https://qiita.com/yamazaki3104/items/b6d567e76e81ea658ff5)

- [『こんにちはPython』のスカッシュゲーム（壁打ちテニス）を JavaScript で写経してみた！（後編、プログラミング玄人向け](https://qiita.com/yamazaki3104/items/ce89373fa862e4f3f439)　←この記事


よろしくお願いいたします。🙇‍♂️