【煩悩】大晦日1日でゲーム作って年内に滑り込みチャレンジ2024【後編】

# 初めに
大晦日の1日だけでゲームを作ってQiitaに記事を投稿できるかチャレンジ。

大晦日といえば、108つのアレですよね。

そうです。煩悩 です。

ということで(？)煩悩をぶっ壊す便利ツール「煩悩デストラクター」を作りました！！！

こちらから遊ぶことができます。遊んでみてね。
http://delphy8.s324.xrea.com/index.html

@Kumanoku と @Delphyilia の合作になります！

この記事では、ゲーム部分・全体構成・レンタルサーバへのデプロイについて紹介します！

ログイン機能についての詳しい解説は @Kumanoku の【前編】をこちらからどうぞ↓
https://qiita.com/Kumanoku/items/70670e4d96f09707e81c

（ゲーム自体は年内に完成したのですが、記事の執筆が間に合いませんでした…）

:::note info
この記事に記載しているコードは、実際にゲームに使用したコードとは異なります。
紹介用に書き換えたり省略している部分があります。
:::


# ゲーム部分の実装（Delphyilia担当）
## 方針
煩悩を消すゲームを作ろう！という原案のもと、以下の仕様を考えました。

+ イメージは「もぐらたたき」
  + 「煩悩」というボードを持ったキャラクターが出現する
  + そのキャラを叩く
  + キャラクタはいい感じに動き回る
+ クリア条件について
  + キャラクタをクリックすると得点
  + 小さいキャラはクリックしづらいので高得点
  + （煩悩なので）108点を超えたらクリア

## 実装
JavaScriptで画像を複製し、動き回らせます。
その前に、画像がどのような動きをするか事前に決めておくため、専用のEnumを設定します。
画像を複製し動き回らせる関数の引数に用います。

```javascript:game.js
// 画像をどの動きで生成するか指定する
// 得点もここで決める
class modeEnum {
    static random_bigsize = 1;
    static random = 2;
}
```
ランダムに動かし、かつ、画像が大きい場合は1点を獲得、
ランダムに動かす場合は2点、というふうに、このEnumで画像の動かし方に対応した得点を決めます。

では、画像を動かす関数を次に示します。
```javascript
function createImage(id, mode, add_flag) {
    const img = document.createElement('img');
    img.src = 'pass/to/image'; // 画像のパスを指定
    img.style.position = 'absolute';
    if (mode == modeEnum.random_bigsize){
        img.style.width = '200px'; // 画像の幅
        img.style.height = '200px'; // 画像の高さ
    }else {
        img.style.width = '150px'; // 画像の幅
        img.style.height = '150px'; // 画像の高さ
    }
    img.style.cursor = 'pointer'; // 手のアイコンに変更
    // ランダムな動きを設定
    function moveRandomly() {
        const newX = Math.random() * (container.clientWidth - 50);
        const newY = Math.random() * (container.clientHeight - 50);
        img.style.transition = 'left 1s linear, top 1s linear';
        img.style.left = `${newX}px`;
        img.style.top = `${newY}px`;
    }
    
    setInterval(moveRandomly, 1000);

    // クリックイベントリスナーを追加
    img.addEventListener('click', (e) => {
        // 得点を増やす
        desire += mode;  // クリックするたびに得点を増やす
        document.getElementById('score').textContent = `打ち砕いた煩悩の数：${desire}個`;

        // 画像を削除
        e.target.remove();

        // ゲームクリアの判定
        if (desire >= max_desire) {
            const endTime = Date.now(); // ゲーム終了時刻
            const timeTaken = (endTime - startTime) / 1000;  // 経過時間（秒）
            
            // ゲームクリアメッセージ
            alert('ゲームクリア！');

            // 結果を送信するためのURLを生成
            const resultUrl = `/cgi-bin/result.cgi?time=${timeTaken}`;

            // 別のCGIに遷移
            window.location.href = resultUrl;
        }
        // 画像が削除されたので新たに画像を追加
        if (add_flag == true){
            createImage(image_id++, modeEnum.random,true);
        }
    });
    container.appendChild(img);
}
```
画像を複製し、クリックしたら画像は消えて得点が加算されます。

関数の引数は `id, mode, add_flag` の3つです。
`id` は複製した画像すべてに連番で付与します。
`mode` は、前述した `modeEnum` を指定します。
`add_flag` ですが、これを `true` と指定すると、画像がクリックされ消えたときに、再びこの関数を呼び出して画像を複製します。
煩悩を消しても新しく煩悩がわいてくるわけです。

ランダムな動きを実現しているのはこの部分です。

```javascript
function moveRandomly() {
        const newX = Math.random() * (container.clientWidth - 50);
        const newY = Math.random() * (container.clientHeight - 50);
        img.style.transition = 'left 1s linear, top 1s linear';
        img.style.left = `${newX}px`;
        img.style.top = `${newY}px`;
    }
    setInterval(moveRandomly, 1000);
```
`setInterval()`で、1000ミリ秒...つまり1秒ごとに `moveRandomly()` を呼び出します。

`Math.random() * (container.clientWidth - 50);`
`Math.random() * (container.clientHeight - 50);`
で画像の位置を再設定します。

つまり、1秒ごとに画像が移動するのを繰り返すことで、ランダムな動きを実現しています。

こんな感じの挙動になりました。

![Videotogif (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/c653790e-7385-f6b3-499e-cd544cac62c6.gif)

ガビガビでわかりにくいですが、 **煩悩** と書かれた札を持ったキャラクタが、わちゃわちゃ動いている様子です。



# プロジェクト全体の構成（Delphyilia担当）
## 方針
[前回の記事](https://qiita.com/Kumanoku/items/70670e4d96f09707e81c)にて @Kumanoku 君が実装してくれたログイン機能と、前述のゲームを組み合わせることで、ランキングを作ります。
ランキングはゲームをするうえでの重要なモチベーションになります。今回は、煩悩を108個壊すまでのタイムを競うことにします。
そこで、次のような方針で作成します。

+ 追加が必要な機能
  + ランキングを表示するページ
  + ログイン情報の保持
  + ゲームのリザルトを保存


+ 追加が必要なファイル
  + ランキングを表示するページ用HTMLファイル
  + ログイン情報をcookieに保存する処理をするCGIファイル
  + ゲームのリザルトを保存するファイル

このような方針で、不足しているファイルを書き足し、ゲームを完成させてゆきます。
各ページの遷移は次のように行います。

![遷移図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/12755ae1-4e29-604d-adb5-1b137193f09d.png)

特に躓いたところが **ログイン情報の保持** でした。この部分を解説します。


## 実装 ログイン情報の保持
@Kumanoku 君が用意したログインページ用のコード `login.cgi` を書き替えました。

```diff_python:login.cgi
#!/usr/bin/python3.6

import cgi 
import hashlib 
import csv 
import os

DATA_FILE = "users.csv" 

# ハッシュ化関数
def hash_password(password): 
    return hashlib.sha256(password.encode()).hexdigest() 

# ユーザ認証関数
def authenticate_user(username, password): 
    hashed_password = hash_password(password) 
    if not os.path.exists(DATA_FILE):
        return False 
    with open(DATA_FILE, "r") as f: 
        reader = csv.reader(f) 
        for row in reader: 
            if row[0] == username and row[1] == hashed_password: 
                return True 
    return False 

# ヘッダーの設定
print("Content-Type: text/html; charset=utf-8\n\n") 

form = cgi.FieldStorage() 
username = form.getvalue("username") 
password = form.getvalue("password") 

if username and password: 
    if authenticate_user(username, password): 
        message = "ログイン成功！" 
 
+       # クッキーにユーザー情報を保存
+       # ログイン成功時、クッキーにユーザー名を保存
+       cookie["username"] = username
+       cookie["username"]["path"] = "/"  # クッキーを全サイトで有効にする
+       cookie["username"]["expires"] = "Thu, 31-Dec-2025 23:59:59 GMT"  # クッキーの有効期限
+       # データを次のCGIに送信
+       print()
+       print(f"""
+       <html lang="ja">
+       <head><title>ログイン</title></head>
+       <body>
+       <h1>ログイン</h1>
+       <form method="post" action="set_cookie.cgi">
+           <input type="hidden" name="username" value="{username}">
+           <input type="submit" value="ゲーム画面開始はこちらから！">
+       </form>
+       <p>{message}</p>
+       </body>
+       </html>
+       """)
    else: 
        message = "ユーザ名またはパスワードが間違っています。" 
else: 
    message = "ユーザ名とパスワードを入力してください" 

print(f"""
<html lang="ja">
<head><title>ログイン</title></head>
<body>
<h1>ログイン</h1>
<form method="post" action="login.cgi">
    <label for="username">ユーザー名: </label><input type="text" id="username" name="username" required><br>
    <label for="password">パスワード: </label><input type="password" id="password" name="password" required><br>
    <input type="submit" value="ログイン">
</form>
<p>{message}</p>
<p><a href="register.cgi">新規登録はこちら</a></p>
</body>
</html>
""")
```
「ログインに成功した」という情報を他のページでも共有するために、`cookie`を用います。
`cookie`への情報の書き込みは、このログインページとは別の`set_cookie.cgi`というページに送信し、そこで行います。

### なぜ別のページなのか
今回の場合、ログインプログラムは次のような挙動をします。

![login画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/50af492b-7a88-7275-226b-7be31e6035b3.png)

ページが完全に読み込まれた後は、リロードなしでログイン検証まで行っています。

`cookie`への情報の書き込みは、ページが完全に読み込まれる前（コーディング的に言うとCGIプログラムの中に書いた`<html>`タグよりも前）に行う必要があるようです。（正確に言うと違いますが）
しかし、このログインページはすでにページを読み込んでしまっています。この状態で`cookie`に何かを書き込もうとすると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/71cbd5b2-0990-abe5-4b97-67f8d7b11e88.png)

画面上部に`cookie`に書き込みたかった情報が出てしまいます！失敗です。
なので、暫定的解決法として、ログインページとは別のページで`cookie`への書き込みを行います。

![login周り.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/bd6e453a-8e00-8066-b523-1420ef911da3.png)


`set_cookie.cgi`では`login.cgi`から送信された情報を`cookie`に書き込み、即座に次のページへ切り替えます。

```python:set_cookie.cgi
#!/usr/bin/env python3
import cgi
import http.cookies
import os

# フォームデータの取得
form = cgi.FieldStorage()
username = form.getvalue("username")

# クッキーにユーザー名を保存
cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE"))
cookie["username"] = username
cookie["username"]["path"] = "/"  # クッキーを全サイトで有効にする

# クッキーを出力
print(cookie.output())
print("Content-Type: text/html; charset=utf-8\n\n")
print()

# HTML出力（JavaScriptでページ遷移）
print(f"""
<html lang="ja">
<head><title>ログイン結果</title>
<script>
    // ページが読み込まれた後にゲームページに遷移
    window.onload = function() {{
        window.location.href = "game_cookie.cgi";  // ゲームページのURL
    }};
</script>
</head>
<body>
<p>ログインデータ書き込み中です...</p>
</body>
</html>
""")
```

`username = form.getvalue("username")`でユーザーネームを取得し、整形したのちに`print(cookie.output())`で`cookie`に出力します。


ここまでで、ようやくログイン情報を全ページで共有する準備が整いました。
ログインしたユーザーネーム...つまり、**誰が遊んでいるか** という情報を、ゲーム内のページ全てで知ることができます。
なので、ユーザーネームに紐づけてスコアを記録することも可能になりました。

量が多くなってしまうので、他のプログラムについては省略させていただきます！

# 開発時のテスト環境について（Delphyilia担当）
## この項目について
@Delphyilia が`CGI`を用いてプログラムを作成するのは今回が初でした。テスト環境を整えるのにも躓いた点がありますので、備忘録として簡単に紹介します。

## 環境
`WSL`を用いています。
エディタは`vscode`です。

## 躓き1 改行コード
WindowsとLinuxでは標準の改行コードが異なります。
`vs code`の拡張機能である`code-eol`を入れると可視化できます。
上がindows、下がLinuxの改行コードです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/ba2a4b41-9438-d544-c5df-016c06949405.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/e9aa30a2-48cf-c921-e635-eb9228bcd01c.png)

`↵`と`↓`です。CGIプログラムを開発用サーバで起動するとき、Windowsの改行コード`↵`が残っているとうまくいかないようです。
全ての改行コードをLinuxの方`↓`に直します。

まず、`dos2unix`というコマンドをインストールします。
```bash
sudo apt-get install dos2unix
```
次に、CGIプログラムを修正します。`login.cgi`というファイルの改行コードを修正したい場合、次のように打ちます。

```bash
dos2unix login.cgi
```
これで改行コードをLinuxの方に揃えることができます！

## 躓き2 実行権限
ファイルに実行権限を付ける必要があります。`login.cgi`に実行権限を付ける場合、次のようにコマンドを打ちます。

```bash
chmod +x login.cgi
```

## 躓き3 サーバの起動方法
CGIを使用可能な状態で起動する必要があります。次のコマンドを打ちます。

```bash
python3 -m http.server --cgi 8000
```
打つと、テスト用のサーバが起動します。`Google Chrome`（なんでもいいですが）を開き、URLに`http://localhost:8000/`と入力すると、自身の作成したページを見ることができます。
CGIプログラムも動作します。

## 躓き4 プロセスキル
テスト用のサーバが動きっぱなしになり、不具合が生じた場面がありました。
そんなときは、次のコマンドを打ちましょう。

```bash
lsof -i :8000
```
8000番のポートで使用しているプロセスの一覧が出ます。例えば、こんな感じです。
```text
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3  12345  user   3u  IPv4  56789      0t0  TCP *:8000 (LISTEN)
```
実行を終わらせたはずなのにプロセスが余計に動いている場合、`PID`に書いてある番号を用いて、次のコマンドを打ちましょう。
```bash
kill 12345
```
こうすることで、余計なプロセスを終了させることができます。再びテストしたい場合は、また`python3 -m http.server --cgi 8000`を打てばよいです。


# デプロイについて（KumanokuとDelphyilia両人とも担当）
## サーバの選定
今回のプロジェクトを置いておくサーバについて、満たしてほしい条件は次の2つです。

+ 無料枠がある
+ pythonによるCGIが使える

これを満たす良い感じのサービスが、`xrea`でした。

https://www.xrea.com/

@Kumanoku 君が以前使用した経験があり、オススメしてくれました。
デプロイ先として良い感じです。

## デプロイ時に困ったこと
xreaでは、契約すると`.shop`というドメインが付いてきます。
次の画像は https://www.xrea.com/signup/ からの引用です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355589/870a9e3c-a87c-be2a-8dd6-16074cf5757d.png)

この`.shop`ドメインが厄介でした。`user.s324.xrea.com/`というようなドメインを使用したいのに、なぜか`user.shop`を無理やり使用させてくるような初期設定になっていました。
詳細についてはすでに先人の方が対処法と共に解説されている記事がありましたので、そちらの紹介のみとさせていただきます。

https://xcs.taken.jp/xrea-bad-initial-setting.html

https://xcs.taken.jp/xrea-wordpress.html

xreaを使用する予定のある方は、この2つの記事に目を通しておかれると良いかと思います。

# 今後の展望
なにぶん開発期間が **1日** でしたので、最低限の機能しかありません。コードも適当で良いとことはchat_gptに頼りました。よくない。

しかし、ランキングのついたゲームということで、最低限の面白さはあると思います！
せっかく作成し愛着も沸いたので、暇な時間ができたらぼちぼち改修したいと思っています。

+ ~~ランキングがソートされていない（ランキングとは）~~ 修正しました
+ ~~スコアが最善のもので上書きされない（想定外。バグ）~~ 修正しました
+ ~~複数回遊ぶと、その回数分だけ記録される（想定外。バグ）~~ 修正しました
+ ログイン周りでバグがある
+ 無駄な遷移ぺージがある
+ 見た目がシンプルすぎる
+ ~~ログアウトボタンがない~~ 追加しました
+ アカウント削除ボタンもない
+ vscodeから直接ファイルをアップロードできなくて準備に時間がかかる

ざっと思いつく改善点だけでもこんなにあります！
少しづつ直して行きましょ～

# 終わりに
繰り返しになりますが、今回のプロジェクトは @Kumanoku との合作になります。
CGIの記法及びレンタルサーバの選定の面で、 @Kumanoku 君にはかなり丸投げしてしまいましたが、良い感じのログインフォームを作ってくれたので利用しやすかったです。
また、どのようなゲーム性にするかという方針についても沢山相談しました。
Gitの練習にもなりました。
ありがとうございます。

バックエンド処理や本番環境へのデプロイ時には、知らない仕様や手順が多く、躓くポイントが多いです。
ですが今回の合作を通して、pythonで書くCGIには少し慣れたかなと思います。

最後になりますが、記事を最後まで読んでくださった皆様、ゲームで遊んでくださった皆様、本当にありがとうございました！