Browse Source

Implement snake and pre-submit check

Kirk Trombley 3 years ago
parent
commit
3670934c13

+ 70 - 38
client/src/components/screens/GamePanel/GuessPane/GuessPane.jsx

@@ -1,9 +1,17 @@
 import { useCallback, useState } from "react";
-import { dispatch } from "../../../../domain/gameStore";
+import {
+  dispatch,
+  useCurrentRound,
+  useTargetPoint,
+} from "../../../../domain/gameStore";
 import ClickMarkerMap from "./ClickMarkerMap";
 import styles from "./GuessPane.module.css";
 import RoundTimer from "./RoundTimer";
+import GunGame from "./GunGame";
 import { useMapResizeKeybindings } from "./hooks";
+import { useGameConfig } from "../../../../hooks/useGameInfo";
+import { GUN_GAME } from "../../../../domain/constants";
+import { checkScore } from "../../../../domain/apiMethods";
 
 const mapSizeOpts = {
   small: styles["pane--small"],
@@ -18,11 +26,30 @@ const GuessPane = () => {
   const [selectedPoint, setSelectedPoint] = useState(null);
   const [submitted, setSubmitted] = useState(false);
   const [mapSize, setMapSize] = useState("small");
+  const [potentialScore, setPotentialScore] = useState(null);
+  const [gunGameBlock, setGunGameBlock] = useState(false);
+  const unblock = useCallback(() => setGunGameBlock(false), []);
   const toggleBig = useCallback(() => setMapSize(toggleMapSize("big")), []);
   const toggleMed = useCallback(() => setMapSize(toggleMapSize("medium")), []);
   useMapResizeKeybindings(toggleBig);
+  const { gameMode, scoreMethod } = useGameConfig();
+  const targetPoint = useTargetPoint();
+  const roundNum = useCurrentRound();
 
   const handleSubmitGuess = async () => {
+    if (gameMode === GUN_GAME) {
+      const { score } = await checkScore(
+        selectedPoint,
+        targetPoint,
+        scoreMethod,
+        roundNum
+      );
+      if (score < 4000) {
+        setPotentialScore(score);
+        setGunGameBlock(true);
+        return;
+      }
+    }
     setSubmitted(true);
     if (!submitted) {
       await dispatch.submitGuess(selectedPoint);
@@ -30,44 +57,49 @@ const GuessPane = () => {
   };
 
   return (
-    <div className={`${styles.pane} ${mapSizeOpts[mapSize]}`}>
-      <button
-        type="button"
-        className={styles.submit}
-        onClick={handleSubmitGuess}
-        disabled={submitted || selectedPoint === null}
-      >
-        Submit Guess
-      </button>
-      <ClickMarkerMap onMarkerMoved={setSelectedPoint} />
-      <RoundTimer onTimeout={handleSubmitGuess} />
-      <div
-        className={styles.resize}
-        onClick={toggleBig}
-        role="button"
-        tabIndex="0"
-        onKeyDown={({ key }) => {
-          if (key === "Enter") {
-            toggleBig();
-          }
-        }}
-      >
-        {mapSize === "small" ? "↗️" : "↙️"}
-      </div>
-      <div
-        className={`${styles.resize} ${styles["resize--medium"]}`}
-        onClick={toggleMed}
-        role="button"
-        tabIndex="0"
-        onKeyDown={({ key }) => {
-          if (key === "Enter") {
-            toggleMed();
-          }
-        }}
-      >
-        {mapSize === "small" ? "➡️" : "⬅️"}
+    <>
+      {gunGameBlock && gameMode === GUN_GAME && (
+        <GunGame points={potentialScore} onFinish={unblock} />
+      )}
+      <div className={`${styles.pane} ${mapSizeOpts[mapSize]}`}>
+        <button
+          type="button"
+          className={styles.submit}
+          onClick={handleSubmitGuess}
+          disabled={submitted || selectedPoint === null}
+        >
+          Submit Guess
+        </button>
+        <ClickMarkerMap onMarkerMoved={setSelectedPoint} />
+        <RoundTimer onTimeout={handleSubmitGuess} />
+        <div
+          className={styles.resize}
+          onClick={toggleBig}
+          role="button"
+          tabIndex="0"
+          onKeyDown={({ key }) => {
+            if (key === "Enter") {
+              toggleBig();
+            }
+          }}
+        >
+          {mapSize === "small" ? "↗️" : "↙️"}
+        </div>
+        <div
+          className={`${styles.resize} ${styles["resize--medium"]}`}
+          onClick={toggleMed}
+          role="button"
+          tabIndex="0"
+          onKeyDown={({ key }) => {
+            if (key === "Enter") {
+              toggleMed();
+            }
+          }}
+        >
+          {mapSize === "small" ? "➡️" : "⬅️"}
+        </div>
       </div>
-    </div>
+    </>
   );
 };
 

+ 32 - 0
client/src/components/screens/GamePanel/GuessPane/GuessPane.module.css

@@ -37,6 +37,38 @@
   display: none;
 }
 
+.gungame {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  bottom: 0px;
+  right: 0px;
+  z-index: 3;
+  display: flex;
+  flex-flow: column nowrap;
+  justify-content: center;
+  align-items: center;
+}
+
+.gungame_panel {
+  background-color: #333;
+  width: 90%;
+  height: 90%;
+  display: flex;
+  flex-flow: column nowrap;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.gungame_label {
+  text-align: center;
+  margin-top: 2em;
+}
+
+.gungame_canvas {
+  margin-bottom: 8em;
+}
+
 @media only screen and (min-width: 600px) and (min-height: 600px) {
   :root {
     --margin: 10px;

+ 153 - 0
client/src/components/screens/GamePanel/GuessPane/GunGame.jsx

@@ -0,0 +1,153 @@
+import { useEffect, useRef } from "react";
+import styles from "./GuessPane.module.css";
+
+const tileSize = 15;
+const tiles = 40;
+const rawSize = tileSize * tiles;
+
+const drawTile = (ctx, x, y, color) => {
+  ctx.fillStyle = color;
+  ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
+};
+
+const snakeGame = () => {
+  const snakeHead = { x: null, y: null, vx: null, vy: null };
+  let snakeTail = [];
+  const apple = { x: null, y: null };
+
+  const randomApple = () => {
+    apple.x = Math.floor(Math.random() * tiles);
+    apple.y = Math.floor(Math.random() * tiles);
+  };
+
+  const reset = () => {
+    snakeHead.x = Math.floor(tiles / 2);
+    snakeHead.y = snakeHead.x;
+    snakeHead.vx = 0;
+    snakeHead.vy = -1;
+    snakeTail = [{ ...snakeHead }];
+    randomApple();
+  };
+
+  reset();
+
+  const onKey = ({ code }) => {
+    switch (code) {
+      case "ArrowUp":
+        snakeHead.vx = 0;
+        snakeHead.vy = -1;
+        break;
+      case "ArrowDown":
+        snakeHead.vx = 0;
+        snakeHead.vy = 1;
+        break;
+      case "ArrowLeft":
+        snakeHead.vx = -1;
+        snakeHead.vy = 0;
+        break;
+      case "ArrowRight":
+        snakeHead.vx = 1;
+        snakeHead.vy = 0;
+        break;
+      default:
+        break;
+    }
+  };
+
+  let interval;
+
+  const start = (ctx, finishScore, onFinish) => {
+    document.addEventListener("keydown", onKey);
+    interval = setInterval(() => {
+      snakeHead.x += snakeHead.vx;
+      snakeHead.y += snakeHead.vy;
+
+      if (
+        snakeHead.x < 0 ||
+        snakeHead.x >= tiles ||
+        snakeHead.y < 0 ||
+        snakeHead.y >= tiles ||
+        snakeTail.find(({ x, y }) => x === snakeHead.x && y === snakeHead.y)
+      ) {
+        reset();
+      }
+
+      snakeTail.push({ ...snakeHead });
+
+      if (apple.x === snakeHead.x && apple.y === snakeHead.y) {
+        randomApple();
+      } else {
+        snakeTail.shift();
+      }
+
+      const finished = snakeTail.length >= finishScore;
+
+      ctx.fillStyle = "black";
+      ctx.fillRect(0, 0, rawSize, rawSize);
+
+      snakeTail.forEach(({ x, y }) => {
+        drawTile(ctx, x, y, "green");
+      });
+
+      if (!finished) {
+        drawTile(ctx, apple.x, apple.y, "red");
+      }
+      ctx.font = "24px serif";
+      ctx.fillStyle = "#fff";
+      ctx.textAlign = "left";
+      ctx.textBaseline = "top";
+      ctx.fillText(`${snakeTail.length}/${finishScore}`, tileSize, tileSize);
+
+      if (finished) {
+        clearInterval(interval);
+        interval = null;
+
+        ctx.font = "48px serif";
+        ctx.textAlign = "center";
+        ctx.textBaseline = "middle";
+        ctx.fillText("Done!", rawSize / 2, rawSize / 2);
+
+        setTimeout(onFinish, 1000);
+      }
+    }, 50);
+  };
+
+  const stop = () => {
+    document.removeEventListener("keydown", onKey);
+    if (interval) {
+      clearInterval(interval);
+    }
+  };
+
+  return { start, stop, onKey };
+};
+
+const GunGame = ({ points, onFinish }) => {
+  const ref = useRef();
+  useEffect(() => {
+    const { start, stop } = snakeGame();
+    start(ref.current?.getContext("2d"), 10, onFinish);
+    return () => {
+      stop();
+    };
+  }, [onFinish]);
+
+  return (
+    <div className={styles.gungame}>
+      <div className={styles.gungame_panel}>
+        <div className={styles.gungame_label}>
+          <div>Not good enough! That guess only got {points} points!</div>
+          <div>Finish this snake game to try again!</div>
+        </div>
+        <canvas
+          className={styles.gungame_canvas}
+          ref={ref}
+          width={rawSize}
+          height={rawSize}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default GunGame;

+ 4 - 1
client/src/components/util/GameCreationForm/GameCreationForm.jsx

@@ -68,7 +68,10 @@ const PRESETS = {
   },
   GUN_GAME: {
     ...DEFAULTS,
+    timer: 120,
+    rounds: 10,
     gameMode: GUN_GAME,
+    clockMode: TIME_BANK,
     generationMethod: DIFFICULTY_TIERED,
   },
 };
@@ -243,7 +246,7 @@ const GameCreationForm = ({ afterCreate, lastSettings = null }) => {
             <Item value={URBAN} display="🏙️">
               Urban Centers
             </Item>
-            <Item value={DIFFICULTY_TIERED} display="🎲">
+            <Item value={DIFFICULTY_TIERED} display="🪜">
               Difficulty Tiered
             </Item>
           </Dropdown>