ReactでSlotMachineを作る(Step2: Coin機能実装)

## はじめに
[前回](https://qiita.com/koji0705/items/be905a51d5b9674f4d46)までの記事の続きです。(シリーズ化予定)

▪️実装Step
[Step1:基本編](https://qiita.com/koji0705/items/be905a51d5b9674f4d46)
[Step2:Coin機能実装編](https://qiita.com/koji0705/items/7c4fe495252728e5457c) ←いまここ
[Step3: 仕様追加、リファクタリング実施](https://qiita.com/koji0705/items/bb1708c50978218607ec)
[Step4:ワイルドカード、セブンカードの追加](https://qiita.com/koji0705/items/037a0fd7ea93bf6c63f3)
[Step5:ディレクトリ整理/リファクタリング](https://qiita.com/koji0705/items/a8b12ea24b774129f784)


## 成果物
![slot_step2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/7a0ae8fb-9363-7bb2-3984-b89567ddb0c2.gif)

https://github.com/kouji0705/react_slot_machine/tree/feat/Step2_coin

追加仕様
・Coinは最初100枚。
・1回で3枚消費する。
・勝利すると10枚コインが追加される。

## プロダクトコード
```tsx:src/components/SlotMachine.tsx
import React, { useState } from "react";
import { Reel } from "./Reel";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";

const symbols = ["🍒", "🍋", "🍊", "🍇", "🍉"];

export const SlotMachine: React.FC = () => {
  const [coins, setCoins] = useState(100);
  const [reels, setReels] = useState(Array(3).fill(symbols[0]));

  const handleSpin = () => {
    const newReels = reels.map(
      () => symbols[Math.floor(Math.random() * symbols.length)]
    );
    setReels(newReels);
    setCoins((prev) => prev - 3);
    // 当たり判定
    if (new Set(newReels).size === 1) {
      setCoins(coins - 3 + 10); // 当たりならコインを追加
    }
  };

  const isWin = new Set(reels).size === 1;

  return (
    <Box sx={{ textAlign: "center", marginTop: 4 }}>
      <Box>Coinの枚数：{coins}枚</Box>
      <Box sx={{ display: "flex", justifyContent: "center", gap: 2 }}>
        {reels.map((symbol, index) => (
          <Reel key={index} symbol={symbol} />
        ))}
      </Box>
      <Button
        variant="contained"
        color="primary"
        style={{ marginTop: "16px" }}
        onClick={handleSpin}
        disabled={coins < 3}
      >
        スピン
      </Button>
      {isWin && <Box sx={{ marginTop: 2 }}>勝利！</Box>}
    </Box>
  );
};

```

## テストコード

```tsx:src/components/SlotMachine.test.tsx
import React from "react";
import { render, fireEvent, screen } from "@testing-library/react";
import { SlotMachine } from "./SlotMachine";

describe("SlotMachine コンポーネント", () => {
  test("初期レンダリングでスピンボタンが表示される", () => {
    render(<SlotMachine />);
    expect(screen.getByRole("button", { name: "スピン" })).toBeInTheDocument();
  });

  test("スピンボタンをクリックするとリールが回転する", () => {
    render(<SlotMachine />);
    const initialReels = screen
      .getAllByText("🍒")
      .map((reel) => reel.textContent);
    fireEvent.click(screen.getByRole("button", { name: "スピン" }));
    const updatedReels = screen
      .getAllByText(/🍒|🍋|🍊|🍇|🍉/)
      .map((reel) => reel.textContent);
    expect(updatedReels).not.toEqual(initialReels);
  });
});

describe("コインの消費", () => {
  test("スピンボタンをクリックするとコインが3枚消費される", () => {
    render(<SlotMachine />);
    fireEvent.click(screen.getByRole("button", { name: "スピン" }));
    expect(screen.getByText("Coinの枚数：97枚")).toBeInTheDocument();
  });
  test("コインが3枚以上あるときはスピンボタンをクリックできる", () => {
    render(<SlotMachine />);
    expect(screen.getByRole("button", { name: "スピン" })).toBeEnabled();
  });
  test("コインが0枚になるとスピンボタンは無効になる", () => {
    render(<SlotMachine />);
    const spinButton = screen.getByRole("button", {
      name: "スピン",
    }) as HTMLButtonElement;

    // コインを0枚にするまでボタンをクリックする
    while (!spinButton.disabled) {
      fireEvent.click(spinButton);
    }

    // ボタンが無効化されていることを検証
    expect(spinButton).toBeDisabled();
  });
});

```

```shell:実行結果
~/develop/react/react_slot_machine$ yarn test src/components/SlotMachine.test.tsx
 PASS  src/components/SlotMachine.test.tsx
  SlotMachine コンポーネント
    ✓ 初期レンダリングでスピンボタンが表示される (44 ms)
    ✓ スピンボタンをクリックするとリールが回転する (16 ms)
  コインの消費
    ✓ スピンボタンをクリックするとコインが3枚消費される (9 ms)
    ✓ コインが3枚以上あるときはスピンボタンをクリックできる (7 ms)
    ✓ コインが0枚になるとスピンボタンは無効になる (31 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        0.449 s, estimated 1 s
Ran all test suites matching /src\/components\/SlotMachine.test.tsx/i.
```

:::note warn
まだ大した仕様を入れていないのに、段々と複雑化してきましたね。
コンポーネントに責務を持たせた関係で、コンポーネントの可読性・保守性が低下。
また、テストも検証がしにくいケースが増えてきました。もうちょっとすると開発がしんどくなり、開発生産性が爆下がりしてきます。どこかでリファクタリングを検討する段階に入りそうです😊
:::