浏览代码

API v5 with split rule set fields

Kirk Trombley 4 年之前
父节点
当前提交
9564e7122a

+ 8 - 2
README.md

@@ -55,7 +55,10 @@ PUT /game
         "rounds": number,
         "countryLock": string || null (default: null),
         "generationMethod": string (default: "RANDOMSTREETVIEW"),
-        "ruleSet": string (default: "NORMAL")
+        "gameMode": string (default: "NORMAL"),
+        "clockMode": string (default: "NORMAL"),
+        "scoreMethod": string (default: "DISTANCE"),
+        "roundPointCap": int || null (default: null)
     }
     Returns 501 vs 200 and {
         "gameId": string
@@ -66,7 +69,10 @@ GET /game/{game_id}/config
         "rounds": number,
         "countryLock": string || null,
         "generationMethod": string,
-        "ruleSet": string
+        "gameMode": string,
+        "clockMode": string,
+        "scoreMethod": string,
+        "roundPointCap": int || null
     }
 GET /game/{game_id}/coords
     Returns 404 vs 200 and {

+ 3 - 3
client/src/components/screens/GamePanel/GamePanel.jsx

@@ -11,17 +11,17 @@ import { useIsFinished } from "./hooks";
 const GamePanel = () => {
   // warn the user if they navigate away
   usePreventNavigation();
-  const { ruleSet } = useGameConfig();
+  const { gameMode, clockMode } = useGameConfig();
   const finished = useIsFinished();
 
   return finished ? (
     <Loading />
   ) : (
     <div className={styles.page}>
-      {ruleSet === RACE && <RaceMode rate={1000} cutoffTime={10} />}
+      {clockMode === RACE && <RaceMode rate={1000} cutoffTime={10} />}
       <div className={styles.streetview}>
         <PositionedStreetView />
-        {ruleSet === FROZEN && <div className={styles.freeze} />}
+        {gameMode === FROZEN && <div className={styles.freeze} />}
       </div>
       <GuessPane />
     </div>

+ 26 - 12
client/src/components/screens/Lobby/Lobby.jsx

@@ -13,33 +13,34 @@ import {
   TIME_BANK,
   FROZEN,
   COUNTRY_RACE,
+  RACE,
 } from "../../../domain/ruleSets";
 
 export const GameInfo = () => {
-  const { rounds, timer, countryLock, ruleSet } = useGameConfig();
+  const {
+    rounds,
+    timer,
+    countryLock,
+    gameMode,
+    clockMode,
+    scoreMethod,
+    roundPointCap,
+  } = useGameConfig();
 
   if (!rounds || !timer) {
     return <Loading />;
   }
 
   let explanation;
-  switch (ruleSet) {
-    case COUNTRY_RACE:
-      explanation = `${rounds !== 1 ? ", each" : ""} with a ${ms(
-        timer * 1000
-      )} time limit, where you must be the fastest to select the right country`;
-      break;
-    case FROZEN:
-      explanation = `${rounds !== 1 ? ", each" : ""} with a ${ms(
-        timer * 1000
-      )} time limit, and you will not be able to adjust your view`;
-      break;
+
+  switch (clockMode) {
     case TIME_BANK:
       explanation = `with a ${ms(
         timer * 1000 * rounds
       )} time bank across all rounds`;
       break;
     case NORMAL: // fall-through
+    case RACE: // fall-through
     default:
       explanation = `${rounds !== 1 ? ", each" : ""} with a ${ms(
         timer * 1000
@@ -47,6 +48,19 @@ export const GameInfo = () => {
       break;
   }
 
+  if (scoreMethod === COUNTRY_RACE) {
+    explanation +=
+      ", where you must be the fastest to select the right country";
+  }
+
+  if (gameMode === FROZEN) {
+    explanation += ", and you will not be able to adjust your view";
+  }
+
+  if (roundPointCap) {
+    explanation += `. Only ${roundPointCap} total points will be up for grabs each round`;
+  }
+
   return (
     <>
       <span className={styles.label}>

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

@@ -8,6 +8,7 @@ import {
   TIME_BANK,
   RACE,
   COUNTRY_RACE,
+  adapt4To5,
 } from "../../../domain/ruleSets";
 import useCountryLookup from "../../../hooks/useCountryLookup";
 import Loading from "../Loading";
@@ -78,7 +79,19 @@ const GameCreationForm = ({ afterCreate }) => {
     setLoading(true);
     let gameId;
     try {
-      gameId = await createGame(timer, rounds, countryLock, genMethod, ruleSet);
+      const { gameMode, clockMode, scoreMethod, roundPointCap } = adapt4To5(
+        ruleSet
+      );
+      gameId = await createGame(
+        timer,
+        rounds,
+        countryLock,
+        genMethod,
+        gameMode,
+        clockMode,
+        scoreMethod,
+        roundPointCap
+      );
     } catch (e) {
       setCreationError(true);
       setLoading(false);

+ 8 - 2
client/src/domain/apiMethods.js

@@ -39,7 +39,10 @@ export const createGame = async (
   rounds,
   countryLock,
   generationMethod,
-  ruleSet
+  gameMode,
+  clockMode,
+  scoreMethod,
+  roundPointCap
 ) => {
   const res = await fetch(`${API_BASE}/game`, {
     method: "PUT",
@@ -51,7 +54,10 @@ export const createGame = async (
       rounds,
       countryLock,
       generationMethod,
-      ruleSet,
+      gameMode,
+      clockMode,
+      scoreMethod,
+      roundPointCap,
     }),
   });
   if (!res.ok) {

+ 37 - 0
client/src/domain/ruleSets.js

@@ -3,3 +3,40 @@ export const TIME_BANK = "TIMEBANK";
 export const FROZEN = "FROZEN";
 export const RACE = "RACE";
 export const COUNTRY_RACE = "COUNTRYRACE";
+export const DISTANCE = "DISTANCE";
+
+// temporary adapter from api v4 to v5
+export const adapt4To5 = ruleSet => {
+  switch (ruleSet) {
+    case FROZEN:
+      return {
+        gameMode: FROZEN,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
+      };
+    case RACE: // fall-through
+    case TIME_BANK:
+      return {
+        gameMode: NORMAL,
+        clockMode: ruleSet,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
+      };
+    case COUNTRY_RACE:
+      return {
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: COUNTRY_RACE,
+        roundPointCap: null,
+      };
+    default:
+      // includes NORMAL
+      return {
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
+      };
+  }
+};

+ 25 - 5
client/src/tests/GamePanel.test.js

@@ -1,6 +1,6 @@
 import React from "react";
 import { shallow } from "enzyme";
-import { FROZEN, NORMAL, RACE } from "../domain/ruleSets";
+import { DISTANCE, FROZEN, NORMAL, RACE } from "../domain/ruleSets";
 import GamePanel from "../components/screens/GamePanel";
 
 jest.mock("../hooks/usePreventNavigation");
@@ -14,7 +14,12 @@ import { useIsFinished } from "../components/screens/GamePanel/hooks";
 describe("GamePanel", () => {
   it("renders for NORMAL game", () => {
     useIsFinished.mockReturnValue(false);
-    useGameConfig.mockReturnValue({ ruleSet: NORMAL });
+    useGameConfig.mockReturnValue({
+      gameMode: NORMAL,
+      clockMode: NORMAL,
+      scoreMethod: DISTANCE,
+      roundPointCap: null,
+    });
     const rendered = shallow(<GamePanel />);
     expect(rendered).toMatchSnapshot();
     expect(usePreventNavigation).toHaveBeenCalled();
@@ -23,7 +28,12 @@ describe("GamePanel", () => {
 
   it("renders for end of game", () => {
     useIsFinished.mockReturnValue(true);
-    useGameConfig.mockReturnValue({ ruleSet: NORMAL });
+    useGameConfig.mockReturnValue({
+      gameMode: NORMAL,
+      clockMode: NORMAL,
+      scoreMethod: DISTANCE,
+      roundPointCap: null,
+    });
     const rendered = shallow(<GamePanel />);
     expect(rendered).toMatchSnapshot();
     expect(usePreventNavigation).toHaveBeenCalled();
@@ -32,7 +42,12 @@ describe("GamePanel", () => {
 
   it("renders for FROZEN game", () => {
     useIsFinished.mockReturnValue(false);
-    useGameConfig.mockReturnValue({ ruleSet: FROZEN });
+    useGameConfig.mockReturnValue({
+      gameMode: FROZEN,
+      clockMode: NORMAL,
+      scoreMethod: DISTANCE,
+      roundPointCap: null,
+    });
     const rendered = shallow(<GamePanel />);
     expect(rendered).toMatchSnapshot();
     expect(usePreventNavigation).toHaveBeenCalled();
@@ -41,7 +56,12 @@ describe("GamePanel", () => {
 
   it("renders for RACE game", () => {
     useIsFinished.mockReturnValue(false);
-    useGameConfig.mockReturnValue({ ruleSet: RACE });
+    useGameConfig.mockReturnValue({
+      gameMode: NORMAL,
+      clockMode: RACE,
+      scoreMethod: DISTANCE,
+      roundPointCap: null,
+    });
     const rendered = shallow(<GamePanel />);
     expect(rendered).toMatchSnapshot();
     expect(usePreventNavigation).toHaveBeenCalled();

+ 41 - 7
client/src/tests/Lobby.test.js

@@ -3,6 +3,7 @@ import { shallow } from "enzyme";
 import Lobby, { GameInfo, PlayerList } from "../components/screens/Lobby/Lobby";
 import {
   COUNTRY_RACE,
+  DISTANCE,
   FROZEN,
   NORMAL,
   RACE,
@@ -64,6 +65,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 5,
         timer: 300,
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -73,6 +78,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 1,
         timer: 300,
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -82,7 +91,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 5,
         timer: 300,
-        ruleSet: COUNTRY_RACE,
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: COUNTRY_RACE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -92,7 +104,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 5,
         timer: 300,
-        ruleSet: FROZEN,
+        gameMode: FROZEN,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -102,7 +117,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 1,
         timer: 300,
-        ruleSet: COUNTRY_RACE,
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: COUNTRY_RACE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -112,7 +130,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 1,
         timer: 300,
-        ruleSet: FROZEN,
+        gameMode: FROZEN,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -122,7 +143,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 5,
         timer: 300,
-        ruleSet: TIME_BANK,
+        gameMode: NORMAL,
+        clockMode: TIME_BANK,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -132,7 +156,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 5,
         timer: 300,
-        ruleSet: RACE,
+        gameMode: NORMAL,
+        clockMode: RACE,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -142,7 +169,10 @@ describe("Lobby", () => {
       useGameConfig.mockReturnValue({
         rounds: 5,
         timer: 300,
-        ruleSet: NORMAL,
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();
@@ -154,6 +184,10 @@ describe("Lobby", () => {
         rounds: 5,
         timer: 300,
         countryLock: "country",
+        gameMode: NORMAL,
+        clockMode: NORMAL,
+        scoreMethod: DISTANCE,
+        roundPointCap: null,
       });
       const rendered = shallow(<GameInfo />);
       expect(rendered).toMatchSnapshot();

+ 3 - 3
client/src/tests/__snapshots__/apiMethods.test.js.snap

@@ -83,7 +83,7 @@ exports[`apiMethods createGame creates game 2`] = `
     Array [
       "https://hiram.services/terrassumptions/api/game",
       Object {
-        "body": "{\\"timer\\":\\"timer\\",\\"rounds\\":\\"rounds\\",\\"countryLock\\":\\"countryLock\\",\\"generationMethod\\":\\"generationMethod\\",\\"ruleSet\\":\\"ruleSet\\"}",
+        "body": "{\\"timer\\":\\"timer\\",\\"rounds\\":\\"rounds\\",\\"countryLock\\":\\"countryLock\\",\\"generationMethod\\":\\"generationMethod\\",\\"gameMode\\":\\"gameMode\\",\\"clockMode\\":\\"clockMode\\",\\"scoreMethod\\":\\"scoreMethod\\",\\"roundPointCap\\":\\"roundPointCap\\"}",
         "headers": Object {
           "Content-Type": "application/json",
         },
@@ -106,7 +106,7 @@ exports[`apiMethods createGame passes thrown error 1`] = `
     Array [
       "https://hiram.services/terrassumptions/api/game",
       Object {
-        "body": "{\\"timer\\":\\"timer\\",\\"rounds\\":\\"rounds\\",\\"countryLock\\":\\"countryLock\\",\\"generationMethod\\":\\"generationMethod\\",\\"ruleSet\\":\\"ruleSet\\"}",
+        "body": "{\\"timer\\":\\"timer\\",\\"rounds\\":\\"rounds\\",\\"countryLock\\":\\"countryLock\\",\\"generationMethod\\":\\"generationMethod\\",\\"gameMode\\":\\"gameMode\\",\\"clockMode\\":\\"clockMode\\",\\"scoreMethod\\":\\"scoreMethod\\",\\"roundPointCap\\":\\"roundPointCap\\"}",
         "headers": Object {
           "Content-Type": "application/json",
         },
@@ -129,7 +129,7 @@ exports[`apiMethods createGame throws on server error 1`] = `
     Array [
       "https://hiram.services/terrassumptions/api/game",
       Object {
-        "body": "{\\"timer\\":\\"timer\\",\\"rounds\\":\\"rounds\\",\\"countryLock\\":\\"countryLock\\",\\"generationMethod\\":\\"generationMethod\\",\\"ruleSet\\":\\"ruleSet\\"}",
+        "body": "{\\"timer\\":\\"timer\\",\\"rounds\\":\\"rounds\\",\\"countryLock\\":\\"countryLock\\",\\"generationMethod\\":\\"generationMethod\\",\\"gameMode\\":\\"gameMode\\",\\"clockMode\\":\\"clockMode\\",\\"scoreMethod\\":\\"scoreMethod\\",\\"roundPointCap\\":\\"roundPointCap\\"}",
         "headers": Object {
           "Content-Type": "application/json",
         },

+ 12 - 3
client/src/tests/apiMethods.test.js

@@ -133,7 +133,10 @@ describe("apiMethods", () => {
           "rounds",
           "countryLock",
           "generationMethod",
-          "ruleSet"
+          "gameMode",
+          "clockMode",
+          "scoreMethod",
+          "roundPointCap"
         )
       ).toMatchSnapshot();
       expect(global.fetch).toMatchSnapshot();
@@ -151,7 +154,10 @@ describe("apiMethods", () => {
           "rounds",
           "countryLock",
           "generationMethod",
-          "ruleSet"
+          "gameMode",
+          "clockMode",
+          "scoreMethod",
+          "roundPointCap"
         )
       ).rejects.toThrow();
       expect(global.fetch).toMatchSnapshot();
@@ -166,7 +172,10 @@ describe("apiMethods", () => {
           "rounds",
           "countryLock",
           "generationMethod",
-          "ruleSet"
+          "gameMode",
+          "clockMode",
+          "scoreMethod",
+          "roundPointCap"
         )
       ).rejects.toThrow();
       expect(global.fetch).toMatchSnapshot();

+ 4 - 2
server/app/api/game.py

@@ -7,7 +7,7 @@ from pydantic import conint, constr
 from sqlalchemy.orm import Session
 
 from .. import scoring
-from ..schemas import GameConfig, Guess, RuleSetEnum
+from ..schemas import GameConfig, Guess, ScoreMethodEnum
 from ..db import get_db, queries, models
 from ..point_gen import points, ExhaustedSourceError, reverse_geocode
 
@@ -147,11 +147,13 @@ async def submit_guess(round_number: conint(gt=0),
                  player: models.Player = Depends(get_player)):
     target = queries.get_coordinate(db, player.game_id, round_number)
     country_code = await reverse_geocode(guess.lat, guess.lng)
-    if game.rule_set == RuleSetEnum.country_race:
+    if game.score_method == ScoreMethodEnum.country_race:
         score = scoring.score_country_race(target.country_code, country_code, guess.time_remaining, game.timer)
         distance = None
     else:
         score, distance = scoring.score((target.latitude, target.longitude), (guess.lat, guess.lng))
+    if game.round_point_cap is not None:
+        score = min(score, max(0, game.round_point_cap - sum(g.round_score for p in game.players for g in p.guesses)))
     added = queries.add_guess(db, guess, player, country_code, round_number, score)
     if not added:
         raise HTTPException(status_code=409, detail="Already submitted guess for this round")

+ 1 - 1
server/app/api/other.py

@@ -43,7 +43,7 @@ class RecentResponse(CamelModel):
 
 @router.get("/health")
 def health():
-    return { "status": "healthy", "version": "4.0" }
+    return { "status": "healthy", "version": "5.0" }
 
 
 @router.post("/score", response_model=Score)

+ 4 - 1
server/app/db/models.py

@@ -15,7 +15,10 @@ class Game(Base):
     rounds = Column(Integer)
     country_lock = Column(String)
     generation_method = Column(String)
-    rule_set = Column(String)
+    game_mode = Column(String)
+    clock_mode = Column(String)
+    score_method = Column(String)
+    round_point_cap = Column(Integer)
     created_at = Column(DateTime)
     coordinates = relationship("Coordinate", lazy=True, order_by="Coordinate.round_number")
     players = relationship("Player", lazy=True, backref="game")

+ 6 - 3
server/app/db/queries.py

@@ -23,7 +23,10 @@ def create_game(db: Session, conf: schemas.GameConfig, coords: List[Tuple[str, f
         rounds=conf.rounds,
         country_lock=conf.country_lock,
         generation_method=conf.generation_method,
-        rule_set=conf.rule_set,
+        game_mode=conf.game_mode,
+        clock_mode=conf.clock_mode,
+        score_method=conf.score_method,
+        round_point_cap=conf.round_point_cap,
         created_at=datetime.now(),
     )
     db.add(new_game)
@@ -91,7 +94,7 @@ def get_next_coordinate(db: Session, player: Player) -> Coordinate:
 
 
 def get_next_round_time(player: Player) -> int:
-    if player.game.rule_set == schemas.RuleSetEnum.time_bank:
+    if player.game.clock_mode == schemas.ClockModeEnum.time_bank:
         if len(player.guesses) == 0:
             return player.game.timer * player.game.rounds
         return player.guesses[-1].time_remaining
@@ -113,7 +116,7 @@ def add_guess(db: Session, guess: schemas.Guess, player: Player, country_code: U
     )
     db.add(g)
     db.commit()
-    if guess.time_remaining <= 0 and player.game.rule_set == schemas.RuleSetEnum.time_bank:
+    if guess.time_remaining <= 0 and player.game.clock_mode == schemas.ClockModeEnum.time_bank:
         for r in range(round_number, player.game.rounds):
             add_timeout(db, player, r + 1)
     return True

+ 14 - 3
server/app/schemas.py

@@ -13,11 +13,19 @@ class GenMethodEnum(str, Enum):
     urban = "URBAN"
 
 
-class RuleSetEnum(str, Enum):
+class GameModeEnum(str, Enum):
     normal = "NORMAL"
-    time_bank = "TIMEBANK"
     frozen = "FROZEN"
+
+
+class ClockModeEnum(str, Enum):
+    normal = "NORMAL"
     race = "RACE"
+    time_bank = "TIMEBANK"
+
+
+class ScoreMethodEnum(str, Enum):
+    distance = "DISTANCE"
     country_race = "COUNTRYRACE"
 
 
@@ -26,7 +34,10 @@ class GameConfig(CamelModel):
     rounds: conint(gt=0)
     country_lock: Union[CountryCode, None] = None
     generation_method: GenMethodEnum = GenMethodEnum.rsv
-    rule_set: RuleSetEnum = RuleSetEnum.normal
+    game_mode: GameModeEnum = GameModeEnum.normal
+    clock_mode: ClockModeEnum = ClockModeEnum.normal
+    score_method: ScoreMethodEnum = ScoreMethodEnum.distance
+    round_point_cap: Union[int, None] = None
 
     class Config:
         orm_mode = True