From 7a15373ad7cf860a39fd70f32d8b31a0ea9fd31f Mon Sep 17 00:00:00 2001 From: Alvaro Estrella <62617705+AEstrellaS@users.noreply.github.com> Date: Fri, 17 Nov 2023 02:14:21 -0800 Subject: [PATCH] v5.0.0-alpha.272: Win By VVH and Computer Logic, Puzzle Remote Server CORS Fix (#103) * feat(vvh): Win By View in VVH. * feat(cpu): CPU play by Strategy: remoteness or win by. * fix(vvh): Fixed issue where links between positions and moves would render on top of the positions and move nodes. * fix(vvh): Fixed issue where move nodes would render on top of position nodes. * fix(vvh): Fixed issue where links would render on top of move nodes. * docs: Added comments based on the @JSDocs standard, cleaned up code. --------- Co-authored-by: Robert Shi <42782709+robertyishi@users.noreply.github.com> --- package.json | 2 +- .../GameBody/AppGameBodyHeaderOptions.vue | 83 +++ src/components/units/VVH/AppGameVvhBody.vue | 531 +++++++++++++++--- src/models/datas/defaultApp.ts | 13 +- src/scripts/apis/gamesCrafters/types.ts | 6 +- src/scripts/gamesmanUni/index.ts | 94 +++- src/scripts/gamesmanUni/types.ts | 5 + src/scripts/plugins/store/index.ts | 28 +- 8 files changed, 675 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index 0d51e80b..deb0252f 100644 --- a/package.json +++ b/package.json @@ -48,5 +48,5 @@ "preview:https": "serve dist", "reinstall": "rm -rf node_modules; yarn; vue-tsc --noEmit" }, - "version": "5.0.0-alpha.271" + "version": "5.0.0-alpha.272" } diff --git a/src/components/units/GameBody/AppGameBodyHeaderOptions.vue b/src/components/units/GameBody/AppGameBodyHeaderOptions.vue index 832490aa..fe93203f 100644 --- a/src/components/units/GameBody/AppGameBodyHeaderOptions.vue +++ b/src/components/units/GameBody/AppGameBodyHeaderOptions.vue @@ -62,6 +62,25 @@ type="checkbox" v-model="updatedLeftPlayer.isComputer" /> +
+
+
+ {{ CPUsStrategy[0] }} ▼ +
+
+
+
+ {{ CPUStrategyOption }} +
+
+
+
+
+ Remoteness +
+
+ Strategy +

Second Player

@@ -79,6 +98,25 @@ type="checkbox" v-model="updatedRightPlayer.isComputer" /> +
+
+
+ {{ CPUsStrategy[1] }} ▼ +
+
+
+
+ {{ CPUStrategyOption }} +
+
+
+
+
+ Remoteness +
+
+ Strategy +
@@ -154,6 +192,8 @@ const currentRightPlayer = computed(() => store.getters.currentRightPlayer); const currentLeftPlayerName = computed(() => (currentLeftPlayer ? currentLeftPlayer.value.name : "")); const currentRightPlayerName = computed(() => (currentRightPlayer ? currentRightPlayer.value.name : "")); + const currentGameType = computed(() => store.getters.currentGameType); + const currentGameId = computed(() => store.getters.currentGameId); const updatedLeftPlayer = ref({ name: "", isComputer: false }); const updatedRightPlayer = ref({ name: "", isComputer: false }); @@ -206,6 +246,29 @@ } } ); + + // Stores true or false, whether the current game supports the Win By view or it does not. + const supportsWinBy = computed(() => + store.getters.supportsWinBy(currentGameType.value, currentGameId.value) + ); + + // Array of the available computer strategies. + const CPUStrategies = ["Remoteness", "Win By"] + + // Default computer strategies [Remoteness, Remoteness]. + const CPUsStrategy = ref([CPUStrategies[0],CPUStrategies[0]]); + + /** + * Changes the strategy of play of the CPU player to a new CPU strategy. + * @param {number} CPUID - the id of the CPU player. 0 for the first player, 1 for the second player. + * @param {string} newCPUStrategy - new CPU player strategy. + * @returns none. + */ + const setCPUStrategy = (CPUID: number, newCPUStrategy: string) => { + CPUsStrategy.value[CPUID] = newCPUStrategy; + store.commit(mutationTypes.setCPUsStrategy, CPUsStrategy.value); + + }; diff --git a/src/components/units/VVH/AppGameVvhBody.vue b/src/components/units/VVH/AppGameVvhBody.vue index 63027129..3dea8fe8 100644 --- a/src/components/units/VVH/AppGameVvhBody.vue +++ b/src/components/units/VVH/AppGameVvhBody.vue @@ -7,7 +7,21 @@ lose

- Remoteness +

+
+ {{ vvhView }} ▼ +
+
+
+
+ {{ vvhViewOption }} +
+
+
+
+
+ Remoteness +

@@ -16,7 +30,7 @@ + xmlns="http://www.w3.org/2000/svg" v-if="vvhView === appVvhViews[0]"> + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + +

Moves

- Remoteness + {{ vvhView }}

-

Remoteness Coordinate Height

+

View Coordinate Height

-

Move Coordinate Width

+

Coordinate Width

@@ -761,16 +1034,17 @@
-

Remoteness Bar Width

+

Bar Width

-

Remoteness Interval Bar Width

+

Interval Bar Width

-

Remoteness Interval

- +

View Interval

+ +
@@ -778,9 +1052,10 @@ diff --git a/src/models/datas/defaultApp.ts b/src/models/datas/defaultApp.ts index 9fc230e6..966ba416 100644 --- a/src/models/datas/defaultApp.ts +++ b/src/models/datas/defaultApp.ts @@ -15,7 +15,7 @@ export const defaultPreferences: Types.Preferences = { export const defaultDataSources: Types.DataSources = { gitHubRepositoryAPI: "https://api.github.com/repos/GamesCrafters/GamesmanUni", - onePlayerGameAPI: "https://nyc.cs.berkeley.edu/puzzles", + onePlayerGameAPI: "https://nyc.cs.berkeley.edu/puzzles/", //onePlayerGameAPI: "http://localhost:9001/", twoPlayerGameAPI: "https://nyc.cs.berkeley.edu/universal/v1/games", //twoPlayerGameAPI: "http://localhost:8082/games" @@ -30,6 +30,7 @@ export const defaultAvailableMove: Types.Move = { position: "", positionValue: "", remoteness: 0, + winby: 0, mex: "", animationPhases: [] }; @@ -45,7 +46,8 @@ export const defaultPosition: Types.Position = { position: "", positionValue: "", remoteness: 0, - mex: "" + mex: "", + winby: 0, }; export const defaultPositions: Types.Positions = {}; @@ -76,7 +78,8 @@ export const defaultGame: Types.Game = { variants: { ...defaultVariants, variants: {} }, status: "", custom: false, - gui_status: "v0" + gui_status: "v0", + supportsWinBy: 0, }; export const defaultGames: Types.Games = { @@ -115,7 +118,7 @@ export const defaultOptions: Types.Options = { showMenu: true, showVvhGuides: true, showVvhMeters: false, - vvhScrolling: false + vvhScrolling: false, }; export const defaultMatches: Types.Matches = {}; @@ -159,6 +162,7 @@ export const defaultApp: Types.App = { commits: { ...defaultUpdate, commits: {} }, options: { ...defaultOptions }, matches: {}, + vvhView: "", currentMatch: { id: 0, gameType: "", gameId: "", @@ -178,4 +182,5 @@ export const defaultApp: Types.App = { backgroundLoading: false, computerMoving: false }, + CPUsStrategy: ["Remoteness", "Remoteness"], }; diff --git a/src/scripts/apis/gamesCrafters/types.ts b/src/scripts/apis/gamesCrafters/types.ts index ce55be9a..15138fa6 100644 --- a/src/scripts/apis/gamesCrafters/types.ts +++ b/src/scripts/apis/gamesCrafters/types.ts @@ -29,12 +29,13 @@ export type OnePlayerGameVariants = Status & { imageAutoGUIData: ImageAutoGUIData; }>; custom: string; + }; }; export type TwoPlayerGames = Status & { response: Array; }; @@ -44,6 +45,7 @@ export type TwoPlayerGameVariants = Status & { gameId: string; instructions: string; name: string; + supportsWinBy: number; variants: Array>; }>; position: string; positionValue: string; remoteness: number; + winby: number; mex: string; }; }; diff --git a/src/scripts/gamesmanUni/index.ts b/src/scripts/gamesmanUni/index.ts index 299808ea..63995f34 100644 --- a/src/scripts/gamesmanUni/index.ts +++ b/src/scripts/gamesmanUni/index.ts @@ -4,6 +4,7 @@ import * as GHAPI from "../apis/gitHub"; import type * as Types from "./types"; import * as Defaults from "../../models/datas/defaultApp"; import { handleMoveAnimation, animationEpilogue } from "./moveAnimation" +import { useStore } from "../plugins/store"; const moveHistoryDelim = ':'; const deepcopy = (obj: Object) => { @@ -59,6 +60,7 @@ export const loadVariants = async (app: Types.App, payload: { gameType: string; gui_status: variant.gui_status }; } + app.gameTypes[payload.gameType].games[payload.gameId].supportsWinBy = payload.gameType === "games" ? (variants).response.supportsWinBy : Defaults.defaultGame.supportsWinBy; return app; }; @@ -86,6 +88,7 @@ const formatMoves = (source: Array<{ position: string; positionValue: string; remoteness: number; + winby: number; mex: string; animationPhases: Array>; }>) => { @@ -119,6 +122,7 @@ const loadPosition = async (app: Types.App, payload: { gameType: string; gameId: position: updatedPosition.response.position, positionValue: updatedPosition.response.positionValue, remoteness: updatedPosition.response.remoteness, + winby: updatedPosition.response.winby, mex: updatedPosition.response["mex"] || "", }; return app; @@ -271,6 +275,14 @@ const loadVariant = async (app: Types.App, payload: { gameType: string; gameId: }; }; +/** + * Determines the maximum Remoteness value between rounds payload.from to payload.to. If there is no Remoteness value greater + * than the threshold of 5, then returns the threshold. + * @param {Types.App} app - App. + * @param {number} payload.from - round id to start calculating the maximum Remoteness value. + * @param {number} payload.to - round id to end calculating the maximum Remoteness value. + * @returns the maximum Remoteness value when it is greater than the threshold, else returns the threshold. +*/ export const getMaximumRemoteness = (app: Types.App, payload: { from: number; to: number }) => { const remotenesses = new Set(); remotenesses.add(5); // In case all involved positions are draw, 5 shall be the default maximum remoteness. @@ -288,6 +300,31 @@ export const getMaximumRemoteness = (app: Types.App, payload: { from: number; to return Math.max(...remotenesses); }; +/** + * Determines the maximum Win By value between rounds payload.from to payload.to. If there is no Win By value greater + * than the threshold of 5, then returns the threshold. + * @param {Types.App} app - App. + * @param {number} payload.from - round id to start calculating the maximum Win By value. + * @param {number} payload.to - round id to end calculating the maximum Win By value. + * @returns the maximum Win By value when it is greater than the threshold, else returns the threshold. +*/ +export const getMaximumWinBy = (app: Types.App, payload: { from: number; to: number }) => { + const winbys = new Set(); + winbys.add(5); // In case all involved positions are draw, 5 shall be the default maximum winby + for (let roundId = payload.from; roundId <= payload.to; roundId++) { + const round = app.currentMatch.rounds[roundId]; + if (round.position.positionValue !== "draw") winbys.add(round.position.winby); + if (app.options.showNextMoves) { + for (const availableMove in round.position.availableMoves) { + if (round.position.availableMoves[availableMove].positionValue !== "draw") { + winbys.add(round.position.availableMoves[availableMove].winby); + } + } + } + } + return Math.max(...winbys); +}; + export const isEndOfMatch = (app: Types.App) => !app.currentMatch.round.position.remoteness || !Object.keys(app.currentMatch.round.position.availableMoves).length; @@ -302,16 +339,61 @@ export const exitMatch = (app: Types.App) => { return app; }; +/** + * Determines the CPU player's next move. If the CPU strategy is set to 'Remoteness' and the CPU player is winning on the current position, returns + * the move with the lowest remoteness value; if there are multiple moves with the lowest remoteness value and the current game supports win by, returns + * the move with the highest win by value. If the CPU player is loosing on the current position, returns the move with the highest remoteness value; if + * there are multiple moves with the highest remoteness value, then returns the move with the lowest win by value. If the CPU strategy is set to 'Win By' + * and the CPU player is winning on the current position, returns the move with the highest win by value; if there are multiple moves with the highest win + * by value, returns the move with the lowest remoteness value. If the CPU player is loosing on the current position, returns the move with the lowest win + * by value; if there are multiple moves with the lowest win by value, returns the move with the highest remoteness. + * @param {Types.Round} round - current round. + * @returns CPU player's next move. +*/ export const generateComputerMove = (round: Types.Round) => { + const store = useStore(); + const currentPlayerTurn = store.getters.currentValuedRounds[round.id].firstPlayerTurn ? 1 : 2; + const CPUStrategy = store.getters.currentCPUStrategy(currentPlayerTurn - 1); + const gameType = store.getters.currentGameType; + const gameId = store.getters.currentGameId; + const supportsWinBy = store.getters.supportsWinBy(gameType, gameId); const availableMoves = Object.values(round.position.availableMoves); const currentPositionValue = round.position.positionValue; + let bestMoves = availableMoves.filter((availableMove) => availableMove.moveValue === currentPositionValue || currentPositionValue === "unsolved"); - if (currentPositionValue === "win" || currentPositionValue === "tie") { - const minimumRemoteness = Math.min(...bestMoves.map((bestMove) => bestMove.remoteness)); - bestMoves = bestMoves.filter((availableMove) => availableMove.remoteness === minimumRemoteness); - } else if (currentPositionValue === "lose") { - const maximumRemoteness = Math.max(...bestMoves.map((bestMove) => bestMove.remoteness)); - bestMoves = availableMoves.filter((availableMove) => availableMove.remoteness === maximumRemoteness); + + if (CPUStrategy === "Remoteness") { + if (currentPositionValue === "win" || currentPositionValue === "tie") { + const minimumRemoteness = Math.min(...bestMoves.map((bestMove) => bestMove.remoteness)); + bestMoves = bestMoves.filter((availableMove) => availableMove.remoteness === minimumRemoteness); + + if (supportsWinBy) { + const maximumWinBy = Math.max(...bestMoves.map((bestMove) => bestMove.winby)); + bestMoves = bestMoves.filter((availableMove) => availableMove.winby === maximumWinBy); + } + } else if (currentPositionValue === "lose") { + const maximumRemoteness = Math.max(...bestMoves.map((bestMove) => bestMove.remoteness)); + bestMoves = bestMoves.filter((availableMove) => availableMove.remoteness === maximumRemoteness); + + if (supportsWinBy) { + const minimumWinBy = Math.min(...bestMoves.map((bestMove) => bestMove.winby)); + bestMoves = bestMoves.filter((availableMove) => availableMove.winby === minimumWinBy); + } + } + } else if (CPUStrategy === "Win By"){ + if (currentPositionValue === "win" || currentPositionValue === "tie") { + const maximumWinBy = Math.max(...bestMoves.map((bestMove) => bestMove.winby)); + bestMoves = bestMoves.filter((availableMove) => availableMove.winby === maximumWinBy); + + const minimumRemoteness = Math.min(...bestMoves.map((bestMove) => bestMove.remoteness)); + bestMoves = bestMoves.filter((availableMove) => availableMove.remoteness === minimumRemoteness); + } else if (currentPositionValue === "lose") { + const minimumWinBy = Math.min(...bestMoves.map((bestMove) => bestMove.winby)); + bestMoves = bestMoves.filter((availableMove) => availableMove.winby === minimumWinBy); + + const maximumRemoteness = Math.max(...bestMoves.map((bestMove) => bestMove.remoteness)); + bestMoves = bestMoves.filter((availableMove) => availableMove.remoteness === maximumRemoteness); + } } return bestMoves[Math.floor(Math.random() * bestMoves.length)].move; }; diff --git a/src/scripts/gamesmanUni/types.ts b/src/scripts/gamesmanUni/types.ts index 354e78af..f809843c 100644 --- a/src/scripts/gamesmanUni/types.ts +++ b/src/scripts/gamesmanUni/types.ts @@ -14,6 +14,8 @@ export type App = Update & { options: Options; matches: Matches; currentMatch: Match; + vvhView: string; + CPUsStrategy: string[]; }; export type Player = { @@ -53,6 +55,7 @@ export type Game = { status: string; gui_status: string; custom: boolean; + supportsWinBy: number; }; export type Variants = Update & { @@ -77,6 +80,7 @@ export type Position = Update & { position: string; positionValue: string; remoteness: number; + winby: number; mex: string; }; @@ -93,6 +97,7 @@ export type Move = { position: string; positionValue: string; remoteness: number; + winby: number; mex: string; animationPhases: Array>; }; diff --git a/src/scripts/plugins/store/index.ts b/src/scripts/plugins/store/index.ts index a36057fd..bc80dfbe 100644 --- a/src/scripts/plugins/store/index.ts +++ b/src/scripts/plugins/store/index.ts @@ -77,6 +77,13 @@ type Getters = { GMUTypes.Variants; version(state: State): string; volume(state: State): number; + supportsWinBy(state: State): + (gameType: string, gameId: string) => boolean; + currentWinBy(state: State): number; + maximumWinBy(state: State): + (from: number, to: number) => number; + currentCPUStrategy(state: State): + (CPUID: number) => string; }; const getters: Vuex.GetterTree & Getters = { @@ -206,7 +213,18 @@ const getters: Vuex.GetterTree & Getters = { version: (state: State) => state.app.version, volume: (state: State) => - state.app.preferences.volume + state.app.preferences.volume, + supportsWinBy: (state: State) => + (gameType: string, gameId: string) => + state.app.gameTypes[gameType] && state.app.gameTypes[gameType].games[gameId].supportsWinBy === 1 ? true : false, + currentWinBy: (state: State) => + state.app.currentMatch.round.position.winby, + maximumWinBy: (state: State) => + (from: number, to: number) => + GMU.getMaximumWinBy(state.app, { from, to }), + currentCPUStrategy: (state: State) => + (CPUID: number) => + state.app.CPUsStrategy[CPUID], }; export enum mutationTypes { @@ -225,6 +243,8 @@ export enum mutationTypes { showVvhGuides = "showVvhGuides", showVvhMeters = "showVvhMeters", toggleVvhScrolling = "toggleVvhScrolling", + setVvhView = "setVvhView", + setCPUsStrategy = "setCPUsStrategy" } type Mutations = { @@ -243,6 +263,8 @@ type Mutations = { [mutationTypes.showVvhGuides](state: State, showVvhGuides: boolean): void; [mutationTypes.showVvhMeters](state: State, showVvhMeters: boolean): void; [mutationTypes.toggleVvhScrolling](state: State, vvhScrolling: boolean): void; + [mutationTypes.setVvhView](state: State, vvhView: string): void; + [mutationTypes.setCPUsStrategy](state: State, CPUsStrategy: string[]): void; }; const mutations: Vuex.MutationTree & Mutations = { @@ -278,6 +300,10 @@ const mutations: Vuex.MutationTree & Mutations = { (state.app.options.showVvhMeters = showVvhMeters), toggleVvhScrolling: (state: State, vvhScrolling: boolean) => (state.app.options.vvhScrolling = vvhScrolling), + setVvhView: (state: State, vvhView: string) => + (state.app.vvhView = vvhView), + setCPUsStrategy: (state: State, CPUsStrategy: string[]) => + (state.app.CPUsStrategy = CPUsStrategy), }; type ActionContext = Omit, "commit"> & {