Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add json import #33

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, trimRegex } from "../utils";
import { rangeRegex } from "../range-utils";
import * as invokes from "../invokes";

import RangeEditor from "./RangeEditor.vue";
Expand Down
223 changes: 223 additions & 0 deletions src/components/HandImporter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<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 ' +
(importTextError ? 'textarea-error' : '')
"
@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,
getExpectedBoardLength,
parseCardString,
Position,
Result,
} from "../utils";
import { setRange, validateRange } from "../range-utils";
import * as invokes from "../invokes";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NonValidatedHandJSON = 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_INPUT_KEYS = configKeys.filter(
(k) => ["expectedBoardLength"].indexOf(k) === -1
);

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]) => CONFIG_INPUT_KEYS.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);
importTextError.value = "";
importDoneText.value = "";
};

const onImportTextChanged = () => {
importDoneText.value = "";
validateImportTextAndDisplayError();
};

const validateConfigPrimitives = (importConfig: unknown): Result => {
if (typeof importConfig !== "object" || !importConfig)
return {
success: false,
error: `Expected '.config' to be an object but got ${typeof importConfig}`,
};

for (const key of CONFIG_INPUT_KEYS.concat(Object.keys(importConfig))) {
const newValue = (importConfig as Record<string, unknown>)[key];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const existingValue = (config as unknown as Record<string, unknown>)[key];

if (existingValue === undefined) {
return { success: false, error: `Unexpected key '.config.${key}'` };
}

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 existingValue} but got ${typeof newValue}`,
};
}

return { success: true };
};

const validateBoard = (board: unknown): Result => {
if (!Array.isArray(board))
return { success: false, error: INVALID_BOARD_ERROR };

for (const i in board) {
const card = board[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 };
}
}

if (board.length > 5) {
return { success: false, error: "Board cannot have more than 5 cards" };
}

return { success: true };
};

const parseJson = (json: string): Result<{ 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 = (): Result<{
json?: HandJSON;
}> => {
importTextError.value = "";

const validation = validateImportText();
if (validation.success) return validation;

importTextError.value = validation.error as string;
return validation;
};

const validateImportText = (): Result<{ 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: (() => Result)[] = [
() => validateConfigPrimitives(importJson.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;
}

const cfg = importJson.config;
cfg.board = cfg.board.map(parseCardString);
cfg.expectedBoardLength = getExpectedBoardLength(
cfg.board.length,
cfg.addedLines,
cfg.removedLines
);

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, unknown>)[key];
(config as unknown as Record<string, unknown>)[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