Skip to content

Commit

Permalink
Add JSON import
Browse files Browse the repository at this point in the history
  • Loading branch information
dylan-chong committed Oct 1, 2023
1 parent 7a9054b commit b652ca6
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 43 deletions.
4 changes: 4 additions & 0 deletions src/components/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
</div>
</div>

<div v-show="store.sideView === 'hand-importer'">
<HandImporter />
</div>
<div v-show="store.sideView === 'oop-range'">
<RangeEditor :player="0" />
</div>
Expand Down Expand Up @@ -70,6 +73,7 @@ import BunchingEffect from "./BunchingEffect.vue";
import RunSolver from "./RunSolver.vue";
import AboutPage from "./AboutPage.vue";
import ResultViewer from "./ResultViewer.vue";
import HandImporter from "./HandImporter.vue";
const store = useStore();
const header = computed(() => store.headers[store.sideView].join(" > "));
Expand Down
19 changes: 9 additions & 10 deletions src/components/BoardSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,21 @@
</template>

<script setup lang="ts">
import { ref } from "vue";
import { ref, watch } from "vue";
import { useConfigStore } from "../store";
import { cardText, parseCardString } from "../utils";
import BoardSelectorCard from "./BoardSelectorCard.vue";
const config = useConfigStore();
const boardText = ref("");
const toggleCard = (cardId: number, updateText = true) => {
watch(
() => config.board,
() => setBoardTextFromButtons(),
{ deep: true }
);
const toggleCard = (cardId: number) => {
if (config.board.includes(cardId)) {
config.board = config.board.filter((card) => card !== cardId);
} else if (config.board.length < 5) {
Expand All @@ -55,10 +61,6 @@ const toggleCard = (cardId: number, updateText = true) => {
config.board.sort((a, b) => b - a);
}
}
if (updateText) {
setBoardTextFromButtons();
}
};
const setBoardTextFromButtons = () => {
Expand All @@ -80,13 +82,11 @@ const onBoardTextChange = () => {
.map(parseCardString)
.filter((cardId): cardId is number => cardId !== null);
new Set(cardIds).forEach((cardId) => toggleCard(cardId, false));
setBoardTextFromButtons();
new Set(cardIds).forEach((cardId) => toggleCard(cardId));
};
const clearBoard = () => {
config.board = [];
setBoardTextFromButtons();
};
const generateRandomBoard = () => {
Expand All @@ -100,6 +100,5 @@ const generateRandomBoard = () => {
}
config.board.sort((a, b) => b - a);
setBoardTextFromButtons();
};
</script>
3 changes: 2 additions & 1 deletion src/components/BunchingEffect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@
<script setup lang="ts">
import { ref } from "vue";
import { useStore, useConfigStore } from "../store";
import { trimRegex, rangeRegex, cardText } from "../utils";
import { cardText } from "../utils";
import { trimRegex, rangeRegex } from "../range-utils";
import * as invokes from "../invokes";
import RangeEditor from "./RangeEditor.vue";
Expand Down
195 changes: 195 additions & 0 deletions src/components/HandImporter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<template>
<div class="mt-1 w-[52rem]">
<textarea
v-model="importText"
class="block w-full h-[36rem] px-2 py-1 rounded-lg textarea text-sm font-mono"
@change="onImportTextChanged"
spellcheck="false"
/>

<div class="w-full">
<button class="button-base button-blue mt-3 mr-3" @click="importHand">
Import
</button>
<span v-if="importDoneText" class="mt-1">{{ importDoneText }}</span>
<span v-if="importTextError" class="mt-1 text-red-500">
{{ 'Error: ' + importTextError }}
</span>
</div>

<div class="mt-2">
Note: If the import has missing values, the current value will be used.
</div>
</div>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
import { Config, configKeys, useConfigStore, useStore } from "../store";
import { cardText, parseCardString, Position, Validation } from "../utils";
import { setRange, validateRange } from "../range-utils";
import * as invokes from "../invokes";
type NonValidatedHandJSON = Record<string, any>;
type HandJSON = {
oopRange: string;
ipRange: string;
config: Omit<Config, "board"> & { board: string[] };
};
const INVALID_BOARD_ERROR = `Invalid '.config.board'. Set board cards manually for an example.`;
const config = useConfigStore();
const store = useStore();
const importText = ref("{\n \n}");
const importTextError = ref("");
const importDoneText = ref("");
watch(
() => store.ranges,
() => generateImportText(),
{ deep: true }
);
watch(
() => Object.values(config),
() => generateImportText(),
{ deep: true }
);
const generateImportText = async () => {
const configObj = Object.fromEntries(
Object.entries(config).filter(
([key, _value]) => configKeys.indexOf(key) !== -1
)
);
const importObj = {
oopRange: await invokes.rangeToString(Position.OOP),
ipRange: await invokes.rangeToString(Position.IP),
config: {
...configObj,
board: config.board.map((cardId) => {
const { rank, suitLetter } = cardText(cardId);
return rank + suitLetter;
}),
},
};
importText.value = JSON.stringify(importObj, null, 2);
};
const onImportTextChanged = () => {
importDoneText.value = "";
validateImportTextAndDisplayError();
};
const validateConfigPrimitives = (importConfig: any): Validation => {
if (typeof importConfig !== "object")
return {
success: false,
error: `Expected '.config' to be an object but got ${typeof importConfig}`,
};
for (const key of configKeys) {
const newValue = importConfig[key];
const existingValue = (config as any)[key];
if (existingValue === null || existingValue === undefined) continue;
if (typeof existingValue === typeof newValue) continue;
if (key === "board") return { success: false, error: INVALID_BOARD_ERROR };
else
return {
success: false,
error: `Expected '.config.${key}' to be ${typeof importConfig} but got ${typeof newValue}`,
};
}
return { success: true };
};
const validateBoard = (board: any): Validation => {
if (!Array.isArray(board))
return { success: false, error: INVALID_BOARD_ERROR };
const boardArr = board as any[];
for (const i in boardArr) {
const card = boardArr[i];
if (typeof card !== "string") {
return { success: false, error: INVALID_BOARD_ERROR };
}
const parsedCard = parseCardString(card);
if (parsedCard === null) {
return { success: false, error: INVALID_BOARD_ERROR };
}
}
return { success: true };
};
const parseJson = (
json: string
): Validation<{ json?: NonValidatedHandJSON }> => {
try {
return { success: true, json: JSON.parse(json) };
} catch (e) {
const message = (e as Error).message;
return { success: false, error: message };
}
};
const validateImportTextAndDisplayError = (): Validation<{
json?: HandJSON;
}> => {
importTextError.value = "";
const validation = validateImportText();
if (validation.success) return validation;
importTextError.value = validation.error as string;
return validation;
};
const validateImportText = (): Validation<{ json?: HandJSON }> => {
const parseValidation = parseJson(importText.value);
if (!parseValidation.success) return parseValidation;
const importJson = parseValidation.json;
if (typeof importJson !== "object")
return { success: false, error: "Not a valid JSON object" };
const validateFns: (() => Validation)[] = [
() => validateConfigPrimitives(config),
() => validateBoard(importJson.config?.board),
() => validateRange(importJson.oopRange, "oopRange"),
() => validateRange(importJson.ipRange, "ipRange"),
];
for (const validate of validateFns) {
const validation = validate();
if (!validation.success) return validation;
}
importJson.config.board = importJson.config.board.map(parseCardString);
return { success: true, json: importJson as HandJSON };
};
const importHand = async () => {
const validation = validateImportTextAndDisplayError();
if (!validation.success) return;
const importJson = validation.json as HandJSON;
for (const key in importJson.config) {
const newValue = (importJson.config as Record<string, any>)[key];
(config as any)[key] = newValue;
}
await setRange(Position.OOP, importJson.oopRange, store);
await setRange(Position.IP, importJson.ipRange, store);
importDoneText.value = "Done!";
};
generateImportText();
</script>
48 changes: 20 additions & 28 deletions src/components/RangeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,10 @@
</template>

<script setup lang="ts">
import { ref } from "vue";
import { ref, watch } from "vue";
import { useStore } from "../store";
import { ranks, trimRegex, rangeRegex } from "../utils";
import { Position, ranks } from "../utils";
import { setRange, validateRange } from "../range-utils";
import * as invokes from "../invokes";
import DbItemPicker from "./DbItemPicker.vue";
Expand All @@ -147,7 +148,7 @@ const yellow500 = "#eab308";
type DraggingMode = "none" | "enabling" | "disabling";
const props = withDefaults(
defineProps<{ player: number; defaultText?: string }>(),
defineProps<{ player: Position; defaultText?: string }>(),
{ defaultText: "" }
);
Expand All @@ -164,6 +165,12 @@ const numCombos = ref(0);
let draggingMode: DraggingMode = "none";
watch(
() => store.ranges,
() => onUpdate(),
{ deep: true }
);
const cellText = (row: number, col: number) => {
const r1 = 13 - Math.min(row, col);
const r2 = 13 - Math.max(row, col);
Expand All @@ -188,36 +195,22 @@ const update = async (row: number, col: number, weight: number) => {
const idx = 13 * (row - 1) + col - 1;
await invokes.rangeUpdate(props.player, row, col, weight / 100);
store.ranges[props.player][idx] = weight;
await onUpdate();
};
const onRangeTextChange = async () => {
const trimmed = rangeText.value.replace(trimRegex, "$1").trim();
const ranges = trimmed.split(",");
if (ranges[ranges.length - 1] === "") {
ranges.pop();
const validation = validateRange(rangeText.value);
if (!validation.success) {
rangeTextError.value = `Failed to parse range: ${validation.error}`;
}
for (const range of ranges) {
if (!rangeRegex.test(range)) {
rangeTextError.value = `Failed to parse range: ${
range || "(empty string)"
}`;
return;
}
}
const errorString = await invokes.rangeFromString(props.player, trimmed);
const assignmentValidation = await setRange(
props.player,
rangeText.value,
store
);
if (errorString) {
rangeTextError.value = errorString;
} else {
const weights = await invokes.rangeGetWeights(props.player);
for (let i = 0; i < 13 * 13; ++i) {
store.ranges[props.player][i] = weights[i] * 100;
}
await onUpdate();
if (!assignmentValidation.success) {
rangeTextError.value = assignmentValidation.error;
}
};
Expand Down Expand Up @@ -263,7 +256,6 @@ const invertRange = async () => {
for (let i = 0; i < 13 * 13; ++i) {
store.ranges[props.player][i] = 100 - store.ranges[props.player][i];
}
await onUpdate();
};
const loadRange = (rangeStr: unknown) => {
Expand Down
Loading

0 comments on commit b652ca6

Please sign in to comment.