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

Variant demo #249

Merged
merged 8 commits into from
Apr 28, 2024
Merged
16 changes: 15 additions & 1 deletion packages/vue-client/src/components/GameView/SeatComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
IPerPlayerTimeControlBase,
ITimeControlConfig,
} from "@ogfcommunity/variants-shared";
import { computed, getCurrentInstance } from "vue";
import GameTimer from "../GameTimer.vue";

defineProps<{
Expand All @@ -15,6 +16,16 @@ defineProps<{
time_config?: ITimeControlConfig;
is_players_turn: boolean;
}>();

defineEmits<{
(event: "select"): void;
(event: "sit"): void;
(event: "leave"): void;
}>();
const hasLeaveCallback = computed(
// https://stackoverflow.com/a/76208995/5001502
() => !!getCurrentInstance()?.vnode.props?.onLeave,
);
</script>

<template>
Expand Down Expand Up @@ -45,7 +56,10 @@ defineProps<{
v-bind:time_control="time_control"
v-bind:time_config="time_config"
/>
<button v-if="occupant.id === user_id" @click.stop="$emit('leave')">
<button
v-if="hasLeaveCallback && occupant.id === user_id"
@click.stop="$emit('leave')"
>
Leave Seat
</button>
</div>
Expand Down
6 changes: 6 additions & 0 deletions packages/vue-client/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const router = createRouter({
component: () => import("../views/GameView.vue"),
props: true,
},
{
path: "/variants/:variant/demo",
name: "demo",
component: () => import("../views/VariantDemoView.vue"),
props: true,
},
{
path: "/components",
name: "components",
Expand Down
209 changes: 209 additions & 0 deletions packages/vue-client/src/views/VariantDemoView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<script setup lang="ts">
import {
makeGameObject,
getOnlyMove,
getDefaultConfig,
} from "@ogfcommunity/variants-shared";
import SeatComponent from "@/components/GameView/SeatComponent.vue";
import { useCurrentUser } from "../stores/user";
import { computed, reactive, ref, type Ref } from "vue";
import { board_map } from "@/board_map";
import { variant_short_description_map } from "../components/variant_descriptions/variant_description.consts";
import type { MovesType } from "@ogfcommunity/variants-shared";
import NavButtons from "@/components/GameView/NavButtons.vue";
import PlayersToMove from "@/components/GameView/PlayersToMove.vue";
import { config_form_map } from "@/config_form_map";

const props = defineProps<{ variant: string }>();

// null <-> viewing the latest round
// while viewing history of game, maybe we should prevent player from making a move (accidentally)
const view_round: Ref<number | null> = ref(null);

const config = ref(getDefaultConfig(props.variant));
const variantConfigForm = computed(() => config_form_map[props.variant]);
const moves = reactive<Array<MovesType>>([]);
const playing_as = ref<undefined | number>(undefined);
const setPlayingAs = (seat: number) => {
if (playing_as.value === seat) {
playing_as.value = undefined;
return;
}
playing_as.value = seat;
};
const game = computed(() =>
getGame(
props.variant,
config.value,
moves,
view_round.value,
playing_as.value,
),
);

function getGame(
variant: string,
config: unknown,
moves: Array<MovesType>,
viewRound: number | null,
playingAs?: number,
) {
const game_obj = makeGameObject(variant, config);

let state: unknown = null;
for (let i = 0; i < moves.length; i++) {
if (viewRound !== null && state === null && game_obj.round === viewRound) {
state = structuredClone(game_obj.exportState(playingAs));
}

const { player, move } = getOnlyMove(moves[i]);
game_obj.playMove(player, move);
}
const result =
game_obj.phase === "gameover" ? game_obj.result || "Game over" : null;

if (state === null) {
state = game_obj.exportState(playingAs);
}
return {
result,
state,
round: game_obj.round,
next_to_play: game_obj.nextToPlay(),
numPlayers: game_obj.numPlayers(),
};
}

const specialMoves = computed(() =>
!props.variant || !config.value
? {}
: makeGameObject(props.variant, config.value).specialMoves(),
);
const variantGameView = computed(() => board_map[props.variant]);
const variantDescriptionShort = computed(
() => variant_short_description_map[props.variant] ?? "",
);

const user = useCurrentUser();

function makeMove(move_str: string) {
const move: { [player: number]: string } = {};

if (playing_as.value === undefined) {
alert("No player selected. Click one of your seats to play a move.");
return;
}

if (view_round.value !== null) {
alert("Can't play moves while viewing previous round.");
return;
}

move[playing_as.value] = move_str;

try {
// Validate the move before pushing onto stack
getGame(
props.variant,
config.value,
moves.concat([move]),
view_round.value,
playing_as.value,
);
moves.push(move);
} catch (e) {
if (e instanceof Error) {
alert(e.message);
}
}
}

function onConfigChange(c: object) {
config.value = { ...c };
// Changing the config most likely invalidates the moves
// so we restart the game
moves.length = 0;
}
</script>

<template>
<div>
<component
v-if="variantGameView && game.state"
v-bind:is="variantGameView"
v-bind:gamestate="game.state"
v-bind:config="config"
v-on:move="makeMove"
/>
<NavButtons v-model="view_round" :gameRound="game.round" />

<div id="variant-info">
<div>
<span class="info-label">Variant:</span>
<span class="info-attribute">
{{ props.variant ?? "unknown" }}
</span>
</div>

<div>
<span class="info-label">Description:</span>
<span class="info-attribute">{{ variantDescriptionShort }}</span>
</div>
</div>
</div>
<div className="seat-list">
<div v-for="(_, idx) in game.numPlayers" :key="idx">
<SeatComponent
:user_id="user?.id"
:occupant="user || undefined"
:player_n="idx"
@select="setPlayingAs(idx)"
:selected="playing_as"
:time_control="null"
:is_players_turn="game.next_to_play?.includes(idx) ?? false"
/>
</div>

<div>
<button
v-for="(value, key) in specialMoves"
:key="key"
@click="makeMove(key as string)"
>
{{ value }}
</button>
</div>

<PlayersToMove :next-to-play="game.next_to_play" />

<component
v-bind:is="variantConfigForm"
v-bind:initialConfig="getDefaultConfig(variant)"
v-on:configChanged="onConfigChange"
/>
</div>

<div v-if="game.result" style="font-weight: bold; font-size: 24pt">
Result: {{ game.result }}
</div>
</template>

<style scoped>
.seat-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: flex-start;
column-gap: 20px;
}

@media (min-width: 664px) {
.board {
width: var(--board-side-length);
height: var(--board-side-length);
}
.seat-list {
max-height: var(--board-side-length);
}
}
</style>
Loading