WIP: Add multiplayer Snake game
N-player (2-8) real-time Snake game alongside Connect 4. Lobby has tabs to switch between games. Players join via invite link with 10-second countdown. Game loop runs at tick-based intervals with NATS pub/sub for state sync. Keyboard input not yet working (Datastar keydown binding issue still under investigation).
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
@@ -179,6 +180,93 @@
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.tab {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
webkit-user-select: none;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
}
|
||||
--tab-p: 0.75rem;
|
||||
--tab-bg: var(--color-base-100);
|
||||
--tab-border-color: var(--color-base-300);
|
||||
--tab-radius-ss: 0;
|
||||
--tab-radius-se: 0;
|
||||
--tab-radius-es: 0;
|
||||
--tab-radius-ee: 0;
|
||||
--tab-order: 0;
|
||||
--tab-radius-min: calc(0.75rem - var(--border));
|
||||
--tab-radius-limit: min(var(--radius-field), var(--tab-radius-min));
|
||||
--tab-radius-grad: #0000 calc(69% - var(--border)),
|
||||
var(--tab-border-color) calc(calc(69% - var(--border)) + 0.25px),
|
||||
var(--tab-border-color) calc(calc(69% - var(--border)) + var(--border)),
|
||||
var(--tab-bg) calc(calc(69% - var(--border)) + var(--border) + 0.25px);
|
||||
border-color: #0000;
|
||||
order: var(--tab-order);
|
||||
height: var(--tab-height);
|
||||
font-size: 0.875rem;
|
||||
padding-inline-start: var(--tab-p);
|
||||
padding-inline-end: var(--tab-p);
|
||||
&:is(input[type="radio"]) {
|
||||
min-width: fit-content;
|
||||
&:after {
|
||||
--tw-content: attr(aria-label);
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
&:is(label) {
|
||||
position: relative;
|
||||
input {
|
||||
position: absolute;
|
||||
inset: calc(0.25rem * 0);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
&:checked, &:is(label:has(:checked)), &:is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]) {
|
||||
& + .tab-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:not( :checked, label:has(:checked), :hover, .tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"] ) {
|
||||
color: var(--color-base-content);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
||||
}
|
||||
}
|
||||
&:not(input):empty {
|
||||
flex-grow: 1;
|
||||
cursor: default;
|
||||
}
|
||||
&:focus {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
&:focus-visible, &:is(label:has(:checked:focus-visible)) {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: -5px;
|
||||
}
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
opacity: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
:where(&) {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
@@ -311,6 +399,58 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.countdown {
|
||||
&.countdown {
|
||||
line-height: 1em;
|
||||
}
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: inline-flex;
|
||||
& > * {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow-y: clip;
|
||||
transition: width 0.4s ease-out 0.2s;
|
||||
height: 1em;
|
||||
--value-v: calc(mod(max(0, var(--value)), 1000));
|
||||
--value-hundreds: calc(round(to-zero, var(--value-v) / 100, 1));
|
||||
--value-tens: calc(round(to-zero, mod(var(--value-v), 100) / 10, 1));
|
||||
--value-ones: calc(mod(var(--value-v), 100));
|
||||
--show-hundreds: clamp(clamp(0, var(--digits, 1) - 2, 1), var(--value-hundreds), 1);
|
||||
--show-tens: clamp(
|
||||
clamp(0, var(--digits, 1) - 1, 1),
|
||||
var(--value-tens) + var(--show-hundreds),
|
||||
1
|
||||
);
|
||||
--first-digits: calc(round(to-zero, var(--value-v) / 10, 1));
|
||||
width: calc(1ch + var(--show-tens) * 1ch + var(--show-hundreds) * 1ch);
|
||||
direction: ltr;
|
||||
&:before, &:after {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
overflow-x: clip;
|
||||
--tw-content: "00\A 01\A 02\A 03\A 04\A 05\A 06\A 07\A 08\A 09\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20\A 21\A 22\A 23\A 24\A 25\A 26\A 27\A 28\A 29\A 30\A 31\A 32\A 33\A 34\A 35\A 36\A 37\A 38\A 39\A 40\A 41\A 42\A 43\A 44\A 45\A 46\A 47\A 48\A 49\A 50\A 51\A 52\A 53\A 54\A 55\A 56\A 57\A 58\A 59\A 60\A 61\A 62\A 63\A 64\A 65\A 66\A 67\A 68\A 69\A 70\A 71\A 72\A 73\A 74\A 75\A 76\A 77\A 78\A 79\A 80\A 81\A 82\A 83\A 84\A 85\A 86\A 87\A 88\A 89\A 90\A 91\A 92\A 93\A 94\A 95\A 96\A 97\A 98\A 99\A";
|
||||
content: var(--tw-content);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: pre;
|
||||
text-align: end;
|
||||
direction: rtl;
|
||||
transition: all 1s cubic-bezier(1, 0, 0, 1), width 0.2s ease-out 0.2s, opacity 0.2s ease-out 0.2s;
|
||||
}
|
||||
&:before {
|
||||
width: calc(1ch + var(--show-hundreds) * 1ch);
|
||||
top: calc(var(--first-digits) * -1em);
|
||||
inset-inline-end: 0;
|
||||
opacity: var(--show-tens);
|
||||
}
|
||||
&:after {
|
||||
width: 1ch;
|
||||
top: calc(var(--value-ones) * -1em);
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.input {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
cursor: text;
|
||||
@@ -797,12 +937,46 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabs-box {
|
||||
@layer daisyui.l1.l2 {
|
||||
background-color: var(--color-base-200);
|
||||
padding: calc(0.25rem * 1);
|
||||
--tabs-box-radius: calc(3 * var(--radius-field));
|
||||
border-radius: calc( min(calc(var(--tab-height) / 2), var(--radius-field)) + min(0.25rem, var(--tabs-box-radius)) );
|
||||
box-shadow: 0 -0.5px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0.5px oklch(0% 0 0 / calc(var(--depth) * 0.05)) inset;
|
||||
> .tab {
|
||||
border-radius: var(--radius-field);
|
||||
border-style: none;
|
||||
&:focus-visible, &:is(label:has(:checked:focus-visible)) {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
> :is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]):not( .tab-disabled, [disabled] ), > :is(input:checked), > :is(label:has(:checked)) {
|
||||
background-color: var(--tab-bg, var(--color-base-100));
|
||||
box-shadow: 0 1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px 1px -1px var(--color-neutral), 0 1px 6px -4px var(--color-neutral);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
box-shadow: 0 1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px 1px -1px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 50%), #0000), 0 1px 6px -4px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 100%), #0000);
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
> .tab-content {
|
||||
margin-top: calc(0.25rem * 1);
|
||||
height: calc(100% - var(--tab-height) + var(--border) - 0.5rem);
|
||||
border-radius: calc( min(calc(var(--tab-height) / 2), var(--radius-field)) + min(0.25rem, var(--tabs-box-radius)) - var(--border) );
|
||||
}
|
||||
}
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mt-6 {
|
||||
margin-top: calc(var(--spacing) * 6);
|
||||
}
|
||||
.mt-8 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
}
|
||||
@@ -812,6 +986,9 @@
|
||||
.mb-4 {
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mb-6 {
|
||||
margin-bottom: calc(var(--spacing) * 6);
|
||||
}
|
||||
.ml-4 {
|
||||
margin-left: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -840,6 +1017,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
--tabs-height: auto;
|
||||
--tabs-direction: row;
|
||||
--tab-height: calc(var(--size-field, 0.25rem) * 10);
|
||||
height: var(--tabs-height);
|
||||
flex-direction: var(--tabs-direction);
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
border-width: var(--border);
|
||||
border-color: var(--alert-border-color, var(--color-base-200));
|
||||
@@ -947,6 +1135,9 @@
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.btn-square {
|
||||
@layer daisyui.l1.l2 {
|
||||
padding-inline: calc(0.25rem * 0);
|
||||
@@ -957,6 +1148,9 @@
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.max-w-md {
|
||||
max-width: var(--container-md);
|
||||
}
|
||||
.max-w-sm {
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
@@ -984,6 +1178,9 @@
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1021,6 +1218,9 @@
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
.p-3 {
|
||||
padding: calc(var(--spacing) * 3);
|
||||
}
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -1073,6 +1273,13 @@
|
||||
--alert-color: var(--color-error);
|
||||
}
|
||||
}
|
||||
.alert-info {
|
||||
@layer daisyui.l1.l2 {
|
||||
color: var(--color-info-content);
|
||||
--alert-border-color: var(--color-info);
|
||||
--alert-color: var(--color-info);
|
||||
}
|
||||
}
|
||||
.alert-success {
|
||||
@layer daisyui.l1.l2 {
|
||||
color: var(--color-success-content);
|
||||
@@ -1099,6 +1306,9 @@
|
||||
.no-underline {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
.opacity-40 {
|
||||
opacity: 40%;
|
||||
}
|
||||
.opacity-60 {
|
||||
opacity: 60%;
|
||||
}
|
||||
@@ -1226,6 +1436,36 @@
|
||||
.player-chip.yellow {
|
||||
background: #facc15;
|
||||
}
|
||||
.snake-board {
|
||||
display: inline-grid;
|
||||
gap: 0;
|
||||
background: #1a1a2e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 3px solid #333;
|
||||
}
|
||||
.snake-row {
|
||||
display: contents;
|
||||
}
|
||||
.snake-cell {
|
||||
background: #16213e;
|
||||
border: 1px solid rgba(255,255,255,0.03);
|
||||
transition: background 0.05s;
|
||||
}
|
||||
.snake-cell.snake-food {
|
||||
background: #ff6b6b;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px rgba(255,107,107,0.6);
|
||||
}
|
||||
.snake-cell.snake-head {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.snake-cell.snake-dead {
|
||||
opacity: 0.35;
|
||||
}
|
||||
.snake-wrapper:focus {
|
||||
outline: none;
|
||||
}
|
||||
@layer base {
|
||||
:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
|
||||
color-scheme: light;
|
||||
|
||||
Reference in New Issue
Block a user