Skip to content
Draft
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
84 changes: 65 additions & 19 deletions src/components/qwixx/NumbersInput.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<script lang="ts">
import { createArray } from '../../utils/array';
import { t } from '../../utils/i18n';
import { Direction, type ColorConfig } from '../../lib/qwixx/types';
import { NUMBERS } from '../../lib/qwixx/constants';
import { isColorLocked } from '../../lib/qwixx/game';

import Lock from './Lock.svelte';
import LockOpen from './LockOpen.svelte';
import { createArray } from "../../utils/array";
import { t } from "../../utils/i18n";
import { Direction, type ColorConfig } from "../../lib/qwixx/types";
import { NUMBERS } from "../../lib/qwixx/constants";
import { isColorCompleted } from "../../lib/qwixx/game";
import Lock from "./Lock.svelte";
import LockOpen from "./LockOpen.svelte";

interface Props {
color: ColorConfig;
numbers: number[];
isLocked: boolean;
toggleNumber: (color: ColorConfig, number: number) => void;
toggleLocked: (color: ColorConfig) => void;
}

const { color, numbers, toggleNumber }: Props = $props();
const { color, numbers, isLocked, toggleNumber, toggleLocked }: Props =
$props();

const { key, direction, style } = color;

Expand All @@ -25,7 +27,7 @@
case Direction.DESCENDING:
return NUMBERS + 1 - index;
default:
throw new Error('Unreachable code')
throw new Error("Unreachable code");
}
});
const lastNumber = numberRange[numberRange.length - 1] as number;
Expand All @@ -34,7 +36,7 @@
const max = $derived(Math.max(...numbers));
const length = $derived(numbers.length);

const isLocked = isColorLocked(numbers, color);
const isCompleted = $derived(isColorCompleted(numbers, color));

const isDisabled = $derived((number: number) => {
switch (direction) {
Expand All @@ -48,25 +50,59 @@
const onClick = $derived(
(event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
const { value, ariaDisabled } = event.currentTarget;
if (ariaDisabled === 'true') {
if (ariaDisabled === "true") {
event.preventDefault();
return;
}
const number = Number.parseInt(value, 10);
toggleNumber(color, number);
},
);

let clickTimerId: ReturnType<typeof setTimeout> | null = null;

const onLockClick = $derived(
(event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();

if (!clickTimerId) {
// Single click
const { value, ariaDisabled } = event.currentTarget;
clickTimerId = setTimeout(() => {
clickTimerId = null;
if (ariaDisabled !== "true") {
const number = Number.parseInt(value, 10);
toggleNumber(color, number);
}
}, 300);
} else {
// FIXME: There's something wrong on mobile
// Double click
clearTimeout(clickTimerId);
clickTimerId = null;
if (!isCompleted) {
toggleLocked(color);
}
}
},
);

const checked = $derived(isCompleted || isLocked);

$effect(() => {
console.log(color.key, isCompleted, isLocked, checked);
});
</script>

<fieldset {style} class:locked={isLocked}>
<fieldset {style} class:locked={isCompleted || isLocked}>
<legend class="hide-visually">{t.qwixx.colors[key]}</legend>
{#each numberRange as number}
<input
id={`${key}-${number}`}
type="checkbox"
name={key}
checked={numbers.includes(number)}
aria-disabled={isDisabled(number)}
aria-disabled={isLocked || isDisabled(number)}
value={number}
onclick={onClick}
class="hide-visually"
Expand All @@ -77,11 +113,12 @@
id={`${key}-lock`}
type="checkbox"
name={key}
checked={numbers.includes(lastNumber)}
{checked}
aria-disabled={isDisabled(lastNumber)}
value={lastNumber}
onclick={onClick}
onclick={onLockClick}
class="hide-visually lock"
class:locked={isLocked}
/>
<label for={`${key}-lock`}>
<Lock class="icon-lock" />
Expand Down Expand Up @@ -118,6 +155,7 @@
background-color var(--transition-micro),
opacity var(--transition-micro);
cursor: pointer;
touch-action: manipulation;
}

@media (prefers-color-scheme: dark) {
Expand All @@ -126,7 +164,7 @@
}
}

input[aria-disabled='true'] + label {
input[aria-disabled="true"] + label {
cursor: not-allowed;
}

Expand All @@ -145,18 +183,26 @@
opacity: 0.5;
}

input[aria-disabled='true']:not(:checked) + label {
input[aria-disabled="true"]:not(:checked) + label {
opacity: 0.5;
}

input:nth-last-of-type(-n + 2)[aria-disabled='true'] + label {
input:nth-last-of-type(-n + 2)[aria-disabled="true"] + label {
opacity: 0.5;
}

input.lock + label {
border-radius: 100%;
}

input.lock.locked + label {
color: hsl(var(--hue) var(--saturation) calc(var(--lightness) - 15%));
background-color: hsl(
var(--hue) calc(var(--saturation) - 10%) calc(var(--lightness) + 15%) /
0.2
);
}

input.lock:checked + label :global(.icon-lock-open) {
display: none;
}
Expand Down
58 changes: 39 additions & 19 deletions src/components/qwixx/PlayerSheet.svelte
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
<script lang="ts">
import { onMount } from 'svelte';
import { persisted } from 'svelte-persisted-store';

import type { ColorConfig, GameState } from '../../lib/qwixx/types.ts';
import { t } from '../../utils/i18n.ts';
import { stack, getPoints } from '../../lib/qwixx/game.ts';
import { COLORS } from '../../lib/qwixx/constants.ts';
import NumbersInput from './NumbersInput.svelte';
import PointsTable from './PointsTable.svelte';
import PenaltiesInput from './PenaltiesInput.svelte';
import ScoreEquation from './ScoreEquation.svelte';
import Undo from './Undo.svelte';
import Redo from './Redo.svelte';
import { onMount } from "svelte";
import { persisted } from "svelte-persisted-store";

import type { ColorConfig, GameState } from "../../lib/qwixx/types.ts";
import { t } from "../../utils/i18n.ts";
import { stack, getPoints } from "../../lib/qwixx/game.ts";
import { COLORS } from "../../lib/qwixx/constants.ts";
import NumbersInput from "./NumbersInput.svelte";
import PointsTable from "./PointsTable.svelte";
import PenaltiesInput from "./PenaltiesInput.svelte";
import ScoreEquation from "./ScoreEquation.svelte";
import Undo from "./Undo.svelte";
import Redo from "./Redo.svelte";

const initialState: GameState = {
red: [],
yellow: [],
green: [],
blue: [],
locked: {},
penalties: 0,
};

const game = stack(persisted('qwixx-state', initialState));
const game = stack(persisted("qwixx-state", initialState));
const { undo, redo, canUndo, canRedo } = game;

function reset() {
Expand Down Expand Up @@ -54,24 +55,37 @@
});
}

function toggleLocked(color: ColorConfig) {
const { key } = color;
game.update((state) => {
return {
...state,
locked: {
...state.locked,
[key]: !state.locked[key],
},
};
});
}

const points = getPoints(game);

onMount(() => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.metaKey && event.key === 'z') {
if (event.metaKey && event.key === "z") {
event.preventDefault();
undo();
}
if (event.metaKey && event.key === 'y') {
if (event.metaKey && event.key === "y") {
event.preventDefault();
redo();
}
};

document.addEventListener('keydown', handleKeydown);
document.addEventListener("keydown", handleKeydown);

return () => {
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener("keydown", handleKeydown);
};
});
</script>
Expand Down Expand Up @@ -111,7 +125,13 @@

<div class="numbers">
{#each COLORS as color (color.key)}
<NumbersInput {color} numbers={$game[color.key]} {toggleNumber} />
<NumbersInput
{color}
numbers={$game[color.key]}
isLocked={Boolean($game.locked[color.key])}
{toggleNumber}
{toggleLocked}
/>
{/each}
</div>
</section>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/qwixx/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function getPoints(state: Readable<GameState>) {
});
}

export function isColorLocked(numbers: number[], color: ColorConfig) {
export function isColorCompleted(numbers: number[], color: ColorConfig) {
const { direction } = color;
return (
(direction === Direction.ASCENDING && numbers.includes(12)) ||
Expand All @@ -86,7 +86,7 @@ function getColorPoints(state: GameState, color: ColorConfig) {
const numbers = state[color.key];
let crosses = numbers.length;

if (isColorLocked(numbers, color)) {
if (isColorCompleted(numbers, color)) {
crosses += 1;
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/qwixx/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type GameState = {
yellow: number[];
green: number[];
blue: number[];
locked: { [key in Color]?: boolean };
penalties: number;
};

Expand Down
Loading