mirror of
https://github.com/danbulant/slightlyComplicatedTicTacToe
synced 2026-05-19 04:08:52 +00:00
parent
63d67c403e
commit
4fd808b0d3
4 changed files with 181 additions and 25 deletions
34
client/src/lib/Dialog.svelte
Normal file
34
client/src/lib/Dialog.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { fade, fly } from "svelte/transition";
|
||||
import { DEFAULT_TRANSITION_DURATION } from "./config";
|
||||
|
||||
export var visible: boolean = false;
|
||||
|
||||
const duration = DEFAULT_TRANSITION_DURATION;
|
||||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if(e.key === "Escape") visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="backdrop" transition:fade={{ duration: duration / 2 }} on:click|self={() => visible = false} on:keydown={keydown}>
|
||||
</div>
|
||||
<div class="dialog" in:fly={{ duration: duration / 1.5, y: 15 }} out:fly={{ duration, y: -15 }}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.backdrop {
|
||||
@apply fixed top-0 left-0 w-full h-full bg-black/50 z-50;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@apply bg-white border-1 border-black/70 border-solid rounded-lg p-8
|
||||
fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 max-h-80vh overflow-y-auto max-w-80vw;
|
||||
}
|
||||
:global(.dark) .dialog {
|
||||
@apply bg-black border-white/50;
|
||||
}
|
||||
</style>
|
||||
96
client/src/lib/RulesDialog.svelte
Normal file
96
client/src/lib/RulesDialog.svelte
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<script lang="ts">
|
||||
import Dialog from "./Dialog.svelte";
|
||||
import BackButton from "./backButton.svelte";
|
||||
import Game from "./game.svelte";
|
||||
|
||||
export var visible: boolean = false;
|
||||
|
||||
let vsm = typeof window !== "undefined" ? Math.min(window.innerWidth, window.innerHeight) / 2.3 : 256;
|
||||
|
||||
function backClicked(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog bind:visible>
|
||||
<div class="absolute top-0 left-0">
|
||||
<BackButton href="/" on:click={backClicked} />
|
||||
</div>
|
||||
<h1>Rules</h1>
|
||||
<p>How do I play the game?</p>
|
||||
<h2>You choose where your opponent will play</h2>
|
||||
<p>The board is divided into sub-boards, and you can always only play in one of them. Based on where you place your
|
||||
piece, the opponent will have to play in the corresponding sub-board.
|
||||
</p>
|
||||
<p class="desktop-only">On desktop, you can move your mouse over the field to see where the opponent "will be sent".</p>
|
||||
|
||||
<div class="flex" style:--vsm="{vsm*1.15}px">
|
||||
<div class="game-container" >
|
||||
<Game readonly showMoveList={false} autoCalculateState={false} innerWidthOverride={vsm} innerHeightOverride={vsm} />
|
||||
</div>
|
||||
<div class="game-container">
|
||||
<Game readonly showMoveList={false} autoCalculateState={false} innerWidthOverride={vsm} innerHeightOverride={vsm}
|
||||
moves={[{ p: 1, i: 4, j: 8 }]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>When you can't play in the target board, you'll stay in the current board. If you can't stay, you'll play in the board the previous move was made in.
|
||||
You can see move list on the right to see where you might get sent. Mouse preview shows this correctly.
|
||||
</p>
|
||||
<h2>Three in a row to win a board</h2>
|
||||
<p>Get 3 in a row/column/diagonally in a sub-board to with that sub-board. A larger symbol will be drawn.</p>
|
||||
|
||||
<div class="flex" style:--vsm="{vsm*1.15}px">
|
||||
<div class="game-container" >
|
||||
<Game readonly showMoveList={false} autoCalculateState={false} innerWidthOverride={vsm} innerHeightOverride={vsm}
|
||||
moves={[{ p: 1, i: 4, j: 6}, { p: 1, i: 4, j: 4}]}
|
||||
/>
|
||||
</div>
|
||||
<div class="game-container">
|
||||
<Game readonly showMoveList={false} autoCalculateState={false} innerWidthOverride={vsm} innerHeightOverride={vsm}
|
||||
moves={[{ p: 1, i: 4, j: 6}, { p: 1, i: 4, j: 4}]}
|
||||
containerStates={[0,0,0,0,1,0,0,0,0]}
|
||||
defaultHighlightedContainer={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Three boards in a row to win the game</h2>
|
||||
<p>Get 3 sub-boards in a row/column/diagonally to win the overall game.</p>
|
||||
|
||||
<div class="flex" style:--vsm="{vsm*1.15}px">
|
||||
<div class="game-container" >
|
||||
<Game readonly showMoveList={false} autoCalculateState={false} innerWidthOverride={vsm} innerHeightOverride={vsm}
|
||||
moves={[{ p: 1, i: 2, j: 6}, { p: 1, i: 2, j: 4}, { p: 1, i: 2, j: 2}]}
|
||||
containerStates={[0,0,0,0,1,0,1,0,0]}
|
||||
/>
|
||||
</div>
|
||||
<div class="game-container">
|
||||
<Game readonly showMoveList={false} autoCalculateState={false} innerWidthOverride={vsm} innerHeightOverride={vsm}
|
||||
moves={[]}
|
||||
containerStates={[0,0,1,0,1,0,1,0,0]}
|
||||
overallState={1}
|
||||
defaultHighlightedContainer={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<style lang="postcss">
|
||||
.flex {
|
||||
@apply flex -mx-5 gap-2;
|
||||
}
|
||||
.desktop-only {
|
||||
display: none;
|
||||
}
|
||||
@media (pointer: fine) {
|
||||
.desktop-only {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.game-container {
|
||||
width: var(--vsm);
|
||||
height: var(--vsm);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,6 +10,13 @@
|
|||
export var twoPlayer: boolean = false;
|
||||
export var selfName: string | null = null;
|
||||
export var opponentName: string | null = null;
|
||||
export var readonly: boolean = false;
|
||||
export var defaultHighlightedContainer: number | null = null;
|
||||
export var defaultHoveredPiece: { i: number, j: number } | null = null;
|
||||
export var autoCalculateState: boolean = true;
|
||||
export var showMoveList: boolean = true;
|
||||
export var innerWidthOverride: number | null = null;
|
||||
export var innerHeightOverride: number | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -25,11 +32,11 @@
|
|||
'bottom right'
|
||||
];
|
||||
|
||||
var hoveredPiece: null | { i: number, j: number } = null;
|
||||
var hoveredPiece: null | { i: number, j: number } = defaultHoveredPiece;
|
||||
|
||||
var highlightedContainer: null | number = null;
|
||||
var highlightedContainer: null | number = defaultHighlightedContainer;
|
||||
|
||||
$: highlightedContainer = hoveredPiece ? highlightContainerByPiece(hoveredPiece.j) : null;
|
||||
$: highlightedContainer = hoveredPiece ? highlightContainerByPiece(hoveredPiece.j) : defaultHighlightedContainer;
|
||||
|
||||
function highlightContainerByPiece(j: number) {
|
||||
if(!containerStates[j]) return j;
|
||||
|
|
@ -47,7 +54,7 @@
|
|||
var currentContainer: number = 4;
|
||||
var currentPlayer: 1 | 2 = 1;
|
||||
|
||||
var moves: { p: 1 | 2, i: number, j: number }[] = [];
|
||||
export var moves: { p: 1 | 2, i: number, j: number }[] = [];
|
||||
|
||||
$: currentContainer = getCurrentContainer(moves);
|
||||
$: currentPlayer = moves[moves.length - 1]?.p == 1 ? 2 : 1;
|
||||
|
|
@ -61,10 +68,11 @@
|
|||
return backtrack() ?? -1;
|
||||
}
|
||||
|
||||
let containerStates = new Array(9).fill(0);
|
||||
let overallState = 0;
|
||||
export let containerStates = new Array(9).fill(0);
|
||||
export let overallState = 0;
|
||||
|
||||
function addMove(i: number, j: number) {
|
||||
if(readonly) return;
|
||||
if(moves.find(move => move.i == i && move.j == j))
|
||||
return;
|
||||
if(currentContainer !== i) return;
|
||||
|
|
@ -97,6 +105,7 @@
|
|||
export { addPlayerMove };
|
||||
|
||||
function updateContainerStates() {
|
||||
if(!autoCalculateState) return;
|
||||
for(var i in containerStates) {
|
||||
if(containerStates[i]) continue;
|
||||
var containerMoves = moves.filter(move => move.i === Number(i));
|
||||
|
|
@ -153,6 +162,7 @@
|
|||
function reset() {
|
||||
moves = [];
|
||||
containerStates = new Array(9).fill(0);
|
||||
overallState = 0;
|
||||
}
|
||||
|
||||
function check(e: MouseEvent) {
|
||||
|
|
@ -186,15 +196,24 @@
|
|||
return () => clearTimeout(i);
|
||||
});
|
||||
|
||||
let innerWidth = typeof window !== "undefined" ? window.innerWidth : 0;
|
||||
let innerHeight = typeof window !== "undefined" ? window.innerHeight : 0;
|
||||
let winnerWidth = typeof window !== "undefined" ? window.innerWidth : 0;
|
||||
let winnerHeight = typeof window !== "undefined" ? window.innerHeight : 0;
|
||||
let innerWidth = innerWidthOverride || winnerWidth;
|
||||
let innerHeight = innerHeightOverride || winnerHeight;
|
||||
|
||||
$: innerWidth = innerWidthOverride || winnerWidth;
|
||||
$: innerHeight = innerHeightOverride || winnerHeight;
|
||||
|
||||
updateContainerStates();
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth bind:innerHeight on:click={() => hoveredPiece = null} />
|
||||
<svelte:window bind:innerWidth={winnerWidth} bind:innerHeight={winnerHeight} on:click={() => hoveredPiece = defaultHoveredPiece} />
|
||||
|
||||
{#if !readonly}
|
||||
<BackButton href="/" on:click={check}/>
|
||||
{/if}
|
||||
|
||||
{#if !twoPlayer}
|
||||
{#if !twoPlayer && !readonly}
|
||||
<!-- I have no idea why x is inverted here -->
|
||||
<div transition:fly={{ duration, delay: duration, x: 120, opacity: 0 }} on:click={reset} on:keydown={reset} class="reload fixed top-0 left-10 w-4 h-4 m-4 p-2 transform transition-transform rotate-180 hover:rotate-360 active:rotate-540">
|
||||
<svg fill="currentColor" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
|
|
@ -210,7 +229,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if innerWidth < 1024}
|
||||
{#if innerWidth < 1024 && showMoveList}
|
||||
<div transition:fly={{ duration, delay: 0, x: 60, opacity: 0 }} class="fixed top-0 right-0 w-4 h-4 m-4 p-2 menu cursor-pointer" on:click={() => movesShown = !movesShown} on:keydown={() => movesShown = !movesShown}>
|
||||
<svg width=16 height=16>
|
||||
<line y1="2" y2="2" x1="0" x2="100%" stroke="currentColor" stroke-width="2" />
|
||||
|
|
@ -224,7 +243,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<main class:disabled={overallState} class="flex flex-wrap h-100vh w-100vw overflow-hidden items-center" style:--vw={innerWidth} style:--vh={innerHeight} style:--vmin={Math.min(innerWidth, innerHeight)}>
|
||||
<main class:disabled={overallState} class:fullsize={!innerHeightOverride && !innerWidthOverride} class="flex flex-wrap overflow-hidden items-center" style:--vw={innerWidth} style:--vh={innerHeight} style:--vmin={Math.min(innerWidth, innerHeight)}>
|
||||
<div class="board relative p-8" style:translate="{Math.min(0, (1 - innerWidth / 768) * -50)}% {Math.min(0, (1 - innerHeight / 856) * -50)}%">
|
||||
{#each classes as className, i}
|
||||
<div
|
||||
|
|
@ -249,8 +268,8 @@
|
|||
class:cross={move && move.p==1}
|
||||
class:circle={move && move.p==2}
|
||||
on:mouseover={() => { if(currentContainer == i) hoveredPiece = { i, j } }}
|
||||
|
||||
on:mouseleave={() => { if(hoveredPiece?.i == i && hoveredPiece.j == j) hoveredPiece = null; }}
|
||||
|
||||
on:mouseleave={() => { if(hoveredPiece?.i == i && hoveredPiece.j == j) hoveredPiece = defaultHoveredPiece; }}
|
||||
>
|
||||
<!-- Focus breaks phones -->
|
||||
<!-- on:focus={() => { if(currentContainer == i) hoveredPiece = { i, j }}} -->
|
||||
|
|
@ -367,8 +386,8 @@
|
|||
{/key}
|
||||
</div>
|
||||
|
||||
|
||||
{#if movesShown || innerWidth >= 1024 || innerWidth / innerHeight > 1.4}
|
||||
|
||||
{#if showMoveList && (movesShown || innerWidth >= 1024 || innerWidth / innerHeight > 1.4)}
|
||||
<div transition:fade={{ duration }} class:hidden={innerWidth / innerHeight > 1.4} class="lg:hidden bg-black/40 fixed inset-0 z-10" on:click={() => movesShown = false} on:keydown={() => movesShown = false} />
|
||||
|
||||
<div transition:fly={{ delay: duration * moveDelayMultiplier * 3, duration, x: 160, opacity: 0 }} class="info z-11 min-w-38 px-4 flex-grow-0 flex-shrink overflow-y-auto h-100vh lt-lg:(absolute top-0 right-0 bg-black)">
|
||||
|
|
@ -395,7 +414,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#each moves as move}
|
||||
<Move player={move.p} board={move.i} piece={move.j} on:mouseover={() => hoveredPiece = { i: move.i, j: move.j }} on:mouseout={() => { if(hoveredPiece?.j == move.j && hoveredPiece.i == move.i) hoveredPiece = null }} />
|
||||
<Move player={move.p} board={move.i} piece={move.j} on:mouseover={() => hoveredPiece = { i: move.i, j: move.j }} on:mouseout={() => { if(hoveredPiece?.j == move.j && hoveredPiece.i == move.i) hoveredPiece = defaultHoveredPiece }} />
|
||||
{/each}
|
||||
<Move latest player={currentPlayer} board={hoveredPiece?.i ?? "?"} piece={hoveredPiece?.j ?? "?"} />
|
||||
</div>
|
||||
|
|
@ -404,8 +423,10 @@
|
|||
</main>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
.fullsize {
|
||||
@apply h-100vh w-100vw;
|
||||
}
|
||||
.info .moves {
|
||||
@apply p-4 font-mono flex flex-col flex-wrap max-w-185 m-auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import DarkmodeIcon from "$lib/DarkmodeIcon.svelte";
|
||||
import RulesDialog from "$lib/RulesDialog.svelte";
|
||||
import { themeStore } from "$lib/themeStore";
|
||||
import { onMount } from "svelte";
|
||||
import { quadOut } from "svelte/easing";
|
||||
|
|
@ -20,6 +21,8 @@
|
|||
$themeStore = "dark";
|
||||
}
|
||||
}
|
||||
|
||||
let rulesVisible = false;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -44,7 +47,7 @@
|
|||
<p>Play with 2 devices, even across the ocean.</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="rules" in:fly={{ delay: duration, duration, opacity: 0, y: 100, easing: quadOut }}>
|
||||
<div class="rules" on:click={() => rulesVisible = true} on:keydown={() => {}} in:fly={{ delay: duration, duration, opacity: 0, y: 100, easing: quadOut }}>
|
||||
<div class="icon">
|
||||
<svg fill="currentColor" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 973.1 973.1" xml:space="preserve"
|
||||
|
|
@ -75,7 +78,9 @@
|
|||
</main>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
<RulesDialog bind:visible={rulesVisible} />
|
||||
|
||||
<style lang="postcss">
|
||||
.computer {
|
||||
@apply w-full bg-black;
|
||||
aspect-ratio: 1/1;
|
||||
|
|
@ -130,13 +135,13 @@
|
|||
@apply bg-white/10;
|
||||
}
|
||||
.rules {
|
||||
@apply cursor-not-allowed text-gray-500 flex justify-center items-center w-full my-8 p-4 border rounded-lg border-gray-400 border-solid;
|
||||
@apply cursor-pointer flex justify-center items-center w-full my-8 p-4 border rounded-lg border-gray-400 border-solid transition-none;
|
||||
}
|
||||
.rules:hover {
|
||||
@apply bg-red-500/3;
|
||||
@apply bg-black/10 duration-150;
|
||||
}
|
||||
.rules:active {
|
||||
@apply bg-red-500/10;
|
||||
:global(.dark) .rules:hover {
|
||||
@apply bg-white/10;
|
||||
}
|
||||
.icon {
|
||||
@apply h-20 w-20 mr-4;
|
||||
|
|
|
|||
Loading…
Reference in a new issue