ReactでSlotMachineを作る(Step4: ワイルドカード、セブンカードの追加)

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

▪️実装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)

## 成果物
![step4_seven.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/d749d0a2-23d7-b3d2-19c6-de5f3feed96e.gif)

▪️追加仕様
・ワイルドカードを追加。ワイルドカードは何でも合わせられる
・セブンカードを追加。セブンカードが3つ揃った場合、JackPotに移動する。
・セブンカードが3つ揃った場合、JackPotに移動する。ワイルドカードでも代替可能だが、少なくとも1つはセブンカードを含む必要がある。
・JackPotに移動した場合、gif動画が流れる。

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

### ソースコード
```shell:ディレクトリ
~/develop/react/react_slot_machine$ tree -I node_modules 
.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   ├── robots.txt
│   └── shiba_motivate.gif
├── src
│   ├── App.tsx
│   ├── components
│   │   ├── Dialog.tsx
│   │   ├── Reel.tsx
│   │   ├── SlotMachine.test.tsx
│   │   ├── SlotMachine.tsx
│   │   ├── const.ts
│   │   ├── func.test.ts
│   │   ├── func.ts
│   │   └── type.ts
│   ├── index.tsx
│   ├── logo.svg
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock

4 directories, 23 files
```

```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";
import {BET_COINS, INITIAL_COINS, SYMBOLS} from "./const"; // 定数のインポート
import {SymbolTile} from "./type";
import {spinReels} from "./func";
import {JackpotDialog} from "./Dialog";

export const SlotMachine: React.FC = () => {
  const [coins, setCoins] = useState(INITIAL_COINS);
  const [reels, setReels] = useState<SymbolTile[]>(Array(3).fill(SYMBOLS[0])); // SymbolTile型の配列
  const [isJackpot, setIsJackpot] = useState(false); // ジャックポット状態の追加

  const handleSpin = () => {
    const {newReels, win, jackpot, points} = spinReels(SYMBOLS, BET_COINS);
    setReels(newReels);
    setIsJackpot(jackpot);
    setCoins((prev) => prev - BET_COINS + (win ? points : 0));
  };

  const isWin = new Set(reels.map((tile) => tile.id)).size === 1;

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

```ts:src/components/const.ts
import {SymbolTile} from "./type";

export const INITIAL_COINS = 100;
export const BET_COINS = 3;

export const CHERRY_ID = "cherry";
export const LEMON_ID = "lemon";
export const ORANGE_ID = "orange";
export const GRAPE_ID = "grape";
export const WATERMELON_ID = "watermelon";
export const WILD_CARD_ID = "wild_card";
export const SEVEN_CARD_ID = "seven";

export const CHERRY_SYMBOL = "🍒";
export const LEMON_SYMBOL = "🍋";
export const ORANGE_SYMBOL = "🍊";
export const GRAPE_SYMBOL = "🍇";
export const WATERMELON_SYMBOL = "🍉";
export const WILD_CARD_SYMBOL = "⚙️";
export const SEVEN_SYMBOL = "💰";

export const SYMBOLS: SymbolTile[] = [
  {
    id: CHERRY_ID,
    label: CHERRY_SYMBOL,
    point: 10,
  },
  //   {
  //     id: LEMON_ID,
  //     label: LEMON_SYMBOL,
  //     point: 10,
  //   },
  //   {
  //     id: ORANGE_ID,
  //     label: ORANGE_SYMBOL,
  //     point: 10,
  //   },
  //   {
  //     id: GRAPE_ID,
  //     label: GRAPE_SYMBOL,
  //     point: 20,
  //   },
  //   {
  //     id: WATERMELON_ID,
  //     label: WATERMELON_SYMBOL,
  //     point: 30,
  //   },
  {
    id: SEVEN_CARD_ID,
    label: SEVEN_SYMBOL,
    point: 30,
    isJackpot: true,
  },
  {
    id: WILD_CARD_ID,
    label: WILD_CARD_SYMBOL,
    point: 30,
    isWildCard: true,
  },
];

```

```ts:src/components/type.ts
import {
  CHERRY_ID,
  CHERRY_SYMBOL,
  GRAPE_ID,
  GRAPE_SYMBOL,
  LEMON_ID,
  LEMON_SYMBOL,
  ORANGE_ID,
  ORANGE_SYMBOL,
  SEVEN_CARD_ID,
  SEVEN_SYMBOL,
  WATERMELON_ID,
  WATERMELON_SYMBOL,
  WILD_CARD_ID,
  WILD_CARD_SYMBOL,
} from "./const";

export type SymbolTile = {
  id: SymbolId;
  label: SymbolLabel;
  point: number;
  isWildCard?: boolean;
  isJackpot?: boolean;
};

export type SymbolId =
  | typeof CHERRY_ID
  | typeof LEMON_ID
  | typeof ORANGE_ID
  | typeof GRAPE_ID
  | typeof WATERMELON_ID
  | typeof WILD_CARD_ID
  | typeof SEVEN_CARD_ID;

export type SymbolLabel =
  | typeof CHERRY_SYMBOL
  | typeof LEMON_SYMBOL
  | typeof ORANGE_SYMBOL
  | typeof GRAPE_SYMBOL
  | typeof WATERMELON_SYMBOL
  | typeof WILD_CARD_SYMBOL
  | typeof SEVEN_SYMBOL;

```

```tsx:src/components/Dialog.tsx
import React, {useState} from "react";
import Dialog from "@mui/material/Dialog";
import {Typography} from "@mui/material";

export const JackpotDialog: React.FC = () => {
  const [open, setOpen] = useState(true); // ダイアログの開閉状態

  // ダイアログを閉じる関数
  const handleClose = () => {
    setOpen(false);
  };

  return (
    <Dialog
      open={open}
      onClose={handleClose}
      maxWidth="md" // または 'lg', 'xl' など、必要に応じてサイズを調整
    >
      <img
        src="/shiba_motivate.gif"
        alt="Motivated Shiba Inu"
        style={{width: "200px"}}
      />
      <Typography>JackPot始まるよ！！！</Typography>
    </Dialog>
  );
};
```

```ts:src/components/func.ts
import {WILD_CARD_ID, SEVEN_CARD_ID} from "./const";
import {SymbolTile} from "./type";

export const spinReels = (
  symbols: SymbolTile[],
  betCoins: number
): {newReels: SymbolTile[]; win: boolean; jackpot: boolean; points: number} => {
  const newReels = Array.from(
    {length: 3},
    () => symbols[Math.floor(Math.random() * symbols.length)]
  );

  const hasWildCard = newReels.some((tile) => tile.id === WILD_CARD_ID);
  const isJackpot =
    newReels.every(
      (tile) => tile.id === SEVEN_CARD_ID || tile.id === WILD_CARD_ID
    ) && newReels.some((tile) => tile.id === SEVEN_CARD_ID);

  let win = false;
  let points = 0;

  if (isJackpot) {
    // ジャックポットの場合
    win = true;
    points = 100; // ジャックポット時のポイント
  } else if (
    new Set(
      newReels.map((tile) =>
        hasWildCard && tile.id !== WILD_CARD_ID ? "wild" : tile.id
      )
    ).size === 1
  ) {
    // ワイルドカードを含む勝利
    win = true;
    points = newReels[0].point;
  }

  return {newReels, win, jackpot: isJackpot, points};
};

```

いい感じでコードが汚くなってきました！！！
次回はディレクトリ整理とリファクタリングを実施していこうと思います。