Compare commits
44 Commits
705881f1ea
...
master
Author | SHA1 | Date | |
---|---|---|---|
a95dc82515 | |||
2180f14850 | |||
502317507b | |||
50d042a19d | |||
881a01bad0 | |||
82b3692919 | |||
4be52d8a33 | |||
49f7642937 | |||
8f6a093ea1 | |||
587adfbbda | |||
9a0e8d01ba | |||
ce5d55cc13 | |||
7ed430d8d7 | |||
723f335f0f | |||
fe21c3e927 | |||
22ddc88d82 | |||
75651d7f76 | |||
2751b6b9dc | |||
a2c5f17e30 | |||
7ae255cc04 | |||
a796b5b5de | |||
718c9c10be | |||
a131183729 | |||
357f42c354 | |||
e84941d593 | |||
a38472a685 | |||
a685686b32 | |||
9900ebd3dd | |||
f97d91ac74 | |||
e9b9b9e559 | |||
f46cbff602 | |||
6ad251fc47 | |||
27e31603da | |||
5b24378956 | |||
913228844a | |||
de2cccf66d | |||
eef4b7941b | |||
413edae4b6 | |||
56845e6141 | |||
3e9a93fbb1 | |||
3af3657c7a | |||
0e2baa1a0f | |||
a4dc8f4bbb | |||
2a2bf4e23d |
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Build stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
WORKDIR /src
|
||||
RUN apk add --no-cache build-base gcc
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -o /gralias .
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /gralias /app/gralias
|
||||
# copy assets and templates
|
||||
COPY assets ./assets
|
||||
COPY components ./components
|
||||
COPY config.toml ./config.toml
|
||||
COPY migrations ./migrations
|
||||
EXPOSE 3000
|
||||
CMD ["/app/gralias"]
|
5
Makefile
5
Makefile
@ -1,6 +1,6 @@
|
||||
.PHONY: all init deps install test lint run stop
|
||||
|
||||
run:
|
||||
run: migrate-up
|
||||
go build
|
||||
./gralias start
|
||||
|
||||
@ -32,7 +32,8 @@ stop-container:
|
||||
docker rm -f gralias 2>/dev/null && echo "old container removed"
|
||||
|
||||
run-container: stop-container
|
||||
docker run --name=gralias -v $(CURDIR)/store.json:/root/store.json -p 0.0.0.0:3000:3000 -d gralias:master
|
||||
migrate -database 'sqlite3://gralias.db' -path migrations up
|
||||
docker run --name=gralias -v $(CURDIR)/gralias.db:/app/gralias.db -p 0.0.0.0:3003:3000 -d gralias:master
|
||||
|
||||
migrate-up:
|
||||
migrate -database 'sqlite3://gralias.db' -path migrations up
|
||||
|
2
assets/htmx.min.js
vendored
2
assets/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
691
assets/output.css
Normal file
691
assets/output.css
Normal file
@ -0,0 +1,691 @@
|
||||
/*! tailwindcss v4.1.5 | MIT License | https://tailwindcss.com */
|
||||
@layer properties;
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--color-red-300: oklch(80.8% 0.114 19.571);
|
||||
--color-red-400: oklch(70.4% 0.191 22.216);
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-red-700: oklch(50.5% 0.213 27.518);
|
||||
--color-orange-100: oklch(95.4% 0.038 75.164);
|
||||
--color-orange-500: oklch(70.5% 0.213 47.604);
|
||||
--color-orange-700: oklch(55.3% 0.195 38.402);
|
||||
--color-amber-100: oklch(96.2% 0.059 95.617);
|
||||
--color-green-600: oklch(62.7% 0.194 149.214);
|
||||
--color-green-700: oklch(52.7% 0.154 150.069);
|
||||
--color-blue-300: oklch(80.9% 0.105 251.813);
|
||||
--color-blue-400: oklch(70.7% 0.165 254.624);
|
||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||
--color-indigo-500: oklch(58.5% 0.233 277.117);
|
||||
--color-indigo-600: oklch(51.1% 0.262 276.966);
|
||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||
--color-gray-300: oklch(87.2% 0.01 258.338);
|
||||
--color-gray-600: oklch(44.6% 0.03 256.802);
|
||||
--color-gray-900: oklch(21% 0.034 264.665);
|
||||
--color-stone-400: oklch(70.9% 0.01 56.259);
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-sm--line-height: calc(1.25 / 0.875);
|
||||
--text-lg: 1.125rem;
|
||||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
--text-xl--line-height: calc(1.75 / 1.25);
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid;
|
||||
}
|
||||
html, :host {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
b, strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
code, kbd, samp, pre {
|
||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||
font-size: 1em;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub, sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
ol, ul, menu {
|
||||
list-style: none;
|
||||
}
|
||||
img, svg, video, canvas, audio, iframe, embed, object {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
button, input, select, optgroup, textarea, ::file-selector-button {
|
||||
font: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup {
|
||||
font-weight: bolder;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup option {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
::file-selector-button {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||
::placeholder {
|
||||
color: currentcolor;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1lh;
|
||||
text-align: inherit;
|
||||
}
|
||||
::-webkit-datetime-edit {
|
||||
display: inline-flex;
|
||||
}
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||
padding-block: 0;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
|
||||
appearance: button;
|
||||
}
|
||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[hidden]:where(:not([hidden='until-found'])) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.mt-2 {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mb-1 {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.max-h-96 {
|
||||
max-height: calc(var(--spacing) * 96);
|
||||
}
|
||||
.w-24 {
|
||||
width: calc(var(--spacing) * 24);
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.min-w-\[100px\] {
|
||||
min-width: 100px;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.gap-2 {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.gap-4 {
|
||||
gap: calc(var(--spacing) * 4);
|
||||
}
|
||||
.space-y-2 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-4 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-6 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-x-2 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));
|
||||
margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
}
|
||||
.space-x-4 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
|
||||
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
}
|
||||
.overflow-y-auto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.rounded-lg {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.rounded-md {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.border {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-0 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 0px;
|
||||
}
|
||||
.border-2 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 2px;
|
||||
}
|
||||
.border-l-4 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
.border-gray-300 {
|
||||
border-color: var(--color-gray-300);
|
||||
}
|
||||
.border-orange-500 {
|
||||
border-color: var(--color-orange-500);
|
||||
}
|
||||
.border-stone-400 {
|
||||
border-color: var(--color-stone-400);
|
||||
}
|
||||
.bg-amber-100 {
|
||||
background-color: var(--color-amber-100);
|
||||
}
|
||||
.bg-blue-300 {
|
||||
background-color: var(--color-blue-300);
|
||||
}
|
||||
.bg-blue-400 {
|
||||
background-color: var(--color-blue-400);
|
||||
}
|
||||
.bg-blue-500 {
|
||||
background-color: var(--color-blue-500);
|
||||
}
|
||||
.bg-blue-700 {
|
||||
background-color: var(--color-blue-700);
|
||||
}
|
||||
.bg-gray-100 {
|
||||
background-color: var(--color-gray-100);
|
||||
}
|
||||
.bg-green-600 {
|
||||
background-color: var(--color-green-600);
|
||||
}
|
||||
.bg-indigo-600 {
|
||||
background-color: var(--color-indigo-600);
|
||||
}
|
||||
.bg-orange-100 {
|
||||
background-color: var(--color-orange-100);
|
||||
}
|
||||
.bg-red-300 {
|
||||
background-color: var(--color-red-300);
|
||||
}
|
||||
.bg-red-400 {
|
||||
background-color: var(--color-red-400);
|
||||
}
|
||||
.bg-red-500 {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
.bg-red-700 {
|
||||
background-color: var(--color-red-700);
|
||||
}
|
||||
.bg-white {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
.px-1 {
|
||||
padding-inline: calc(var(--spacing) * 1);
|
||||
}
|
||||
.px-2 {
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
.px-3 {
|
||||
padding-inline: calc(var(--spacing) * 3);
|
||||
}
|
||||
.px-4 {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-1\.5 {
|
||||
padding-block: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.font-mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.text-lg {
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||
}
|
||||
.text-sm {
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
.text-xl {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
}
|
||||
.leading-6 {
|
||||
--tw-leading: calc(var(--spacing) * 6);
|
||||
line-height: calc(var(--spacing) * 6);
|
||||
}
|
||||
.font-bold {
|
||||
--tw-font-weight: var(--font-weight-bold);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.font-medium {
|
||||
--tw-font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.font-semibold {
|
||||
--tw-font-weight: var(--font-weight-semibold);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
.text-black {
|
||||
color: var(--color-black);
|
||||
}
|
||||
.text-blue-400 {
|
||||
color: var(--color-blue-400);
|
||||
}
|
||||
.text-blue-500 {
|
||||
color: var(--color-blue-500);
|
||||
}
|
||||
.text-blue-600 {
|
||||
color: var(--color-blue-600);
|
||||
}
|
||||
.text-blue-700 {
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
.text-gray-600 {
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
.text-gray-900 {
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
.text-orange-700 {
|
||||
color: var(--color-orange-700);
|
||||
}
|
||||
.text-red-400 {
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
.text-red-500 {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
.text-red-700 {
|
||||
color: var(--color-red-700);
|
||||
}
|
||||
.text-white {
|
||||
color: var(--color-white);
|
||||
}
|
||||
.shadow-sm {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-1 {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-gray-300 {
|
||||
--tw-ring-color: var(--color-gray-300);
|
||||
}
|
||||
.transition-colors {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.ring-inset {
|
||||
--tw-ring-inset: inset;
|
||||
}
|
||||
.placeholder\:text-gray-300 {
|
||||
&::placeholder {
|
||||
color: var(--color-gray-300);
|
||||
}
|
||||
}
|
||||
.hover\:bg-green-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-green-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-indigo-500 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-indigo-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
.focus\:border-blue-500 {
|
||||
&:focus {
|
||||
border-color: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
.focus\:ring-2 {
|
||||
&:focus {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
.focus\:ring-blue-500 {
|
||||
&:focus {
|
||||
--tw-ring-color: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
.focus\:ring-indigo-600 {
|
||||
&:focus {
|
||||
--tw-ring-color: var(--color-indigo-600);
|
||||
}
|
||||
}
|
||||
.focus\:ring-inset {
|
||||
&:focus {
|
||||
--tw-ring-inset: inset;
|
||||
}
|
||||
}
|
||||
.focus-visible\:outline {
|
||||
&:focus-visible {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
}
|
||||
.focus-visible\:outline-2 {
|
||||
&:focus-visible {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 2px;
|
||||
}
|
||||
}
|
||||
.focus-visible\:outline-offset-2 {
|
||||
&:focus-visible {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
.focus-visible\:outline-indigo-600 {
|
||||
&:focus-visible {
|
||||
outline-color: var(--color-indigo-600);
|
||||
}
|
||||
}
|
||||
.sm\:grid-cols-5 {
|
||||
@media (width >= 40rem) {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.sm\:text-sm {
|
||||
@media (width >= 40rem) {
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
}
|
||||
.sm\:leading-6 {
|
||||
@media (width >= 40rem) {
|
||||
--tw-leading: calc(var(--spacing) * 6);
|
||||
line-height: calc(var(--spacing) * 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@property --tw-space-y-reverse {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0;
|
||||
}
|
||||
@property --tw-space-x-reverse {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-leading {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-font-weight {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-inset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-inset-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-inset-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-ring-inset {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-offset-width {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --tw-ring-offset-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: #fff;
|
||||
}
|
||||
@property --tw-ring-offset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-space-y-reverse: 0;
|
||||
--tw-space-x-reverse: 0;
|
||||
--tw-border-style: solid;
|
||||
--tw-leading: initial;
|
||||
--tw-font-weight: initial;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-color: initial;
|
||||
--tw-shadow-alpha: 100%;
|
||||
--tw-inset-shadow: 0 0 #0000;
|
||||
--tw-inset-shadow-color: initial;
|
||||
--tw-inset-shadow-alpha: 100%;
|
||||
--tw-ring-color: initial;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-inset-ring-color: initial;
|
||||
--tw-inset-ring-shadow: 0 0 #0000;
|
||||
--tw-ring-inset: initial;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-outline-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
@ -1636,7 +1636,6 @@ increase
|
||||
computer
|
||||
bobby
|
||||
accused
|
||||
500
|
||||
nonsense
|
||||
close
|
||||
finger
|
||||
@ -2726,7 +2725,6 @@ attendance
|
||||
present
|
||||
find
|
||||
lead
|
||||
wtv
|
||||
champion
|
||||
gasoline
|
||||
national
|
||||
@ -2746,7 +2744,6 @@ excitement
|
||||
quote
|
||||
forehead
|
||||
wax
|
||||
mckinley
|
||||
television
|
||||
can
|
||||
voyage
|
||||
@ -2835,7 +2832,6 @@ least
|
||||
boot
|
||||
alien
|
||||
employer
|
||||
viscosity
|
||||
theft
|
||||
wall
|
||||
vapor
|
||||
@ -2848,7 +2844,6 @@ sovereign
|
||||
smoke
|
||||
fool
|
||||
intelligence
|
||||
indictment
|
||||
flame
|
||||
advance
|
||||
mud
|
||||
@ -2981,7 +2976,6 @@ agent
|
||||
motel
|
||||
punishment
|
||||
lime
|
||||
magnification
|
||||
snap
|
||||
surgeon
|
||||
short
|
||||
@ -3150,7 +3144,6 @@ cope
|
||||
law
|
||||
lap
|
||||
recommendation
|
||||
patrolman
|
||||
purple
|
||||
imagery
|
||||
offer
|
||||
|
@ -52,11 +52,20 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
// w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = "*" // Fallback for non-browser clients
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
messageChan := make(NotifierChan)
|
||||
broker.newClients <- messageChan
|
||||
defer func() { broker.closingClients <- messageChan }()
|
||||
ctx := r.Context()
|
||||
// browser can close sse on its own
|
||||
heartbeat := time.NewTicker(15 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@ -70,6 +79,12 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case <-heartbeat.C:
|
||||
// Send SSE heartbeat comment
|
||||
if _, err := w.Write([]byte(": heartbeat\n\n")); err != nil {
|
||||
return // Client disconnected
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,7 +110,8 @@ func (broker *Broker) Listen() {
|
||||
select {
|
||||
case clientMessageChan <- event:
|
||||
case <-time.After(patience):
|
||||
slog.Info("Client was skipped", "clients listening", len(broker.clients))
|
||||
delete(broker.clients, clientMessageChan)
|
||||
slog.Info("Client was removed", "clients listening", len(broker.clients))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Alias</title>
|
||||
<script src="/assets/helpers.js"></script>
|
||||
<script src="/assets/htmx.min.js"></script>
|
||||
<script src="/assets/htmx.sse.js"></script>
|
||||
<script src="/assets/tailwind.css"></script>
|
||||
<link rel="stylesheet" href="/assets/style.css"/>
|
||||
<script src="/assets/htmx.min.js"></script>
|
||||
<script src="/assets/htmx.sse.js"></script>
|
||||
<script src="/assets/helpers.js"></script>
|
||||
<meta charset="utf-8" name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<link rel="icon" sizes="64x64" href="/assets/favicon/wolfhead_negated.ico"/>
|
||||
<style type="text/css">
|
||||
|
@ -28,9 +28,12 @@
|
||||
</div>
|
||||
<div class="h-6 bg-stone-600 rounded-b flex items-center justify-center text-white text-sm cursor-pointer"
|
||||
hx-get="/mark-card?word={{.Word}}" hx-trigger="click" hx-swap="outerHTML transition:true swap:.05s">
|
||||
{{range .Mark}}
|
||||
{{if .Active}}
|
||||
<span class="mx-0.5">X</span>
|
||||
{{range .Marks}}
|
||||
{{ $length := len .Username }}
|
||||
{{ if lt $length 3 }}
|
||||
<span class="mx-0.5">{{.Username}}</span>
|
||||
{{else}}
|
||||
<span class="mx-0.5">{{slice .Username 0 3}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -4,8 +4,7 @@
|
||||
{{ else if ne .LinkLogin "" }}
|
||||
{{template "linklogin" .LinkLogin}}
|
||||
{{ else if not .State.RoomID }}
|
||||
<div id="hello-user">
|
||||
<p>data: {{.}} {{.State}} {{.Room}}</p>
|
||||
<div id="hello-user" class="text-xl py-2">
|
||||
<p>Hello {{.State.Username}}</p>
|
||||
</div>
|
||||
<div id="create-room" class="create-room-div">
|
||||
|
@ -8,6 +8,10 @@
|
||||
<input type="hidden" name="room_id" value={{.}}>
|
||||
</div>
|
||||
<div id="login_notice">this name looks available</div>
|
||||
<label For="password" class="block text-sm font-medium leading-6 text-white-900">password</label>
|
||||
<div>
|
||||
<input id="password" name="password" type="password" class="block w-full rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
|
||||
</div>
|
||||
|
@ -2,11 +2,15 @@
|
||||
<div id="logindiv">
|
||||
<form class="space-y-6" hx-post="/login" hx-target="#ancestor">
|
||||
<div>
|
||||
<label For="username" class="block text-sm font-medium leading-6 text-white-900">tell us your username</label>
|
||||
<label For="username" class="block text-sm font-medium leading-6 text-white-900">tell us your username (signup|login)</label>
|
||||
<div class="mt-2">
|
||||
<input id="username" name="username" hx-target="#login_notice" hx-swap="outerHTML" hx-post="/check/name" hx-trigger="input changed delay:400ms" autocomplete="username" required class="block w-full rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
||||
</div>
|
||||
<div id="login_notice">this name looks available</div>
|
||||
</div>
|
||||
<div>
|
||||
<label For="password" class="block text-sm font-medium leading-6 text-white-900">password</label>
|
||||
<input id="password" name="password" type="password" class="block w-full rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
|
||||
|
@ -49,17 +49,18 @@
|
||||
{{template "teamlist" .Room.RedTeam}}
|
||||
</div>
|
||||
<hr/>
|
||||
<div id="systembox" style="overflow-y: auto; max-height: 100px;">
|
||||
Server says: <br>
|
||||
<div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
|
||||
bot thought: <br>
|
||||
<ul>
|
||||
{{range .Room.LogJournal}}
|
||||
<li>{{.}}</li>
|
||||
<li>{{.Username}}: {{.Entry}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<div sse-swap="journal_{{.Room.ID}}">
|
||||
bot thoughts
|
||||
<div>
|
||||
<div hx-get="/actionhistory" hx-trigger="sse:backlog_{{.Room.ID}}">
|
||||
{{template "actionhistory" .Room.ActionHistory}}
|
||||
</div>
|
||||
<hr/>
|
||||
<div id="cardtable">
|
||||
{{template "cardtable" .Room}}
|
||||
</div>
|
||||
@ -77,9 +78,6 @@
|
||||
<button hx-get="/renotify-bot" hx-swap="none" class="bg-gray-100 text-black px-1 py-1 rounded">Btn in case llm call failed</button>
|
||||
{{end}}
|
||||
</div>
|
||||
<div hx-get="/actionhistory" hx-trigger="sse:backlog_{{.Room.ID}}">
|
||||
{{template "actionhistory" .Room.ActionHistory}}
|
||||
</div>
|
||||
{{if not .Room.IsRunning}}
|
||||
<div id="exitbtn">
|
||||
<button button id="exit-room-btn" type="submit" class="justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" hx-get="/room/exit" hx-target="#ancestor">Exit Room</button>
|
||||
|
@ -1,9 +1,6 @@
|
||||
{{define "roomlist"}}
|
||||
<div id="roomlist" hx-get="/" hx-trigger="sse:roomlistupdate" hx-target="#ancestor">
|
||||
{{range .}}
|
||||
<p>
|
||||
{{.ID}}
|
||||
</p>
|
||||
<div hx-get="/room-join?id={{.ID}}" hx-target="#ancestor" class="room-item mb-3 p-4 border rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="room-info">
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{define "turntimer"}}
|
||||
<div>
|
||||
Timer: <span sse-swap="turntimer_{{.ID}}">{{.Settings.TurnSecondsLeft}}</span>
|
||||
Timer: <span sse-swap="turntimer_{{.ID}}">no time to lose</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -1,7 +1,7 @@
|
||||
BASE_URL = "https://localhost:3000"
|
||||
SESSION_LIFETIME_SECONDS = 30000
|
||||
COOKIE_SECRET = "test"
|
||||
DB_PATH = "sqlite3://gralias.db"
|
||||
DB_PATH = "gralias.db"
|
||||
|
||||
[SERVICE]
|
||||
HOST = "localhost"
|
||||
|
@ -39,7 +39,7 @@ func LoadConfigOrDefault(fn string) *Config {
|
||||
config.CookieSecret = "test"
|
||||
config.ServerConfig.Host = "localhost"
|
||||
config.ServerConfig.Port = "3000"
|
||||
config.DBPath = "sqlite3://gralias.db"
|
||||
config.DBPath = "gralias.db"
|
||||
}
|
||||
fmt.Printf("config debug; config.LLMConfig.URL: %s\n", config.LLMConfig.URL)
|
||||
return config
|
||||
|
@ -2,6 +2,10 @@ package crons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"gralias/broker"
|
||||
"gralias/models"
|
||||
"gralias/repos"
|
||||
"log/slog"
|
||||
"time"
|
||||
@ -25,6 +29,8 @@ func (cm *CronManager) Start() {
|
||||
for range ticker.C {
|
||||
cm.CleanupRooms()
|
||||
cm.CleanupActions()
|
||||
cm.CleanupPlayersRoom()
|
||||
ticker.Reset(30 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -43,7 +49,6 @@ func (cm *CronManager) CleanupRooms() {
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
rooms, err := cm.repo.RoomList(ctx)
|
||||
if err != nil {
|
||||
cm.log.Error("failed to get rooms list", "err", err)
|
||||
@ -52,14 +57,12 @@ func (cm *CronManager) CleanupRooms() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, room := range rooms {
|
||||
players, err := cm.repo.PlayerListByRoom(ctx, room.ID)
|
||||
if err != nil {
|
||||
cm.log.Error("failed to get players for room", "room_id", room.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(players) == 0 {
|
||||
cm.log.Info("deleting empty room", "room_id", room.ID)
|
||||
if err := cm.repo.RoomDeleteByID(ctx, room.ID); err != nil {
|
||||
@ -70,7 +73,6 @@ func (cm *CronManager) CleanupRooms() {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
creatorInRoom := false
|
||||
for _, player := range players {
|
||||
if player.Username == room.CreatorName {
|
||||
@ -78,12 +80,32 @@ func (cm *CronManager) CleanupRooms() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !creatorInRoom {
|
||||
cm.log.Info("deleting room because creator left", "room_id", room.ID)
|
||||
isInactive := false
|
||||
// If the creator is in the room and the room is more than one hour old, check for inactivity
|
||||
if creatorInRoom && time.Since(room.CreatedAt) > time.Hour {
|
||||
lastActionTime, err := cm.repo.ActionGetLastTimeByRoomID(ctx, room.ID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
cm.log.Error("failed to get last action time for room", "room_id", room.ID, "err", err)
|
||||
// Skip setting isInactive and proceed
|
||||
} else {
|
||||
// If there are no actions, lastActionTime is the zero value (or from sql.ErrNoRows we get zero as well)
|
||||
if lastActionTime.IsZero() {
|
||||
isInactive = true
|
||||
} else if time.Since(lastActionTime) > time.Hour {
|
||||
isInactive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the creator is not in the room or the room is inactive, it's time to delete
|
||||
if !creatorInRoom || isInactive {
|
||||
reason := "creator left"
|
||||
if isInactive {
|
||||
reason = "inactive"
|
||||
}
|
||||
cm.log.Info("deleting room", "room_id", room.ID, "reason", reason)
|
||||
for _, player := range players {
|
||||
if player.IsBot {
|
||||
if err := cm.repo.PlayerDelete(ctx, room.ID, player.Username); err != nil {
|
||||
if err := cm.repo.PlayerDelete(ctx, room.ID); err != nil {
|
||||
cm.log.Error("failed to delete bot player", "room_id", room.ID, "username", player.Username, "err", err)
|
||||
}
|
||||
} else {
|
||||
@ -93,17 +115,22 @@ func (cm *CronManager) CleanupRooms() {
|
||||
}
|
||||
}
|
||||
if err := cm.repo.RoomDeleteByID(ctx, room.ID); err != nil {
|
||||
cm.log.Error("failed to delete room after creator left", "room_id", room.ID, "err", err)
|
||||
cm.log.Error("failed to delete room", "room_id", room.ID, "reason", reason, "err", err)
|
||||
}
|
||||
if err := cm.repo.SettingsDeleteByRoomID(ctx, room.ID); err != nil {
|
||||
cm.log.Error("failed to delete settings after creator left", "room_id", room.ID, "err", err)
|
||||
cm.log.Error("failed to delete settings for room", "room_id", room.ID, "reason", reason, "err", err)
|
||||
}
|
||||
// Move to the next room
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cm.log.Error("failed to commit transaction", "err", err)
|
||||
}
|
||||
broker.Notifier.Notifier <- broker.NotificationEvent{
|
||||
EventName: models.NotifyRoomListUpdate,
|
||||
Payload: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *CronManager) CleanupActions() {
|
||||
@ -120,17 +147,14 @@ func (cm *CronManager) CleanupActions() {
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cm.repo.ActionsDeleteOrphaned(ctx); err != nil {
|
||||
if err := cm.repo.ActionDeleteOrphaned(ctx); err != nil {
|
||||
cm.log.Error("failed to delete orphaned actions", "err", err)
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cm.log.Error("failed to rollback transaction for actions cleanup", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cm.log.Error("failed to commit transaction for actions cleanup", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
74
crons/players.go
Normal file
74
crons/players.go
Normal file
@ -0,0 +1,74 @@
|
||||
package crons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gralias/llmapi"
|
||||
)
|
||||
|
||||
func (cm *CronManager) CleanupPlayersRoom() {
|
||||
ctx, tx, err := cm.repo.InitTx(context.Background())
|
||||
if err != nil {
|
||||
cm.log.Error("failed to init transaction for actions cleanup", "err", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cm.log.Error("failed to rollback transaction for actions cleanup", "err", err)
|
||||
}
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
players, err := cm.repo.PlayerListAll(ctx)
|
||||
if err != nil {
|
||||
cm.log.Error("failed to list players", "err", err)
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cm.log.Error("failed to rollback transaction for actions cleanup", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// get all rooms to have only one req
|
||||
rooms, err := cm.repo.RoomList(ctx)
|
||||
if err != nil {
|
||||
cm.log.Error("failed to list rooms", "err", err)
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cm.log.Error("failed to rollback transaction for actions cleanup", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, player := range players {
|
||||
found := false
|
||||
for _, room := range rooms {
|
||||
// check if room exists
|
||||
if player.RoomID != nil && room.ID == *player.RoomID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cm.log.Debug("player routine; not found room", "username", player.Username)
|
||||
if !player.IsBot && player.RoomID != nil {
|
||||
// delete roomid from player
|
||||
if err := cm.repo.PlayerExitRoom(ctx, player.Username); err != nil {
|
||||
cm.log.Error("failed to unset room", "err", err)
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cm.log.Error("failed to rollback transaction for actions cleanup", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if player.IsBot {
|
||||
cm.log.Debug("trying to remove bot", "name", player.Username)
|
||||
// delete player and stop the bot
|
||||
if err := llmapi.RemoveBotNoRoom(player.Username); err != nil {
|
||||
cm.log.Error("failed to remove bot", "err", err)
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cm.log.Error("failed to rollback transaction for actions cleanup", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
cm.log.Error("failed to commit transaction for actions cleanup", "err", err)
|
||||
}
|
||||
}
|
@ -26,6 +26,9 @@ func createRoom(ctx context.Context, req *models.RoomReq) (*models.Room, error)
|
||||
|
||||
func saveFullInfo(ctx context.Context, fi *models.FullInfo) error {
|
||||
// INFO: no transactions; so case is possible where first object is updated but the second is not
|
||||
if fi.State == nil {
|
||||
return errors.New("player is nil")
|
||||
}
|
||||
if err := repo.PlayerUpdate(ctx, fi.State); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -65,20 +68,44 @@ func getFullInfoByCtx(ctx context.Context) (*models.FullInfo, error) {
|
||||
if err != nil {
|
||||
// room was deleted; remove it from player;
|
||||
log.Warn("failed to find room despite knowing room_id;",
|
||||
"room_id", state.RoomID)
|
||||
"room_id", state.RoomID, "error", err)
|
||||
state.Team = models.UserTeamNone
|
||||
state.Role = models.UserRoleNone
|
||||
if err := repo.PlayerExitRoom(ctx, state.Username); err != nil {
|
||||
log.Warn("failed to exit room",
|
||||
log.Warn("failed to exit room", "error", err,
|
||||
"room_id", state.RoomID, "username", state.Username)
|
||||
return resp, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// get card_marks
|
||||
if room.IsRunning && room.MimeDone {
|
||||
if err := fillCardMarks(ctx, room); err != nil {
|
||||
log.Warn("failed to fill card marks", "error", err,
|
||||
"room_id", state.RoomID, "username", state.Username)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resp.Room = room
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func fillCardMarks(ctx context.Context, room *models.Room) error {
|
||||
marks, err := repo.CardMarksByRoomID(ctx, room.ID)
|
||||
if err != nil {
|
||||
log.Warn("failed to fetch card marks by room_id", "room_id", room.ID, "error", err)
|
||||
return err
|
||||
}
|
||||
for i, card := range room.Cards {
|
||||
for _, mark := range marks {
|
||||
if mark.CardID == card.ID {
|
||||
room.Cards[i].Marks = append(room.Cards[i].Marks, mark)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPlayerByCtx(ctx context.Context) (*models.Player, error) {
|
||||
username, ok := ctx.Value(models.CtxUsernameKey).(string)
|
||||
if !ok {
|
||||
@ -88,18 +115,6 @@ func getPlayerByCtx(ctx context.Context) (*models.Player, error) {
|
||||
return repo.PlayerGetByName(ctx, username)
|
||||
}
|
||||
|
||||
// // DEPRECATED
|
||||
// func leaveRole(fi *models.FullInfo) {
|
||||
// fi.Room.RedTeam.Guessers = utils.RemoveFromSlice(fi.State.Username, fi.Room.RedTeam.Guessers)
|
||||
// fi.Room.BlueTeam.Guessers = utils.RemoveFromSlice(fi.State.Username, fi.Room.BlueTeam.Guessers)
|
||||
// if fi.Room.RedTeam.Mime == fi.State.Username {
|
||||
// fi.Room.RedTeam.Mime = ""
|
||||
// }
|
||||
// if fi.Room.BlueTeam.Mime == fi.State.Username {
|
||||
// fi.Room.BlueTeam.Mime = ""
|
||||
// }
|
||||
// }
|
||||
|
||||
func joinTeam(ctx context.Context, role, team string) (*models.FullInfo, error) {
|
||||
// get username
|
||||
fi, _ := getFullInfoByCtx(ctx)
|
||||
@ -155,26 +170,6 @@ func joinTeam(ctx context.Context, role, team string) (*models.FullInfo, error)
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// get all rooms
|
||||
// func listRooms(allRooms bool) []*models.Room {
|
||||
// cacheMap := memcache.GetAll()
|
||||
// publicRooms := []*models.Room{}
|
||||
// // no way to know if room is public until unmarshal -_-;
|
||||
// for key, value := range cacheMap {
|
||||
// if strings.HasPrefix(key, models.CacheRoomPrefix) {
|
||||
// room := &models.Room{}
|
||||
// if err := json.Unmarshal(value, &room); err != nil {
|
||||
// log.Warn("failed to unmarshal room", "error", err)
|
||||
// continue
|
||||
// }
|
||||
// if room.IsPublic || allRooms {
|
||||
// publicRooms = append(publicRooms, room)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return publicRooms
|
||||
// }
|
||||
|
||||
// get bots
|
||||
func listBots() []models.Player {
|
||||
bots, err := repo.PlayerList(context.Background(), true)
|
||||
@ -193,6 +188,11 @@ func notify(event, msg string) {
|
||||
}
|
||||
|
||||
func loadCards(room *models.Room) {
|
||||
// remove old cards
|
||||
room.Cards = []models.WordCard{}
|
||||
// try to delete old cards from db (in case players play another round)
|
||||
// nolint: errcheck
|
||||
repo.WordCardsDeleteByRoomID(context.Background(), room.ID)
|
||||
// store it somewhere
|
||||
wordMap := map[string]string{
|
||||
"en": "assets/words/en_nouns.txt",
|
||||
@ -205,10 +205,6 @@ func loadCards(room *models.Room) {
|
||||
fmt.Println("failed to load cards", "error", err)
|
||||
}
|
||||
room.Cards = cards
|
||||
room.WCMap = make(map[string]models.WordColor)
|
||||
for _, card := range room.Cards {
|
||||
room.WCMap[card.Word] = card.Color
|
||||
}
|
||||
}
|
||||
|
||||
func recoverBots() {
|
||||
@ -236,35 +232,6 @@ func recoverBot(bm models.Player) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// func recoverPlayers() {
|
||||
// players := listPlayers()
|
||||
// for playerName, playerMap := range players {
|
||||
// if err := recoverPlayer(playerMap); err != nil {
|
||||
// log.Warn("failed to recover player", "playerName", playerName, "error", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func recoverPlayer(pm map[string]string) error {
|
||||
// // check if room still exists
|
||||
// room, err := repo.RoomGetByID(context.Background(), pm["RoomID"])
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("no such room: %s; err: %w", pm["RoomID"], err)
|
||||
// }
|
||||
// log.Debug("recovering player", "player", pm)
|
||||
// role, team, ok := room.GetPlayerByName(pm["Username"])
|
||||
// if !ok {
|
||||
// return fmt.Errorf("failed to find player %s in the room %v", pm["Username"], room)
|
||||
// }
|
||||
// us := &models.Player{
|
||||
// Username: pm["Username"],
|
||||
// RoomID: pm["RoomID"],
|
||||
// Team: team,
|
||||
// Role: role,
|
||||
// }
|
||||
// return saveState(pm["Username"], us)
|
||||
// }
|
||||
|
||||
// validateMove checks if it is players turn
|
||||
func validateMove(fi *models.FullInfo, ur models.UserRole) error {
|
||||
if fi.State.Role != ur {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"gralias/models"
|
||||
"gralias/utils"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@ -36,7 +37,6 @@ func HandleNameCheck(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
cleanName := utils.RemoveSpacesFromStr(username)
|
||||
// allNames := getAllNames()
|
||||
allNames, err := repo.PlayerListNames(r.Context())
|
||||
if err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
@ -73,10 +73,12 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) {
|
||||
abortWithError(w, msg)
|
||||
return
|
||||
}
|
||||
password := r.PostFormValue("password")
|
||||
var makeplayer bool
|
||||
roomID := r.PostFormValue("room_id")
|
||||
// make sure username does not exists
|
||||
cleanName := utils.RemoveSpacesFromStr(username)
|
||||
clearPass := utils.RemoveSpacesFromStr(password)
|
||||
// login user
|
||||
cookie, err := makeCookie(cleanName, r.RemoteAddr)
|
||||
if err != nil {
|
||||
@ -84,15 +86,20 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
// check if that user was already in db
|
||||
// userstate, err := loadState(cleanName)
|
||||
userstate, err := repo.PlayerGetByName(r.Context(), cleanName)
|
||||
if err != nil || userstate == nil {
|
||||
|
||||
log.Debug("making new player", "error", err, "state", userstate)
|
||||
userstate = models.InitPlayer(cleanName)
|
||||
makeplayer = true
|
||||
} else {
|
||||
if userstate.Password != clearPass {
|
||||
log.Error("wrong password", "username", cleanName, "password", clearPass)
|
||||
abortWithError(w, "wrong password")
|
||||
return
|
||||
}
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
fi := &models.FullInfo{
|
||||
State: userstate,
|
||||
}
|
||||
@ -105,20 +112,12 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// room.PlayerList = append(room.PlayerList, fi.State.Username)
|
||||
fi.Room = room
|
||||
fi.List = nil
|
||||
fi.State.RoomID = &room.ID
|
||||
if err := repo.PlayerSetRoomID(r.Context(), fi.State.Username, room.ID); err != nil {
|
||||
if err := repo.PlayerSetRoomID(r.Context(), room.ID, fi.State.Username); err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// repo.RoomUpdate()
|
||||
// save full info instead
|
||||
// if err := saveFullInfo(r.Context(), fi); err != nil {
|
||||
// abortWithError(w, err.Error())
|
||||
// return
|
||||
// }
|
||||
} else {
|
||||
log.Debug("no room_id in login")
|
||||
// fi.List = listRooms(false)
|
||||
@ -128,19 +127,15 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// save state to cache
|
||||
// if err := saveState(cleanName, userstate); err != nil {
|
||||
if makeplayer {
|
||||
userstate.Password = clearPass
|
||||
if err := repo.PlayerAdd(r.Context(), userstate); err != nil {
|
||||
// if err := saveFullInfo(r.Context(), fi); err != nil {
|
||||
log.Error("failed to save state", "error", err)
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
|
||||
// log.Error("failed to execute base template", "error", err)
|
||||
// }
|
||||
http.Redirect(w, r, "/", 302)
|
||||
}
|
||||
|
||||
@ -181,7 +176,15 @@ func makeCookie(username string, remote string) (*http.Cookie, error) {
|
||||
cookie.Secure = false
|
||||
log.Info("changing cookie domain", "domain", cookie.Domain)
|
||||
}
|
||||
// set ctx?
|
||||
player, err := repo.PlayerGetByName(context.Background(), username)
|
||||
if err != nil || player == nil {
|
||||
// make player first, since username is fk to players table
|
||||
player = models.InitPlayer(username)
|
||||
if err := repo.PlayerAdd(context.Background(), player); err != nil {
|
||||
slog.Error("failed to create player", "username", username)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := repo.SessionCreate(context.Background(), session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -50,7 +50,8 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
color, exists := fi.Room.WCMap[word]
|
||||
// color, exists := fi.Room.WCMap[word]
|
||||
color, exists := fi.Room.FindColor(word)
|
||||
if !exists {
|
||||
abortWithError(w, "word is not found")
|
||||
return
|
||||
@ -60,7 +61,16 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
Color: color,
|
||||
Revealed: true,
|
||||
}
|
||||
fi.Room.RevealSpecificWord(word)
|
||||
revCardID := fi.Room.RevealSpecificWord(word)
|
||||
if revCardID == 0 {
|
||||
// error
|
||||
abortWithError(w, "word has 0 id")
|
||||
return
|
||||
}
|
||||
if err := repo.WordCardReveal(r.Context(), word, fi.Room.ID); err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
fi.Room.UpdateCounter()
|
||||
action := models.Action{
|
||||
Actor: fi.State.Username,
|
||||
@ -68,10 +78,16 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
WordColor: string(color),
|
||||
Action: models.ActionTypeGuess,
|
||||
Word: word,
|
||||
RoomID: fi.Room.ID,
|
||||
}
|
||||
if err := repo.ActionCreate(r.Context(), &action); err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
// if opened card is of color of opp team, change turn
|
||||
oppositeColor := fi.Room.GetOppositeTeamColor()
|
||||
var clearMarks bool
|
||||
fi.Room.OpenedThisTurn++
|
||||
log.Debug("got show-color request", "word", word, "color", color,
|
||||
"limit", fi.Room.ThisTurnLimit, "opened", fi.Room.OpenedThisTurn,
|
||||
@ -83,7 +99,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.MimeDone = false
|
||||
fi.Room.OpenedThisTurn = 0
|
||||
fi.Room.ThisTurnLimit = 0
|
||||
fi.Room.ClearMarks()
|
||||
clearMarks = true
|
||||
StopTurnTimer(fi.Room.ID)
|
||||
}
|
||||
switch string(color) {
|
||||
@ -94,6 +110,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.IsOver = true
|
||||
fi.Room.TeamWon = oppositeColor
|
||||
action := models.Action{
|
||||
RoomID: fi.Room.ID,
|
||||
Actor: fi.State.Username,
|
||||
ActorColor: string(fi.State.Team),
|
||||
WordColor: models.WordColorBlack,
|
||||
@ -102,15 +119,16 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.OpenedThisTurn = 0
|
||||
fi.Room.ThisTurnLimit = 0
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
fi.Room.ClearMarks()
|
||||
clearMarks = true
|
||||
StopTurnTimer(fi.Room.ID)
|
||||
case string(models.WordColorWhite), string(oppositeColor):
|
||||
log.Debug("opened opposite color word", "room", fi.Room, "opposite-color", oppositeColor)
|
||||
log.Debug("opened white or opposite color word", "word", word, "opposite-color", oppositeColor)
|
||||
// end turn
|
||||
fi.Room.TeamTurn = oppositeColor
|
||||
fi.Room.MimeDone = false
|
||||
fi.Room.OpenedThisTurn = 0
|
||||
fi.Room.ThisTurnLimit = 0
|
||||
clearMarks = true
|
||||
StopTurnTimer(fi.Room.ID)
|
||||
// check if no cards left => game over
|
||||
if fi.Room.BlueCounter == 0 {
|
||||
@ -119,13 +137,13 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.IsOver = true
|
||||
fi.Room.TeamWon = "blue"
|
||||
action := models.Action{
|
||||
RoomID: fi.Room.ID,
|
||||
Actor: fi.State.Username,
|
||||
ActorColor: string(fi.State.Team),
|
||||
WordColor: models.WordColorBlue,
|
||||
Action: models.ActionTypeGameOver,
|
||||
}
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
fi.Room.ClearMarks()
|
||||
}
|
||||
if fi.Room.RedCounter == 0 {
|
||||
// red won
|
||||
@ -133,13 +151,13 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.IsOver = true
|
||||
fi.Room.TeamWon = "red"
|
||||
action := models.Action{
|
||||
RoomID: fi.Room.ID,
|
||||
Actor: fi.State.Username,
|
||||
ActorColor: string(fi.State.Team),
|
||||
WordColor: models.WordColorRed,
|
||||
Action: models.ActionTypeGameOver,
|
||||
}
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
fi.Room.ClearMarks()
|
||||
}
|
||||
default: // same color as the team
|
||||
// check if game over
|
||||
@ -148,13 +166,19 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.IsOver = true
|
||||
fi.Room.TeamWon = fi.State.Team
|
||||
action := models.Action{
|
||||
RoomID: fi.Room.ID,
|
||||
Actor: fi.State.Username,
|
||||
ActorColor: string(fi.State.Team),
|
||||
WordColor: models.WordColorRed,
|
||||
Action: models.ActionTypeGameOver,
|
||||
}
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
}
|
||||
}
|
||||
if clearMarks {
|
||||
fi.Room.ClearMarks()
|
||||
if err := repo.CardMarksRemoveByRoomID(r.Context(), fi.Room.ID); err != nil {
|
||||
log.Error("failed to remove marks", "error", err, "room_id", fi.Room.ID)
|
||||
}
|
||||
}
|
||||
if err := saveFullInfo(r.Context(), fi); err != nil {
|
||||
@ -186,8 +210,8 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
color, exists := fi.Room.WCMap[word]
|
||||
log.Debug("got show-color request", "word", word, "color", color)
|
||||
color, exists := fi.Room.FindColor(word)
|
||||
log.Debug("got mark-card request", "word", word, "color", color)
|
||||
if !exists {
|
||||
abortWithError(w, "word is not found")
|
||||
return
|
||||
@ -205,20 +229,33 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the current user already has an active mark on this card
|
||||
found := false
|
||||
var newMarks []models.CardMark
|
||||
for _, mark := range card.Mark {
|
||||
if mark.Username == fi.State.Username && mark.Active {
|
||||
for _, mark := range card.Marks {
|
||||
if mark.Username == fi.State.Username {
|
||||
found = true
|
||||
} else {
|
||||
newMarks = append(newMarks, mark)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
newMarks = append(newMarks, models.CardMark{
|
||||
cm := models.CardMark{
|
||||
Username: fi.State.Username,
|
||||
Active: true,
|
||||
})
|
||||
CardID: card.ID,
|
||||
}
|
||||
fi.Room.Cards[i].Mark = newMarks
|
||||
newMarks = append(newMarks, cm)
|
||||
if err := repo.CardMarksAdd(r.Context(), &cm); err != nil {
|
||||
log.Error("failed to add mark", "error", err, "card", card)
|
||||
abortWithError(w, "failed to add mark")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// if mark was found, it needs to be removed
|
||||
if err := repo.CardMarksRemove(r.Context(), card.ID, fi.State.Username); err != nil {
|
||||
log.Error("failed to remove mark", "error", err, "card", card)
|
||||
abortWithError(w, "failed to remove mark")
|
||||
return
|
||||
}
|
||||
}
|
||||
fi.Room.Cards[i].Marks = newMarks
|
||||
cardword = fi.Room.Cards[i]
|
||||
}
|
||||
if err := saveFullInfo(r.Context(), fi); err != nil {
|
||||
@ -257,7 +294,14 @@ func HandleAddBot(w http.ResponseWriter, r *http.Request) {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
botname := fmt.Sprintf("bot_%d", len(llmapi.SignalChanMap)+1) // what if many rooms?
|
||||
var botname string
|
||||
maxID, err := repo.PlayerGetMaxID(r.Context())
|
||||
if err != nil {
|
||||
log.Warn("failed to get players max id")
|
||||
botname = fmt.Sprintf("bot_%d", len(llmapi.SignalChanMap)+1) // what if many rooms?
|
||||
} else {
|
||||
botname = fmt.Sprintf("bot_%d", maxID+1) // what if many rooms?
|
||||
}
|
||||
_, err = llmapi.NewBot(role, team, botname, fi.Room.ID, cfg, false)
|
||||
if err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
|
@ -40,22 +40,11 @@ func HandleCreateRoom(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
fi.State.RoomID = &room.ID
|
||||
fi.Room = room
|
||||
// if err := repo.RoomCreate(r.Context(), room); err != nil {
|
||||
// log.Error("failed to create a room", "error", err)
|
||||
// abortWithError(w, err.Error())
|
||||
// return
|
||||
// }
|
||||
if err := repo.PlayerSetRoomID(r.Context(), fi.State.Username, room.ID); err != nil {
|
||||
if err := repo.PlayerSetRoomID(r.Context(), room.ID, fi.State.Username); err != nil {
|
||||
log.Error("failed to set room id", "error", err)
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// if err := saveFullInfo(r.Context(), fi); err != nil {
|
||||
// msg := "failed to set current room to session"
|
||||
// log.Error(msg, "error", err)
|
||||
// abortWithError(w, msg)
|
||||
// return
|
||||
// }
|
||||
notify(models.NotifyRoomListUpdate, "")
|
||||
tmpl, err := template.ParseGlob("components/*.html")
|
||||
if err != nil {
|
||||
@ -174,6 +163,7 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
fi.Room.MimeDone = false
|
||||
fi.Room.IsRunning = true
|
||||
fi.Room.IsOver = false
|
||||
fi.Room.TeamTurn = "blue"
|
||||
@ -183,6 +173,8 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.UpdateCounter()
|
||||
fi.Room.TeamWon = ""
|
||||
action := models.Action{
|
||||
RoomID: fi.Room.ID,
|
||||
CreatedAt: time.Now(),
|
||||
Actor: fi.State.Username,
|
||||
ActorColor: string(fi.State.Team),
|
||||
WordColor: string(fi.State.Team),
|
||||
@ -190,17 +182,15 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
// Use the new context with transaction
|
||||
if err := saveFullInfo(ctx, fi); err != nil {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("failed to rollback transaction", "error", err)
|
||||
}
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// if err := saveFullInfo(ctx, fi); err != nil {
|
||||
// if err := tx.Rollback(); err != nil {
|
||||
// log.Error("failed to rollback transaction", "error", err)
|
||||
// }
|
||||
// abortWithError(w, err.Error())
|
||||
// return
|
||||
// }
|
||||
// Save action history
|
||||
action.RoomID = fi.Room.ID
|
||||
action.CreatedAt = time.Now()
|
||||
if err := repo.CreateAction(ctx, &action); err != nil {
|
||||
if err := repo.ActionCreate(ctx, &action); err != nil {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("failed to rollback transaction", "error", err)
|
||||
}
|
||||
@ -220,14 +210,19 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo.RoomUpdate(ctx, fi.Room); err != nil {
|
||||
log.Error("failed to update room", "error", err)
|
||||
// nolint: errcheck
|
||||
tx.Rollback()
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// Commit the transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("failed to commit transaction", "error", err)
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// reveal all cards
|
||||
if fi.State.Role == "mime" {
|
||||
fi.Room.MimeView()
|
||||
@ -338,7 +333,7 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) {
|
||||
Number: num,
|
||||
}
|
||||
fi.Room.ActionHistory = append(fi.Room.ActionHistory, action)
|
||||
if err := repo.CreateAction(r.Context(), &action); err != nil {
|
||||
if err := repo.ActionCreate(r.Context(), &action); err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
@ -348,15 +343,15 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) {
|
||||
fi.Room.ThisTurnLimit = 9
|
||||
}
|
||||
fi.Room.OpenedThisTurn = 0
|
||||
fi.Room.Settings.TurnSecondsLeft = fi.Room.Settings.RoundTime
|
||||
StartTurnTimer(fi.Room.ID, time.Duration(fi.Room.Settings.RoundTime)*time.Second)
|
||||
StartTurnTimer(fi.Room.ID, fi.Room.Settings.RoundTime)
|
||||
log.Debug("given clue", "clue", clue, "limit", fi.Room.ThisTurnLimit)
|
||||
notify(models.NotifyBacklogPrefix+fi.Room.ID, clue+num)
|
||||
notifyBotIfNeeded(fi.Room)
|
||||
// notify(models.NotifyBacklogPrefix+fi.Room.ID, clue+num)
|
||||
notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, clue+num)
|
||||
if err := saveFullInfo(r.Context(), fi); err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
notifyBotIfNeeded(fi.Room)
|
||||
}
|
||||
|
||||
func HandleRenotifyBot(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -24,16 +24,11 @@ func init() {
|
||||
Level: slog.LevelDebug,
|
||||
AddSource: true,
|
||||
}))
|
||||
// memcache = cache.MemCache
|
||||
cfg = config.LoadConfigOrDefault("")
|
||||
Notifier = broker.Notifier
|
||||
// cache.MemCache.StartBackupRoutine(15 * time.Second) // Reduced backup interval
|
||||
// bot loader
|
||||
// check the rooms if it has bot_{digits} in them, create bots if have
|
||||
repo = repos.NewRepoProvider("sqlite3://../gralias.db")
|
||||
// repo = repos.NewRepoProvider("sqlite3://../gralias.db")
|
||||
repo = repos.RP
|
||||
recoverBots()
|
||||
// if player has a roomID, but no team and role, try to recover
|
||||
// recoverPlayers()
|
||||
}
|
||||
|
||||
func HandlePing(w http.ResponseWriter, r *http.Request) {
|
||||
@ -93,32 +88,22 @@ func HandleExit(w http.ResponseWriter, r *http.Request) {
|
||||
creatorLeft = true
|
||||
}
|
||||
exitedRoom := fi.ExitRoom()
|
||||
// if err := saveRoom(exitedRoom); err != nil {
|
||||
// abortWithError(w, err.Error())
|
||||
// return
|
||||
// }
|
||||
if creatorLeft {
|
||||
if err := repo.RoomDeleteByID(r.Context(), exitedRoom.ID); err != nil {
|
||||
log.Error("failed to remove room", "error", err)
|
||||
}
|
||||
// removeRoom(exitedRoom.ID)
|
||||
// TODO: notify users if creator left
|
||||
// and throw them away
|
||||
notify(models.NotifyRoomListUpdate, "")
|
||||
}
|
||||
// scary to update the whole room
|
||||
fiToSave := &models.FullInfo{
|
||||
Room: exitedRoom,
|
||||
}
|
||||
if err := repo.PlayerExitRoom(r.Context(), fi.State.Username); err != nil {
|
||||
log.Error("failed to exit room", "error", err)
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if err := saveFullInfo(r.Context(), fiToSave); err != nil {
|
||||
if err := repo.RoomUpdate(r.Context(), exitedRoom); err != nil {
|
||||
log.Error("failed to update room", "error", err)
|
||||
abortWithError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// fi.List = listRooms(false)
|
||||
fi.List, err = repo.RoomList(r.Context())
|
||||
if err != nil {
|
||||
abortWithError(w, err.Error())
|
||||
|
@ -28,8 +28,8 @@ func GetSession(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sessionCookie, err := r.Cookie(models.AuthCookie)
|
||||
if err != nil {
|
||||
msg := "auth failed; failed to get session token from cookies"
|
||||
log.Debug(msg, "error", err)
|
||||
// msg := "auth failed; failed to get session token from cookies"
|
||||
// log.Debug(msg, "error", err)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -3,76 +3,37 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"gralias/models"
|
||||
"gralias/timer"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type roomTimer struct {
|
||||
ticker *time.Ticker
|
||||
done chan bool
|
||||
}
|
||||
func StartTurnTimer(roomID string, timeLeft uint32) {
|
||||
logger := slog.Default().With("room_id", roomID)
|
||||
|
||||
var (
|
||||
timers = make(map[string]*roomTimer)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func StartTurnTimer(roomID string, duration time.Duration) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, exists := timers[roomID]; exists {
|
||||
return // Timer already running
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
done := make(chan bool)
|
||||
timers[roomID] = &roomTimer{ticker: ticker, done: done}
|
||||
go func() {
|
||||
settings, err := repo.SettingsGetByRoomID(context.Background(), roomID)
|
||||
if err != nil {
|
||||
log.Error("failed to get settings by room id", "error", err)
|
||||
StopTurnTimer(roomID)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if settings.TurnSecondsLeft <= 0 {
|
||||
log.Info("turn time is over", "room_id", roomID)
|
||||
onTurnEnd := func(ctx context.Context, roomID string) {
|
||||
room, err := repo.RoomGetByID(context.Background(), roomID)
|
||||
if err != nil {
|
||||
log.Error("failed to get room by id", "error", err)
|
||||
StopTurnTimer(roomID)
|
||||
logger.Error("failed to get room by id", "error", err)
|
||||
return
|
||||
}
|
||||
logger.Info("turn time is over")
|
||||
room.ChangeTurn()
|
||||
room.MimeDone = false
|
||||
if err := repo.RoomUpdate(context.Background(), room); err != nil {
|
||||
log.Error("failed to save room", "error", err)
|
||||
logger.Error("failed to save room", "error", err)
|
||||
}
|
||||
notify(models.NotifyTurnTimerPrefix+room.ID, strconv.FormatUint(uint64(room.Settings.TurnSecondsLeft), 10))
|
||||
notify(models.NotifyTurnTimerPrefix+room.ID, strconv.FormatUint(uint64(room.Settings.RoundTime), 10))
|
||||
notifyBotIfNeeded(room)
|
||||
StopTurnTimer(roomID)
|
||||
return
|
||||
}
|
||||
settings.TurnSecondsLeft--
|
||||
if err := repo.SettingsUpdate(context.Background(), settings); err != nil {
|
||||
log.Error("failed to update settings", "error", err)
|
||||
|
||||
onTick := func(ctx context.Context, roomID string, currentLeft uint32) {
|
||||
notify(models.NotifyTurnTimerPrefix+roomID, strconv.FormatUint(uint64(currentLeft), 10))
|
||||
}
|
||||
notify(models.NotifyRoomUpdatePrefix+roomID, "")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
timer.StartTurnTimer(context.Background(), roomID, timeLeft, onTurnEnd, onTick, logger)
|
||||
}
|
||||
|
||||
func StopTurnTimer(roomID string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if timer, exists := timers[roomID]; exists {
|
||||
timer.ticker.Stop()
|
||||
close(timer.done)
|
||||
delete(timers, roomID)
|
||||
}
|
||||
timer.StopTurnTimer(roomID)
|
||||
}
|
159
llmapi/main.go
159
llmapi/main.go
@ -19,13 +19,13 @@ import (
|
||||
|
||||
var (
|
||||
// botname -> channel
|
||||
repo = repos.NewRepoProvider("sqlite3://../gralias.db")
|
||||
repo = repos.RP
|
||||
SignalChanMap = make(map[string]chan bool)
|
||||
DoneChanMap = make(map[string]chan bool)
|
||||
// got prompt: control character (\\u0000-\\u001F) found while parsing a string at line 4 column 0
|
||||
MimePrompt = `we are playing alias;\nyou are a mime (player who gives a clue of one noun word and number of cards you expect them to open) of the %s team (people who would guess by your clue want open the %s cards);\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the game info in json:\n%s`
|
||||
GuesserPrompt = `we are playing alias;\nyou are to guess words of the %s team (you want open %s cards) by given clue and a number of meant guesses;\nplease return your guesses and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guesses\": [\"word1\", \"word2\", ...],\n\"could_be\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the cards (and other info), you need to choose revealed==false words:\n%s`
|
||||
GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that left:\n%v`
|
||||
GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that you can choose from:\n%v`
|
||||
MimeSimplePrompt = `we are playing alias;\nyou are to give one word clue and a number of words you mean your team to open; your team words: %v;\nhere are the words of opposite team you want to avoid: %v;\nand here is a black word that is critical not to pick: %s;\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;`
|
||||
)
|
||||
|
||||
@ -50,25 +50,9 @@ func convertToSliceOfStrings(value any) ([]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: unused
|
||||
func (b *Bot) checkGuesses(tempMap map[string]any, room *models.Room) error {
|
||||
guesses, err := convertToSliceOfStrings(tempMap["guesses"])
|
||||
if err != nil {
|
||||
b.log.Warn("failed to parse bot given guesses", "mimeResp", tempMap, "bot_name", b.BotName)
|
||||
return err
|
||||
}
|
||||
for _, word := range guesses {
|
||||
if err := b.checkGuess(word, room); err != nil {
|
||||
// log error
|
||||
b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", word)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) checkGuess(word string, room *models.Room) error {
|
||||
color, exists := room.WCMap[word]
|
||||
// color, exists := room.WCMap[word]
|
||||
color, exists := room.FindColor(word)
|
||||
b.log.Debug("bot trying to open card", "word", word, "color",
|
||||
color, "exists", exists, "limit", room.ThisTurnLimit,
|
||||
"opened", room.OpenedThisTurn)
|
||||
@ -76,8 +60,15 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
|
||||
return fmt.Errorf("fn: checkGuess; %s does not exists", word)
|
||||
}
|
||||
room.RevealSpecificWord(word)
|
||||
if err := repo.WordCardReveal(context.Background(), word, room.ID); err != nil {
|
||||
b.log.Error("failed to reveal word in db", "word", word, "color",
|
||||
color, "exists", exists, "limit", room.ThisTurnLimit,
|
||||
"opened", room.OpenedThisTurn)
|
||||
return err
|
||||
}
|
||||
room.UpdateCounter()
|
||||
action := models.Action{
|
||||
RoomID: room.ID,
|
||||
Actor: b.BotName,
|
||||
ActorColor: b.Team,
|
||||
WordColor: string(color),
|
||||
@ -97,6 +88,7 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
|
||||
room.MimeDone = false
|
||||
room.OpenedThisTurn = 0
|
||||
room.ThisTurnLimit = 0
|
||||
b.StopTurnTimer()
|
||||
}
|
||||
switch string(color) {
|
||||
case string(models.WordColorBlack):
|
||||
@ -107,18 +99,21 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
|
||||
room.OpenedThisTurn = 0
|
||||
room.ThisTurnLimit = 0
|
||||
action := models.Action{
|
||||
RoomID: room.ID,
|
||||
Actor: b.BotName,
|
||||
ActorColor: string(b.Team),
|
||||
WordColor: models.WordColorBlack,
|
||||
Action: models.ActionTypeGameOver,
|
||||
}
|
||||
room.ActionHistory = append(room.ActionHistory, action)
|
||||
b.StopTurnTimer()
|
||||
case string(models.WordColorWhite), string(oppositeColor):
|
||||
// end turn
|
||||
room.TeamTurn = oppositeColor
|
||||
room.MimeDone = false
|
||||
room.OpenedThisTurn = 0
|
||||
room.ThisTurnLimit = 0
|
||||
b.StopTurnTimer()
|
||||
}
|
||||
// check if no cards left => game over
|
||||
if room.BlueCounter == 0 {
|
||||
@ -129,12 +124,14 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
|
||||
room.OpenedThisTurn = 0
|
||||
room.ThisTurnLimit = 0
|
||||
action := models.Action{
|
||||
RoomID: room.ID,
|
||||
Actor: b.BotName,
|
||||
ActorColor: string(b.Team),
|
||||
WordColor: models.WordColorBlack,
|
||||
Action: models.ActionTypeGameOver,
|
||||
}
|
||||
room.ActionHistory = append(room.ActionHistory, action)
|
||||
b.StopTurnTimer()
|
||||
}
|
||||
if room.RedCounter == 0 {
|
||||
// red won
|
||||
@ -144,14 +141,31 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
|
||||
room.OpenedThisTurn = 0
|
||||
room.ThisTurnLimit = 0
|
||||
action := models.Action{
|
||||
RoomID: room.ID,
|
||||
Actor: b.BotName,
|
||||
ActorColor: string(b.Team),
|
||||
WordColor: models.WordColorBlack,
|
||||
Action: models.ActionTypeGameOver,
|
||||
}
|
||||
room.ActionHistory = append(room.ActionHistory, action)
|
||||
b.StopTurnTimer()
|
||||
}
|
||||
if err := saveRoom(room); err != nil {
|
||||
ctx, tx, err := repo.InitTx(context.Background())
|
||||
// nolint: errcheck
|
||||
defer tx.Commit()
|
||||
if err != nil {
|
||||
b.log.Error("failed to init tx", "error", err)
|
||||
}
|
||||
if err := repo.ActionCreate(ctx, &action); err != nil {
|
||||
// nolint: errcheck
|
||||
tx.Rollback()
|
||||
b.log.Error("failed to create action", "error", err, "action", action)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := repo.RoomUpdate(ctx, room); err != nil {
|
||||
// nolint: errcheck
|
||||
tx.Rollback()
|
||||
b.log.Error("failed to save room", "room", room)
|
||||
err = fmt.Errorf("fn: checkGuess, failed to save room; err: %w", err)
|
||||
return err
|
||||
@ -169,13 +183,25 @@ func (b *Bot) BotMove() {
|
||||
b.log.Error("bot loop", "error", err)
|
||||
return
|
||||
}
|
||||
eventName := models.NotifyBacklogPrefix + room.ID
|
||||
// eventName := models.NotifyBacklogPrefix + room.ID
|
||||
eventName := models.NotifyRoomUpdatePrefix + room.ID
|
||||
eventPayload := ""
|
||||
defer func() { // save room
|
||||
// just incase, get the room once more
|
||||
// room, err = repo.RoomGetExtended(context.Background(), b.RoomID)
|
||||
// if err != nil {
|
||||
// b.log.Error("bot loop", "error", err)
|
||||
// return
|
||||
// }
|
||||
if err := saveRoom(room); err != nil {
|
||||
b.log.Error("failed to save room", "error", err)
|
||||
return
|
||||
}
|
||||
if botName := room.WhichBotToMove(); botName != "" {
|
||||
b.log.Debug("notifying bot", "name", botName)
|
||||
SignalChanMap[botName] <- true
|
||||
b.log.Debug("after sending the signal", "name", botName)
|
||||
}
|
||||
broker.Notifier.Notifier <- broker.NotificationEvent{
|
||||
EventName: eventName,
|
||||
Payload: eventPayload,
|
||||
@ -187,13 +213,21 @@ func (b *Bot) BotMove() {
|
||||
// call llm
|
||||
llmResp, err := b.CallLLM(prompt)
|
||||
if err != nil {
|
||||
room.LogJournal = append(room.LogJournal, b.BotName+" send call got error: "+err.Error())
|
||||
room.LogJournal = append(room.LogJournal, models.Journal{
|
||||
Entry: "send call got error: " + err.Error(),
|
||||
Username: b.BotName,
|
||||
RoomID: room.ID,
|
||||
})
|
||||
b.log.Error("bot loop", "error", err)
|
||||
return
|
||||
}
|
||||
tempMap, err := b.LLMParser.ParseBytes(llmResp)
|
||||
if err != nil {
|
||||
room.LogJournal = append(room.LogJournal, b.BotName+" parse resp got error: "+err.Error())
|
||||
room.LogJournal = append(room.LogJournal, models.Journal{
|
||||
Entry: "parse resp got error: " + err.Error(),
|
||||
Username: b.BotName,
|
||||
RoomID: room.ID,
|
||||
})
|
||||
b.log.Error("bot loop", "error", err, "resp", string(llmResp))
|
||||
return
|
||||
}
|
||||
@ -209,6 +243,7 @@ func (b *Bot) BotMove() {
|
||||
return
|
||||
}
|
||||
action := models.Action{
|
||||
RoomID: room.ID,
|
||||
Actor: b.BotName,
|
||||
ActorColor: b.Team,
|
||||
WordColor: b.Team,
|
||||
@ -218,55 +253,73 @@ func (b *Bot) BotMove() {
|
||||
}
|
||||
room.ActionHistory = append(room.ActionHistory, action)
|
||||
room.MimeDone = true
|
||||
meant := fmt.Sprintf(b.BotName+" meant to open: %v", tempMap["words_I_mean_my_team_to_open"])
|
||||
room.LogJournal = append(room.LogJournal, meant)
|
||||
entry := fmt.Sprintf("meant to open: %v", tempMap["words_I_mean_my_team_to_open"])
|
||||
lj := models.Journal{
|
||||
Entry: entry,
|
||||
Username: b.BotName,
|
||||
RoomID: room.ID,
|
||||
}
|
||||
room.LogJournal = append(room.LogJournal, lj)
|
||||
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
||||
b.log.Warn("failed to write to journal", "entry", lj)
|
||||
}
|
||||
eventPayload = mimeResp.Clue + mimeResp.Number
|
||||
guessLimitU64, err := strconv.ParseUint(mimeResp.Number, 10, 8)
|
||||
if err != nil {
|
||||
b.log.Warn("failed to parse bot given limit", "mimeResp", mimeResp, "bot_name", b.BotName)
|
||||
}
|
||||
room.OpenedThisTurn = 0 // in case it is not
|
||||
room.ThisTurnLimit = uint8(guessLimitU64)
|
||||
if room.ThisTurnLimit == 0 {
|
||||
b.log.Warn("turn limit is 0", "mimeResp", mimeResp)
|
||||
room.ThisTurnLimit = 9
|
||||
}
|
||||
if err := repo.ActionCreate(context.Background(), &action); err != nil {
|
||||
b.log.Error("failed to create action", "error", err)
|
||||
return
|
||||
}
|
||||
b.StartTurnTimer(room.Settings.RoundTime)
|
||||
if err := saveRoom(room); err != nil {
|
||||
b.log.Error("failed to save room", "error", err)
|
||||
return
|
||||
}
|
||||
case models.UserRoleGuesser:
|
||||
// // deprecated
|
||||
// if err := b.checkGuesses(tempMap, room); err != nil {
|
||||
// b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName)
|
||||
// continue
|
||||
// }
|
||||
guess, ok := tempMap["guess"].(string)
|
||||
if !ok || guess == "" {
|
||||
b.log.Warn("failed to parse guess", "mimeResp", tempMap, "bot_name", b.BotName)
|
||||
}
|
||||
if err := b.checkGuess(guess, room); err != nil {
|
||||
b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", guess, "error", err)
|
||||
msg := fmt.Sprintf("failed to check guess; mimeResp: %v; bot_name: %s; guess: %s; error: %v", tempMap, b.BotName, guess, err)
|
||||
room.LogJournal = append(room.LogJournal, msg)
|
||||
b.log.Error("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", guess, "error", err)
|
||||
entry := fmt.Sprintf("failed to check guess; mimeResp: %v; guess: %s; error: %v", tempMap, guess, err)
|
||||
lj := models.Journal{
|
||||
Entry: entry,
|
||||
Username: b.BotName,
|
||||
RoomID: room.ID,
|
||||
}
|
||||
room.LogJournal = append(room.LogJournal, lj)
|
||||
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
||||
b.log.Warn("failed to write to journal", "entry", lj)
|
||||
}
|
||||
}
|
||||
b.log.Info("guesser resp log", "guesserResp", tempMap)
|
||||
couldBe, err := convertToSliceOfStrings(tempMap["could_be"])
|
||||
if err != nil {
|
||||
b.log.Warn("failed to parse could_be", "bot_resp", tempMap, "bot_name", b.BotName)
|
||||
}
|
||||
room.LogJournal = append(room.LogJournal, fmt.Sprintf("%s also considered this: %v", b.BotName, couldBe))
|
||||
eventName = models.NotifyRoomUpdatePrefix + room.ID
|
||||
eventPayload = ""
|
||||
// TODO: needs to decide if it wants to open the next cardword or end turn
|
||||
// or end turn on limit
|
||||
entry := fmt.Sprintf("%s guessed: %s; also considered this: %v", b.BotName, guess, couldBe)
|
||||
lj := models.Journal{
|
||||
Entry: entry,
|
||||
Username: b.BotName,
|
||||
RoomID: room.ID,
|
||||
}
|
||||
room.LogJournal = append(room.LogJournal, lj)
|
||||
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
||||
b.log.Warn("failed to write to journal", "entry", lj)
|
||||
}
|
||||
default:
|
||||
b.log.Error("unexpected role", "role", b.Role, "resp-map", tempMap)
|
||||
return
|
||||
}
|
||||
if botName := room.WhichBotToMove(); botName != "" {
|
||||
b.log.Debug("notifying bot", "name", botName)
|
||||
SignalChanMap[botName] <- true
|
||||
}
|
||||
}
|
||||
|
||||
// StartBot
|
||||
@ -294,13 +347,31 @@ func RemoveBot(botName string, room *models.Room) error {
|
||||
// remove role from room
|
||||
room.RemovePlayer(botName)
|
||||
slog.Debug("removing bot player", "name", botName, "room_id", room.ID, "room", room)
|
||||
if err := repo.PlayerDelete(context.Background(), room.ID, botName); err != nil {
|
||||
if err := repo.PlayerDelete(context.Background(), botName); err != nil {
|
||||
slog.Error("failed to remove bot player", "name", botName, "room_id", room.ID)
|
||||
return err
|
||||
}
|
||||
return saveRoom(room)
|
||||
}
|
||||
|
||||
func RemoveBotNoRoom(botName string) error {
|
||||
// channels
|
||||
dc, ok := DoneChanMap[botName]
|
||||
if ok {
|
||||
dc <- true
|
||||
close(DoneChanMap[botName])
|
||||
}
|
||||
sc, ok := SignalChanMap[botName]
|
||||
if ok {
|
||||
close(sc)
|
||||
}
|
||||
// maps
|
||||
delete(DoneChanMap, botName)
|
||||
delete(SignalChanMap, botName)
|
||||
// remove role from room
|
||||
return repo.PlayerDelete(context.Background(), botName)
|
||||
}
|
||||
|
||||
// EndBot
|
||||
|
||||
func NewBot(role, team, name, roomID string, cfg *config.Config, recovery bool) (*Bot, error) {
|
||||
|
49
llmapi/timer.go
Normal file
49
llmapi/timer.go
Normal file
@ -0,0 +1,49 @@
|
||||
package llmapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gralias/broker"
|
||||
"gralias/models"
|
||||
"gralias/repos"
|
||||
"gralias/timer"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (b *Bot) StartTurnTimer(timeLeft uint32) {
|
||||
logger := b.log.With("room_id", b.RoomID)
|
||||
|
||||
onTurnEnd := func(ctx context.Context, roomID string) {
|
||||
room, err := repos.RP.RoomGetByID(context.Background(), roomID)
|
||||
if err != nil {
|
||||
logger.Error("failed to get room by id", "error", err)
|
||||
return
|
||||
}
|
||||
logger.Info("turn time is over")
|
||||
room.ChangeTurn()
|
||||
room.MimeDone = false
|
||||
if err := repos.RP.RoomUpdate(context.Background(), room); err != nil {
|
||||
logger.Error("failed to save room", "error", err)
|
||||
}
|
||||
broker.Notifier.Notifier <- broker.NotificationEvent{
|
||||
EventName: models.NotifyTurnTimerPrefix + room.ID,
|
||||
Payload: strconv.FormatUint(uint64(room.Settings.RoundTime), 10),
|
||||
}
|
||||
// notifyBotIfNeeded(room)
|
||||
if botName := room.WhichBotToMove(); botName != "" {
|
||||
SignalChanMap[botName] <- true
|
||||
}
|
||||
}
|
||||
|
||||
onTick := func(ctx context.Context, roomID string, currentLeft uint32) {
|
||||
broker.Notifier.Notifier <- broker.NotificationEvent{
|
||||
EventName: models.NotifyTurnTimerPrefix + roomID,
|
||||
Payload: strconv.FormatUint(uint64(currentLeft), 10),
|
||||
}
|
||||
}
|
||||
|
||||
timer.StartTurnTimer(context.Background(), b.RoomID, timeLeft, onTurnEnd, onTick, logger)
|
||||
}
|
||||
|
||||
func (b *Bot) StopTurnTimer() {
|
||||
timer.StopTurnTimer(b.RoomID)
|
||||
}
|
5
main.go
5
main.go
@ -25,7 +25,7 @@ func ListenToRequests(port string) *http.Server {
|
||||
server := &http.Server{
|
||||
Handler: handlers.LogRequests(handlers.GetSession(mux)),
|
||||
Addr: ":" + port,
|
||||
ReadTimeout: time.Second * 5, // TODO: to cfg
|
||||
// ReadTimeout: time.Second * 5, // does this timeout conflict with sse connection?
|
||||
WriteTimeout: 0, // sse streaming
|
||||
}
|
||||
fs := http.FileServer(http.Dir("assets/"))
|
||||
@ -62,7 +62,8 @@ func main() {
|
||||
// Setup graceful shutdown
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
repo := repos.NewRepoProvider(cfg.DBPath)
|
||||
// repo := repos.NewRepoProvider(cfg.DBPath)
|
||||
repo := repos.RP
|
||||
defer repo.Close()
|
||||
cm := crons.NewCronManager(repo, slog.Default())
|
||||
cm.Start()
|
||||
|
@ -21,6 +21,7 @@ CREATE TABLE players (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id TEXT, -- nullable
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
team TEXT NOT NULL DEFAULT '', -- 'red' or 'blue'
|
||||
role TEXT NOT NULL DEFAULT '', -- 'guesser' or 'mime'
|
||||
is_bot BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
@ -40,7 +41,6 @@ CREATE TABLE word_cards (
|
||||
CREATE TABLE card_marks (
|
||||
card_id INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
FOREIGN KEY (card_id) REFERENCES word_cards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE,
|
||||
PRIMARY KEY (card_id, username)
|
||||
@ -65,7 +65,6 @@ CREATE TABLE settings (
|
||||
language TEXT NOT NULL DEFAULT 'en',
|
||||
room_pass TEXT NOT NULL DEFAULT '',
|
||||
turn_time INTEGER NOT NULL DEFAULT 60, -- seconds
|
||||
turn_seconds_left INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
@ -78,3 +77,30 @@ CREATE TABLE sessions(
|
||||
username TEXT NOT NULL,
|
||||
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE journal(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
entry TEXT NOT NULL DEFAULT '',
|
||||
username TEXT NOT NULL,
|
||||
room_id TEXT NOT NULL,
|
||||
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE player_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_username TEXT NOT NULL UNIQUE,
|
||||
games_played INTEGER NOT NULL DEFAULT 0,
|
||||
games_won INTEGER NOT NULL DEFAULT 0,
|
||||
games_lost INTEGER NOT NULL DEFAULT 0,
|
||||
opened_opposite_words INTEGER NOT NULL DEFAULT 0,
|
||||
opened_white_words INTEGER NOT NULL DEFAULT 0,
|
||||
opened_black_words INTEGER NOT NULL DEFAULT 0,
|
||||
mime_winrate REAL NOT NULL DEFAULT 0.0,
|
||||
guesser_winrate REAL NOT NULL DEFAULT 0.0,
|
||||
played_as_mime INTEGER NOT NULL DEFAULT 0,
|
||||
played_as_guesser INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (player_username) REFERENCES players(username) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"gralias/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
@ -105,6 +106,7 @@ type Player struct {
|
||||
ID uint32 `json:"id" db:"id"`
|
||||
RoomID *string `json:"room_id" db:"room_id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Password string `json:"-" db:"password"`
|
||||
Team UserTeam `json:"team" db:"team"`
|
||||
Role UserRole `json:"role" db:"role"`
|
||||
IsBot bool `json:"is_bot" db:"is_bot"`
|
||||
@ -127,7 +129,29 @@ type BotPlayer struct {
|
||||
type CardMark struct {
|
||||
CardID uint32 `db:"card_id"`
|
||||
Username string `db:"username"`
|
||||
Active bool `db:"active"`
|
||||
}
|
||||
|
||||
type Journal struct {
|
||||
ID uint32 `db:"id"`
|
||||
Username string `db:"username"`
|
||||
RoomID string `db:"room_id"`
|
||||
Entry string `db:"entry"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
type PlayerStats struct {
|
||||
ID uint32 `db:"id"`
|
||||
PlayerUsername string `db:"player_username"`
|
||||
GamesPlayed int `db:"games_played"`
|
||||
GamesWon int `db:"games_won"`
|
||||
GamesLost int `db:"games_lost"`
|
||||
OpenedOppositeWords int `db:"opened_opposite_words"`
|
||||
OpenedWhiteWords int `db:"opened_white_words"`
|
||||
OpenedBlackWords int `db:"opened_black_words"`
|
||||
MimeWinrate float64 `db:"mime_winrate"`
|
||||
GuesserWinrate float64 `db:"guesser_winrate"`
|
||||
PlayedAsMime int `db:"played_as_mime"`
|
||||
PlayedAsGuesser int `db:"played_as_guesser"`
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
@ -150,16 +174,23 @@ type Room struct {
|
||||
RedTeam Team `db:"-"`
|
||||
BlueTeam Team `db:"-"`
|
||||
Cards []WordCard `db:"-"`
|
||||
WCMap map[string]WordColor `db:"-"`
|
||||
BotMap map[string]BotPlayer `db:"-"`
|
||||
Mark CardMark `db:"-"`
|
||||
LogJournal []string `db:"-"`
|
||||
LogJournal []Journal `db:"-"`
|
||||
Settings GameSettings `db:"-"`
|
||||
}
|
||||
|
||||
func (r *Room) FindColor(word string) (WordColor, bool) {
|
||||
for _, card := range r.Cards {
|
||||
if strings.EqualFold(card.Word, word) {
|
||||
return card.Color, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (r *Room) ClearMarks() {
|
||||
for i, _ := range r.Cards {
|
||||
r.Cards[i].Mark = []CardMark{}
|
||||
for i := range r.Cards {
|
||||
r.Cards[i].Marks = []CardMark{}
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,7 +310,7 @@ func getGuesser(m map[string]BotPlayer, team UserTeam) string {
|
||||
func (r *Room) WhichBotToMove() string {
|
||||
fmt.Println("looking for bot to move", "team-turn:", r.TeamTurn,
|
||||
"mime-done:", r.MimeDone, "bot-map:", r.BotMap, "is_running:", r.IsRunning,
|
||||
"blueMime:", r.BlueTeam.Mime, "redMime:", r.RedTeam.Mime)
|
||||
"blueMime:", r.BlueTeam.Mime, "redMime:", r.RedTeam.Mime, "card-limit:", r.ThisTurnLimit, "opened:", r.OpenedThisTurn)
|
||||
if !r.IsRunning {
|
||||
return ""
|
||||
}
|
||||
@ -366,12 +397,14 @@ func (r *Room) GuesserView() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) RevealSpecificWord(word string) {
|
||||
func (r *Room) RevealSpecificWord(word string) uint32 {
|
||||
for i, card := range r.Cards {
|
||||
if card.Word == word {
|
||||
r.Cards[i].Revealed = true
|
||||
return card.ID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type WordCard struct {
|
||||
@ -381,7 +414,7 @@ type WordCard struct {
|
||||
Color WordColor `json:"color" db:"color"`
|
||||
Revealed bool `json:"revealed" db:"revealed"`
|
||||
Mime bool `json:"mime" db:"mime_view"` // user who sees that card is mime
|
||||
Mark []CardMark `json:"marks" db:"-"`
|
||||
Marks []CardMark `json:"marks" db:"-"`
|
||||
}
|
||||
|
||||
// table: settings
|
||||
@ -390,7 +423,7 @@ type GameSettings struct {
|
||||
RoomID string `db:"room_id"`
|
||||
Language string `json:"language" example:"en" form:"language" db:"language"`
|
||||
RoomPass string `json:"room_pass" db:"room_pass"`
|
||||
TurnSecondsLeft uint32 `db:"turn_seconds_left"`
|
||||
|
||||
RoundTime uint32 `json:"round_time" db:"turn_time"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
@ -432,7 +465,6 @@ type FullInfo struct {
|
||||
}
|
||||
|
||||
func (f *FullInfo) ExitRoom() *Room {
|
||||
// f.Room.PlayerList = utils.RemoveFromSlice(f.State.Username, f.Room.PlayerList)
|
||||
f.Room.RedTeam.Guessers = utils.RemoveFromSlice(f.State.Username, f.Room.RedTeam.Guessers)
|
||||
f.Room.BlueTeam.Guessers = utils.RemoveFromSlice(f.State.Username, f.Room.BlueTeam.Guessers)
|
||||
if f.Room.RedTeam.Mime == f.State.Username {
|
||||
@ -441,8 +473,10 @@ func (f *FullInfo) ExitRoom() *Room {
|
||||
if f.Room.BlueTeam.Mime == f.State.Username {
|
||||
f.Room.BlueTeam.Mime = ""
|
||||
}
|
||||
// f.State.ExitRoom()
|
||||
f.State.RoomID = nil
|
||||
resp := f.Room
|
||||
f.Room = nil
|
||||
return resp
|
||||
}
|
||||
|
||||
// =======
|
||||
|
@ -3,19 +3,21 @@ package repos
|
||||
import (
|
||||
"context"
|
||||
"gralias/models"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type ActionsRepo interface {
|
||||
ListActions(ctx context.Context, roomID string) ([]models.Action, error)
|
||||
CreateAction(ctx context.Context, action *models.Action) error
|
||||
GetLastClue(ctx context.Context, roomID string) (*models.Action, error)
|
||||
DeleteActionsByRoomID(ctx context.Context, roomID string) error
|
||||
ActionsDeleteOrphaned(ctx context.Context) error
|
||||
ActionList(ctx context.Context, roomID string) ([]models.Action, error)
|
||||
ActionCreate(ctx context.Context, action *models.Action) error
|
||||
ActionGetLastClue(ctx context.Context, roomID string) (*models.Action, error)
|
||||
ActionDeleteByRoomID(ctx context.Context, roomID string) error
|
||||
ActionDeleteOrphaned(ctx context.Context) error
|
||||
ActionGetLastTimeByRoomID(ctx context.Context, roomID string) (time.Time, error)
|
||||
}
|
||||
|
||||
func (p *RepoProvider) ListActions(ctx context.Context, roomID string) ([]models.Action, error) {
|
||||
func (p *RepoProvider) ActionList(ctx context.Context, roomID string) ([]models.Action, error) {
|
||||
actions := []models.Action{}
|
||||
err := sqlx.SelectContext(ctx, p.DB, &actions, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? ORDER BY created_at ASC`, roomID)
|
||||
if err != nil {
|
||||
@ -24,13 +26,23 @@ func (p *RepoProvider) ListActions(ctx context.Context, roomID string) ([]models
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) CreateAction(ctx context.Context, a *models.Action) error {
|
||||
func (p *RepoProvider) ActionCreate(ctx context.Context, a *models.Action) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, a.RoomID, a.Actor, a.ActorColor, a.Action, a.Word, a.WordColor, a.Number, a.CreatedAt.UnixNano())
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, a.RoomID, a.Actor, a.ActorColor, a.Action, a.Word, a.WordColor, a.Number, time.Now())
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) GetLastClue(ctx context.Context, roomID string) (*models.Action, error) {
|
||||
func (p *RepoProvider) ActionGetLastTimeByRoomID(ctx context.Context, roomID string) (time.Time, error) {
|
||||
lastTime := time.Time{}
|
||||
err := sqlx.GetContext(ctx, p.DB, &lastTime,
|
||||
`SELECT created_at FROM actions WHERE room_id = ? ORDER BY created_at DESC LIMIT 1`, roomID)
|
||||
if err != nil {
|
||||
return lastTime, err
|
||||
}
|
||||
return lastTime, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) ActionGetLastClue(ctx context.Context, roomID string) (*models.Action, error) {
|
||||
action := &models.Action{}
|
||||
err := sqlx.GetContext(ctx, p.DB, action, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? AND action_type = 'gave_clue' ORDER BY created_at DESC LIMIT 1`, roomID)
|
||||
if err != nil {
|
||||
@ -39,13 +51,13 @@ func (p *RepoProvider) GetLastClue(ctx context.Context, roomID string) (*models.
|
||||
return action, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) DeleteActionsByRoomID(ctx context.Context, roomID string) error {
|
||||
func (p *RepoProvider) ActionDeleteByRoomID(ctx context.Context, roomID string) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, `DELETE FROM actions WHERE room_id = ?`, roomID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) ActionsDeleteOrphaned(ctx context.Context) error {
|
||||
func (p *RepoProvider) ActionDeleteOrphaned(ctx context.Context) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, `DELETE FROM actions WHERE room_id NOT IN (SELECT id FROM rooms)`)
|
||||
return err
|
||||
|
@ -35,7 +35,7 @@ func setupActionsTestDB(t *testing.T) (*sqlx.DB, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsRepo_CreateAction(t *testing.T) {
|
||||
func TestActionsRepo_ActionCreate(t *testing.T) {
|
||||
db, teardown := setupActionsTestDB(t)
|
||||
defer teardown()
|
||||
|
||||
@ -53,7 +53,7 @@ func TestActionsRepo_CreateAction(t *testing.T) {
|
||||
RoomID: roomID,
|
||||
}
|
||||
|
||||
err := repo.CreateAction(context.Background(), action)
|
||||
err := repo.ActionCreate(context.Background(), action)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var retrievedAction models.Action
|
||||
@ -95,7 +95,7 @@ func TestActionsRepo_ListActions(t *testing.T) {
|
||||
_, err = db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action2.Actor, action2.ActorColor, action2.Action, action2.Word, action2.WordColor, action2.Number, action2.CreatedAt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
actions, err := repo.ListActions(context.Background(), roomID)
|
||||
actions, err := repo.ActionList(context.Background(), roomID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 2)
|
||||
assert.Equal(t, action1.Word, actions[0].Word)
|
||||
@ -145,7 +145,7 @@ func TestActionsRepo_GetLastClue(t *testing.T) {
|
||||
_, err = db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action3.Actor, action3.ActorColor, action3.Action, action3.Word, action3.WordColor, action3.Number, action3.CreatedAt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lastClue, err := repo.GetLastClue(context.Background(), roomID)
|
||||
lastClue, err := repo.ActionGetLastClue(context.Background(), roomID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, lastClue)
|
||||
assert.Equal(t, action2.Word, lastClue.Word)
|
||||
@ -170,7 +170,7 @@ func TestActionsRepo_DeleteActionsByRoomID(t *testing.T) {
|
||||
_, err := db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action1.Actor, action1.ActorColor, action1.Action, action1.Word, action1.WordColor, action1.Number, action1.CreatedAt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = repo.DeleteActionsByRoomID(context.Background(), roomID)
|
||||
err = repo.ActionDeleteByRoomID(context.Background(), roomID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var count int
|
||||
@ -178,4 +178,3 @@ func TestActionsRepo_DeleteActionsByRoomID(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
|
44
repos/card_marks.go
Normal file
44
repos/card_marks.go
Normal file
@ -0,0 +1,44 @@
|
||||
package repos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gralias/models"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type CardMarksRepo interface {
|
||||
CardMarksByCardID(ctx context.Context, cardID uint32) ([]models.CardMark, error)
|
||||
CardMarksAdd(ctx context.Context, cm *models.CardMark) error
|
||||
CardMarksRemove(ctx context.Context, cardID uint32, username string) error
|
||||
CardMarksByRoomID(ctx context.Context, roomID string) ([]models.CardMark, error)
|
||||
CardMarksRemoveByRoomID(ctx context.Context, roomID string) error
|
||||
}
|
||||
|
||||
func (r *RepoProvider) CardMarksByCardID(ctx context.Context, cardID uint32) ([]models.CardMark, error) {
|
||||
var cardMarks []models.CardMark
|
||||
err := sqlx.SelectContext(ctx, getDB(ctx, r.DB), &cardMarks, "SELECT * FROM card_marks WHERE card_id = ?", cardID)
|
||||
return cardMarks, err
|
||||
}
|
||||
|
||||
func (r *RepoProvider) CardMarksAdd(ctx context.Context, cm *models.CardMark) error {
|
||||
_, err := getDB(ctx, r.DB).ExecContext(ctx, "INSERT INTO card_marks (card_id, username) VALUES (?, ?)", cm.CardID, cm.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RepoProvider) CardMarksRemove(ctx context.Context, cardID uint32, username string) error {
|
||||
db := getDB(ctx, r.DB)
|
||||
_, err := db.ExecContext(ctx, "DELETE FROM card_marks WHERE card_id = ? AND username = ?", cardID, username)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RepoProvider) CardMarksByRoomID(ctx context.Context, roomID string) ([]models.CardMark, error) {
|
||||
var cardMarks []models.CardMark
|
||||
err := sqlx.SelectContext(ctx, getDB(ctx, r.DB), &cardMarks, "SELECT * FROM card_marks WHERE card_id IN (select id from word_cards where room_id = ?)", roomID)
|
||||
return cardMarks, err
|
||||
}
|
||||
func (r *RepoProvider) CardMarksRemoveByRoomID(ctx context.Context, roomID string) error {
|
||||
db := getDB(ctx, r.DB)
|
||||
_, err := db.ExecContext(ctx, "DELETE FROM card_marks WHERE room_id = ?;", roomID)
|
||||
return err
|
||||
}
|
35
repos/journal.go
Normal file
35
repos/journal.go
Normal file
@ -0,0 +1,35 @@
|
||||
package repos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gralias/models"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type JournalRepo interface {
|
||||
JournalByRoomID(ctx context.Context, roomID string) ([]models.Journal, error)
|
||||
JournalCreate(ctx context.Context, j *models.Journal) error
|
||||
JournalDeleteByRoomID(ctx context.Context, roomID string) error
|
||||
}
|
||||
|
||||
func (p *RepoProvider) JournalByRoomID(ctx context.Context, roomID string) ([]models.Journal, error) {
|
||||
journals := []models.Journal{}
|
||||
err := sqlx.SelectContext(ctx, p.DB, &journals, `SELECT id, created_at, entry, username, room_id FROM journal WHERE room_id = ? ORDER BY created_at ASC`, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return journals, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) JournalCreate(ctx context.Context, j *models.Journal) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO journal (entry, username, room_id) VALUES (?, ?, ?)`, j.Entry, j.Username, j.RoomID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) JournalDeleteByRoomID(ctx context.Context, roomID string) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, `DELETE FROM journal WHERE room_id = ?`, roomID)
|
||||
return err
|
||||
}
|
@ -2,6 +2,7 @@ package repos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gralias/config"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
@ -18,7 +19,11 @@ type AllRepos interface {
|
||||
SessionsRepo
|
||||
WordCardsRepo
|
||||
SettingsRepo
|
||||
CardMarksRepo
|
||||
PlayerStatsRepo
|
||||
JournalRepo
|
||||
InitTx(ctx context.Context) (context.Context, *sqlx.Tx, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
type RepoProvider struct {
|
||||
@ -27,25 +32,39 @@ type RepoProvider struct {
|
||||
pathToDB string
|
||||
}
|
||||
|
||||
func NewRepoProvider(pathToDB string) *RepoProvider {
|
||||
var RP AllRepos
|
||||
|
||||
func init() {
|
||||
cfg := config.LoadConfigOrDefault("")
|
||||
// sqlite3 has lock on write, so we need to have only one connection per whole app
|
||||
// https://github.com/mattn/go-sqlite3/issues/274#issuecomment-232942571
|
||||
RP = NewRepoProvider(cfg.DBPath)
|
||||
}
|
||||
|
||||
func NewRepoProvider(pathToDB string) AllRepos {
|
||||
db, err := sqlx.Connect("sqlite3", pathToDB)
|
||||
if err != nil {
|
||||
slog.Error("Unable to connect to database", "error", err)
|
||||
slog.Error("Unable to connect to database", "error", err, "pathToDB", pathToDB)
|
||||
os.Exit(1)
|
||||
}
|
||||
_, err = db.Exec("PRAGMA foreign_keys = ON;")
|
||||
stmts := []string{
|
||||
"PRAGMA foreign_keys = ON;",
|
||||
"PRAGMA busy_timeout=200;",
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
_, err = db.Exec(stmt)
|
||||
if err != nil {
|
||||
slog.Error("Unable to enable foreign keys", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
slog.Info("Successfully connected to database")
|
||||
// db.SetMaxOpenConns(2)
|
||||
rp := &RepoProvider{
|
||||
DB: db,
|
||||
pathToDB: pathToDB,
|
||||
}
|
||||
|
||||
go rp.pingLoop()
|
||||
|
||||
return rp
|
||||
}
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
package repos
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewRepoProvider(t *testing.T) {
|
||||
// Create a temporary SQLite database file for testing
|
||||
tmpDBFile := "./test_gralias.db"
|
||||
defer os.Remove(tmpDBFile) // Clean up the temporary file after the test
|
||||
|
||||
// Initialize a new RepoProvider
|
||||
repoProvider := NewRepoProvider(tmpDBFile)
|
||||
|
||||
// Assert that the DB connection is not nil
|
||||
assert.NotNil(t, repoProvider.DB, "DB connection should not be nil")
|
||||
|
||||
// Close the database connection
|
||||
err := repoProvider.DB.Close()
|
||||
assert.NoError(t, err, "Error closing database connection")
|
||||
}
|
42
repos/player_stats.go
Normal file
42
repos/player_stats.go
Normal file
@ -0,0 +1,42 @@
|
||||
package repos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gralias/models"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type PlayerStatsRepo interface {
|
||||
GetPlayerStats(ctx context.Context, username string) (*models.PlayerStats, error)
|
||||
UpdatePlayerStats(ctx context.Context, stats *models.PlayerStats) error
|
||||
CreatePlayerStats(ctx context.Context, username string) error
|
||||
}
|
||||
|
||||
func (p *RepoProvider) GetPlayerStats(ctx context.Context, username string) (*models.PlayerStats, error) {
|
||||
stats := &models.PlayerStats{}
|
||||
err := sqlx.GetContext(ctx, p.DB, stats, "SELECT * FROM player_stats WHERE player_username = ?", username)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) UpdatePlayerStats(ctx context.Context, stats *models.PlayerStats) error {
|
||||
_, err := p.DB.NamedExecContext(ctx, `UPDATE player_stats SET
|
||||
games_played = :games_played,
|
||||
games_won = :games_won,
|
||||
games_lost = :games_lost,
|
||||
opened_opposite_words = :opened_opposite_words,
|
||||
opened_white_words = :opened_white_words,
|
||||
opened_black_words = :opened_black_words,
|
||||
mime_winrate = :mime_winrate,
|
||||
guesser_winrate = :guesser_winrate,
|
||||
played_as_mime = :played_as_mime,
|
||||
played_as_guesser = :played_as_guesser
|
||||
WHERE player_username = :player_username`, stats)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) CreatePlayerStats(ctx context.Context, username string) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, "INSERT INTO player_stats (player_username) VALUES (?)", username)
|
||||
return err
|
||||
}
|
@ -12,12 +12,14 @@ type PlayersRepo interface {
|
||||
PlayerGetByName(ctx context.Context, username string) (*models.Player, error)
|
||||
PlayerAdd(ctx context.Context, player *models.Player) error
|
||||
PlayerUpdate(ctx context.Context, player *models.Player) error
|
||||
PlayerDelete(ctx context.Context, roomID, username string) error
|
||||
PlayerSetRoomID(ctx context.Context, username, roomID string) error
|
||||
PlayerDelete(ctx context.Context, username string) error
|
||||
PlayerSetRoomID(ctx context.Context, roomID, username string) error
|
||||
PlayerExitRoom(ctx context.Context, username string) error
|
||||
PlayerListNames(ctx context.Context) ([]string, error)
|
||||
PlayerList(ctx context.Context, isBot bool) ([]models.Player, error)
|
||||
PlayerListAll(ctx context.Context) ([]models.Player, error)
|
||||
PlayerListByRoom(ctx context.Context, roomID string) ([]models.Player, error)
|
||||
PlayerGetMaxID(ctx context.Context) (uint32, error)
|
||||
}
|
||||
|
||||
func (p *RepoProvider) PlayerListNames(ctx context.Context) ([]string, error) {
|
||||
@ -31,7 +33,7 @@ func (p *RepoProvider) PlayerListNames(ctx context.Context) ([]string, error) {
|
||||
|
||||
func (p *RepoProvider) PlayerGetByName(ctx context.Context, username string) (*models.Player, error) {
|
||||
var player models.Player
|
||||
err := sqlx.GetContext(ctx, p.DB, &player, "SELECT id, room_id, username, team, role, is_bot FROM players WHERE username = ?", username)
|
||||
err := sqlx.GetContext(ctx, p.DB, &player, "SELECT id, room_id, username, team, role, is_bot, password FROM players WHERE username = ?", username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -43,21 +45,21 @@ func (p *RepoProvider) PlayerGetByName(ctx context.Context, username string) (*m
|
||||
|
||||
func (p *RepoProvider) PlayerAdd(ctx context.Context, player *models.Player) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, "INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)",
|
||||
player.RoomID, player.Username, player.Team, player.Role, player.IsBot)
|
||||
_, err := db.ExecContext(ctx, "INSERT INTO players (room_id, username, team, role, is_bot, password) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
player.RoomID, player.Username, player.Team, player.Role, player.IsBot, player.Password)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) PlayerUpdate(ctx context.Context, player *models.Player) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, "UPDATE players SET team = ?, role = ? WHERE username = ?",
|
||||
_, err := db.ExecContext(ctx, "UPDATE players SET team = ?, role = ? WHERE username = ?;",
|
||||
player.Team, player.Role, player.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *RepoProvider) PlayerDelete(ctx context.Context, roomID, username string) error {
|
||||
func (p *RepoProvider) PlayerDelete(ctx context.Context, username string) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, "DELETE FROM players WHERE room_id = ? AND username = ?", roomID, username)
|
||||
_, err := db.ExecContext(ctx, "DELETE FROM players WHERE username = ?", username)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -95,6 +97,25 @@ func (p *RepoProvider) PlayerList(ctx context.Context, isBot bool) ([]models.Pla
|
||||
return players, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) PlayerGetMaxID(ctx context.Context) (uint32, error) {
|
||||
var maxID uint32
|
||||
err := sqlx.GetContext(ctx, p.DB, &maxID, "SELECT COALESCE(MAX(id), 0) FROM players")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return maxID, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) PlayerListAll(ctx context.Context) ([]models.Player, error) {
|
||||
var players []models.Player
|
||||
query := "SELECT id, room_id, username, team, role, is_bot FROM players;"
|
||||
err := sqlx.SelectContext(ctx, p.DB, &players, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return players, nil
|
||||
}
|
||||
|
||||
func (p *RepoProvider) PlayerListByRoom(ctx context.Context, roomID string) ([]models.Player, error) {
|
||||
var players []models.Player
|
||||
err := sqlx.SelectContext(ctx, p.DB, &players, "SELECT id, room_id, username, team, role, is_bot FROM players WHERE room_id = ?", roomID)
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) {
|
||||
@ -19,6 +19,7 @@ func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) {
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id TEXT,
|
||||
username TEXT,
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
team TEXT,
|
||||
role TEXT,
|
||||
is_bot BOOLEAN
|
||||
@ -98,7 +99,7 @@ func TestPlayersRepo_DeletePlayer(t *testing.T) {
|
||||
_, err := db.Exec(`INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)`, player.RoomID, player.Username, player.Team, player.Role, player.IsBot)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = repo.PlayerDelete(context.Background(), *player.RoomID, player.Username)
|
||||
err = repo.PlayerDelete(context.Background(), player.Username)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var count int
|
||||
@ -106,3 +107,4 @@ func TestPlayersRepo_DeletePlayer(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ func (p *RepoProvider) RoomCreate(ctx context.Context, r *models.Room) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.ExecContext(ctx, `INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left) VALUES (?, ?, ?, ?, ?)`, r.ID, r.Settings.Language, r.Settings.RoomPass, r.Settings.RoundTime, r.Settings.TurnSecondsLeft)
|
||||
_, err = db.ExecContext(ctx, `INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, r.ID, r.Settings.Language, r.Settings.RoomPass, r.Settings.RoundTime)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ func (p *RepoProvider) RoomUpdate(ctx context.Context, r *models.Room) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.ExecContext(ctx, `UPDATE settings SET language = ?, room_pass = ?, turn_time = ?, turn_seconds_left = ? WHERE room_id = ?`, r.Settings.Language, r.Settings.RoomPass, r.Settings.RoundTime, r.Settings.TurnSecondsLeft, r.ID)
|
||||
_, err = db.ExecContext(ctx, `UPDATE settings SET language = ?, room_pass = ?, turn_time = ? WHERE room_id = ?`, r.Settings.Language, r.Settings.RoomPass, r.Settings.RoundTime, r.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -131,5 +131,12 @@ func (p *RepoProvider) RoomGetExtended(ctx context.Context, id string) (*models.
|
||||
return nil, err
|
||||
}
|
||||
room.Settings = *settings
|
||||
// get log jounral
|
||||
journals := []models.Journal{}
|
||||
err = sqlx.SelectContext(ctx, p.DB, &journals, `SELECT id, created_at, entry, username, room_id FROM journal WHERE room_id = ? ORDER BY created_at ASC`, room.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
room.LogJournal = journals
|
||||
return room, nil
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) (*sqlx.DB, func()) {
|
||||
@ -84,7 +84,6 @@ func setupTestDB(t *testing.T) (*sqlx.DB, func()) {
|
||||
language TEXT NOT NULL DEFAULT 'en',
|
||||
room_pass TEXT NOT NULL DEFAULT '',
|
||||
turn_time INTEGER NOT NULL DEFAULT 60,
|
||||
turn_seconds_left INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
@ -145,12 +144,12 @@ func TestRoomsRepo_CreateRoom(t *testing.T) {
|
||||
assert.Equal(t, room.CreatorName, retrievedRoom.CreatorName)
|
||||
|
||||
var retrievedSettings models.GameSettings
|
||||
err = db.Get(&retrievedSettings, "SELECT id, language, room_pass, turn_time, turn_seconds_left FROM settings WHERE room_id = ?", room.ID)
|
||||
err = db.Get(&retrievedSettings, "SELECT id, language, room_pass, turn_time FROM settings WHERE room_id = ?", room.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, room.Settings.Language, retrievedSettings.Language)
|
||||
assert.Equal(t, room.Settings.RoundTime, retrievedSettings.RoundTime)
|
||||
assert.Equal(t, room.Settings.RoomPass, retrievedSettings.RoomPass)
|
||||
assert.Equal(t, room.Settings.TurnSecondsLeft, retrievedSettings.TurnSecondsLeft)
|
||||
assert.Equal(t, room.Settings.RoundTime, retrievedSettings.RoundTime)
|
||||
}
|
||||
|
||||
func TestRoomsRepo_GetRoomByID(t *testing.T) {
|
||||
@ -184,7 +183,7 @@ func TestRoomsRepo_GetRoomByID(t *testing.T) {
|
||||
// Insert a room directly into the database for testing GetRoomByID
|
||||
_, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink)
|
||||
assert.NoError(t, err)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left) VALUES (?, ?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime, room.Settings.TurnSecondsLeft)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime)
|
||||
assert.NoError(t, err)
|
||||
|
||||
retrievedRoom, err := repo.RoomGetByID(context.Background(), room.ID)
|
||||
@ -246,12 +245,12 @@ func TestRoomsRepo_ListRooms(t *testing.T) {
|
||||
|
||||
_, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room1.ID, room1.CreatedAt, room1.CreatorName, room1.TeamTurn, room1.ThisTurnLimit, room1.OpenedThisTurn, room1.BlueCounter, room1.RedCounter, room1.RedTurn, room1.MimeDone, room1.IsRunning, room1.IsOver, room1.TeamWon, room1.RoomLink)
|
||||
assert.NoError(t, err)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left) VALUES (?, ?, ?, ?, ?)`, room1.ID, room1.Settings.Language, room1.Settings.RoomPass, room1.Settings.RoundTime, room1.Settings.TurnSecondsLeft)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room1.ID, room1.Settings.Language, room1.Settings.RoomPass, room1.Settings.RoundTime)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room2.ID, room2.CreatedAt, room2.CreatorName, room2.TeamTurn, room2.ThisTurnLimit, room2.OpenedThisTurn, room2.BlueCounter, room2.RedCounter, room2.RedTurn, room2.MimeDone, room2.IsRunning, room2.IsOver, room2.TeamWon, room2.RoomLink)
|
||||
assert.NoError(t, err)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left) VALUES (?, ?, ?, ?, ?)`, room2.ID, room2.Settings.Language, room2.Settings.RoomPass, room2.Settings.RoundTime, room2.Settings.TurnSecondsLeft)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room2.ID, room2.Settings.Language, room2.Settings.RoomPass, room2.Settings.RoundTime)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rooms, err := repo.RoomList(context.Background())
|
||||
@ -289,7 +288,7 @@ func TestRoomsRepo_DeleteRoomByID(t *testing.T) {
|
||||
|
||||
_, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink)
|
||||
assert.NoError(t, err)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left) VALUES (?, ?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime, room.Settings.TurnSecondsLeft)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Insert a word card for the room
|
||||
@ -348,21 +347,19 @@ func TestRoomsRepo_UpdateRoom(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink)
|
||||
var err error
|
||||
_, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink)
|
||||
assert.NoError(t, err)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left) VALUES (?, ?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime, room.Settings.TurnSecondsLeft)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime)
|
||||
assert.NoError(t, err)
|
||||
|
||||
room.TeamTurn = "red"
|
||||
room.BlueCounter = 10
|
||||
room.Settings.RoundTime = 120
|
||||
room.Settings.TurnSecondsLeft = 30 // Set a value for turn_seconds_left
|
||||
|
||||
err = repo.RoomUpdate(context.Background(), room)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
|
||||
var updatedRoom models.Room
|
||||
err = db.Get(&updatedRoom, "SELECT * FROM rooms WHERE id = ?", room.ID)
|
||||
assert.NoError(t, err)
|
||||
@ -372,6 +369,6 @@ func TestRoomsRepo_UpdateRoom(t *testing.T) {
|
||||
var updatedSettings models.GameSettings
|
||||
err = db.Get(&updatedSettings, "SELECT * FROM settings WHERE room_id = ?", room.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint32(120), updatedSettings.RoundTime)
|
||||
assert.Equal(t, uint32(30), updatedSettings.TurnSecondsLeft)
|
||||
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ func (p *RepoProvider) SettingsGetByRoomID(ctx context.Context, roomID string) (
|
||||
|
||||
func (p *RepoProvider) SettingsUpdate(ctx context.Context, s *models.GameSettings) error {
|
||||
db := getDB(ctx, p.DB)
|
||||
_, err := db.ExecContext(ctx, `UPDATE settings SET language = ?, room_pass = ?, turn_time = ?, turn_seconds_left = ? WHERE room_id = ?`, s.Language, s.RoomPass, s.RoundTime, s.TurnSecondsLeft, s.RoomID)
|
||||
_, err := db.ExecContext(ctx, `UPDATE settings SET language = ?, room_pass = ?, turn_time = ? WHERE room_id = ?`, s.Language, s.RoomPass, s.RoundTime, s.RoomID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -30,17 +30,17 @@ func TestSettingsRepo_SettingsUpdate(t *testing.T) {
|
||||
Language: "en",
|
||||
RoomPass: "pass123",
|
||||
RoundTime: 60,
|
||||
TurnSecondsLeft: 60,
|
||||
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Insert initial settings
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, turn_seconds_left, created_at) VALUES (?, ?, ?, ?, ?, ?)`, settings.RoomID, settings.Language, settings.RoomPass, settings.RoundTime, settings.TurnSecondsLeft, settings.CreatedAt)
|
||||
_, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time, created_at) VALUES (?, ?, ?, ?, ?)`, settings.RoomID, settings.Language, settings.RoomPass, settings.RoundTime, settings.CreatedAt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update settings
|
||||
settings.RoundTime = 120
|
||||
settings.TurnSecondsLeft = 30
|
||||
|
||||
settings.Language = "ru"
|
||||
|
||||
err = repo.SettingsUpdate(context.Background(), settings)
|
||||
@ -52,6 +52,6 @@ func TestSettingsRepo_SettingsUpdate(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint32(120), updatedSettings.RoundTime)
|
||||
assert.Equal(t, uint32(30), updatedSettings.TurnSecondsLeft)
|
||||
|
||||
assert.Equal(t, "ru", updatedSettings.Language)
|
||||
}
|
||||
|
83
timer/timer.go
Normal file
83
timer/timer.go
Normal file
@ -0,0 +1,83 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TurnEndCallback defines the function signature for actions to be performed when a turn ends.
|
||||
type TurnEndCallback func(ctx context.Context, roomID string)
|
||||
|
||||
// TickCallback defines the function signature for actions to be performed on each timer tick.
|
||||
type TickCallback func(ctx context.Context, roomID string, timeLeft uint32)
|
||||
|
||||
type RoomTimer struct {
|
||||
ticker *time.Ticker
|
||||
done chan bool
|
||||
roomID string
|
||||
onTurnEnd TurnEndCallback
|
||||
onTick TickCallback
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
timers = make(map[string]*RoomTimer)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// StartTurnTimer initializes and starts a new turn timer for a given room.
|
||||
func StartTurnTimer(ctx context.Context, roomID string, timeLeft uint32, onTurnEnd TurnEndCallback, onTick TickCallback, logger *slog.Logger) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if _, exists := timers[roomID]; exists {
|
||||
logger.Debug("trying to launch already running timer", "room_id", roomID)
|
||||
return // Timer already running
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
done := make(chan bool)
|
||||
|
||||
rt := &RoomTimer{
|
||||
ticker: ticker,
|
||||
done: done,
|
||||
roomID: roomID,
|
||||
onTurnEnd: onTurnEnd,
|
||||
onTick: onTick,
|
||||
log: logger,
|
||||
}
|
||||
timers[roomID] = rt
|
||||
|
||||
go func() {
|
||||
currentLeft := timeLeft
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if currentLeft <= 0 {
|
||||
rt.onTurnEnd(ctx, roomID)
|
||||
StopTurnTimer(roomID)
|
||||
return
|
||||
}
|
||||
rt.onTick(ctx, roomID, currentLeft)
|
||||
currentLeft--
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopTurnTimer stops the timer for a given room.
|
||||
func StopTurnTimer(roomID string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if rt, exists := timers[roomID]; exists {
|
||||
rt.ticker.Stop()
|
||||
close(rt.done)
|
||||
delete(timers, roomID)
|
||||
rt.log.Info("timer stopped", "room_id", roomID)
|
||||
}
|
||||
}
|
37
todos.md
37
todos.md
@ -21,6 +21,8 @@
|
||||
- redo card .revealed use: it should mean that card is revealed for everybody, while mime should be able to see cards as is; +
|
||||
- better styles and fluff;
|
||||
- common auth system between sites;
|
||||
- signup vs login;
|
||||
- passwords (to room and to login);
|
||||
===
|
||||
- show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right;
|
||||
- gameover to backlog;
|
||||
@ -28,9 +30,10 @@
|
||||
- ended turn action to backlog;
|
||||
===
|
||||
- clear indication that model (llm) is thinking / answered;
|
||||
- possibly turn markings into parts of names of users (first three letters?);
|
||||
- at game creation list languages and support them at backend;
|
||||
- sql ping goroutine with reconnect on fail;
|
||||
- possibly turn markings into parts of names of users (first three letters?); +
|
||||
- at game creation list languages and support them at backend; +
|
||||
- sql ping goroutine with reconnect on fail; +
|
||||
- player stats: played games, lost, won, rating elo, opened opposite words, opened white words, opened black words.
|
||||
|
||||
#### sse points
|
||||
- clue sse update;
|
||||
@ -61,10 +64,28 @@
|
||||
- there is a clue window for a mime before game started; +
|
||||
- sse hangs / fails connection which causes to wait for cards to open a few seconds (on local machine) (did not reoccur so far);
|
||||
- invite link gets cutoff;
|
||||
- when llm guesses the word it is not removed from a pool of words making it keep guessing it;
|
||||
- bot team does not loses their turn after white card (or limit);
|
||||
- when llm guesses the word it is not removed from a pool of words making it keep guessing it; +
|
||||
- bot team does not loses their turn after white card (or limit); +
|
||||
- name check does not work;
|
||||
- game did not end when all blue cards were open;
|
||||
- bot ends a turn after guessing one word only;
|
||||
- game did not end when all blue cards were open; +
|
||||
- bot ends a turn after guessing one word only; +
|
||||
- sync writing to json cache; what happens now: timer (or other side routine) overwrites old room, while mime making clue; +
|
||||
-----------------
|
||||
- card marks; +
|
||||
- on server recover relaunch guess timer if needed;
|
||||
- start new game: clear last clue; mimedone to false; unload old cards; +
|
||||
- backlog shows white word with opposite color;
|
||||
- bot actions are not recorded; +
|
||||
- bot recieves opp-color clue because of it ^; +
|
||||
- old cards are still around; +
|
||||
|
||||
- sync writing to json cache; what happens now: timer (or other side routine) overwrites old room, while mime making clue;
|
||||
- bot mime makes a clue -> no update in the room for players; +
|
||||
- red moves after bot gave (metal - 3) ended after two guesses (showed in logs, that opened 3. double click on the same card? or somewhere counter is not dropped?); the first guess moved counter to 2, in logs only two requests as expected; +
|
||||
- bot mime gve a blue -> timer did not start; timer should be in third package, maybe in crons; +
|
||||
- marks did not clear after end of the turn (feature?) +
|
||||
|
||||
- start new game satrted timer for a mime; (feature? in other cases mime has no timer);
|
||||
- timer ended and went to 300;
|
||||
- mime sees the clue input out of turn; (eh)
|
||||
- there is a problem of two timers, they both could switch turn, but it is not easy to stop them from llmapi or handlers. +
|
||||
- journal still does not work; +
|
||||
|
Reference in New Issue
Block a user