first version

main
Ugo Finnendahl 1 week ago
commit c22b2b8ccd
  1. 21
      .editorconfig
  2. 50
      .gitignore
  3. 67
      .htaccess
  4. 34
      README.md
  5. 107
      assets/css/src/font.scss
  6. 598
      assets/css/src/style.scss
  7. 1
      assets/css/style.min.css
  8. BIN
      assets/fonts/opensans/open-sans-v40-latin-300.woff2
  9. BIN
      assets/fonts/opensans/open-sans-v40-latin-300italic.woff2
  10. BIN
      assets/fonts/opensans/open-sans-v40-latin-500.woff2
  11. BIN
      assets/fonts/opensans/open-sans-v40-latin-500italic.woff2
  12. BIN
      assets/fonts/opensans/open-sans-v40-latin-600.woff2
  13. BIN
      assets/fonts/opensans/open-sans-v40-latin-600italic.woff2
  14. BIN
      assets/fonts/opensans/open-sans-v40-latin-700.woff2
  15. BIN
      assets/fonts/opensans/open-sans-v40-latin-700italic.woff2
  16. BIN
      assets/fonts/opensans/open-sans-v40-latin-800.woff2
  17. BIN
      assets/fonts/opensans/open-sans-v40-latin-800italic.woff2
  18. BIN
      assets/fonts/opensans/open-sans-v40-latin-italic.woff2
  19. BIN
      assets/fonts/opensans/open-sans-v40-latin-regular.woff2
  20. BIN
      assets/img/YGDC.png
  21. 21
      assets/img/dart-board.svg
  22. 20
      assets/img/dart.svg
  23. BIN
      assets/img/placeholder_person.png
  24. 16
      assets/img/watch.svg
  25. 105
      assets/js/componentPromise.js
  26. 25
      assets/js/components/dialog.js
  27. 22
      assets/js/components/init.js
  28. 17
      assets/js/components/inputElem.js
  29. 17
      assets/js/components/list.js
  30. 14
      assets/js/components/overlay.js
  31. 16
      assets/js/components/plainElem.js
  32. 23
      assets/js/components/playerElem.js
  33. 29
      assets/js/components/playerselect.js
  34. 22
      assets/js/components/squareElem.js
  35. 155
      assets/js/handlers.js
  36. 152
      assets/js/kirby.js
  37. 46
      assets/js/stateMgr.js
  38. 229
      assets/js/views/home.js
  39. 499
      assets/js/views/xoi.js.bak
  40. 463
      assets/js/views/xoi/logic.js
  41. 698
      assets/js/views/xoi/main.js
  42. 15444
      assets/js/vue.esm-browser.js
  43. 8
      assets/js/vueusecore.js
  44. 18
      assets/js/websocket.js
  45. 40
      composer.json
  46. 25
      content/1_members/10_23/member.txt
  47. BIN
      content/1_members/10_23/whatsapp-image-2024-02-26-at-22.54.05-3.jpeg
  48. 1
      content/1_members/10_23/whatsapp-image-2024-02-26-at-22.54.05-3.jpeg.txt
  49. 25
      content/1_members/11_8/member.txt
  50. BIN
      content/1_members/11_8/whatsapp-image-2024-02-26-at-22.54.05-7.jpeg
  51. 1
      content/1_members/11_8/whatsapp-image-2024-02-26-at-22.54.05-7.jpeg.txt
  52. 25
      content/1_members/12_99/member.txt
  53. BIN
      content/1_members/12_99/whatsapp-image-2024-02-26-at-22.54.05-1.jpeg
  54. 1
      content/1_members/12_99/whatsapp-image-2024-02-26-at-22.54.05-1.jpeg.txt
  55. 25
      content/1_members/13_9/member.txt
  56. BIN
      content/1_members/13_9/whatsapp-image-2024-02-26-at-22.54.05-6.jpeg
  57. 1
      content/1_members/13_9/whatsapp-image-2024-02-26-at-22.54.05-6.jpeg.txt
  58. 25
      content/1_members/14_001/member.txt
  59. 25
      content/1_members/15_14/member.txt
  60. BIN
      content/1_members/15_14/whatsapp-image-2024-02-26-at-22.54.05-4.jpeg
  61. 1
      content/1_members/15_14/whatsapp-image-2024-02-26-at-22.54.05-4.jpeg.txt
  62. 25
      content/1_members/16_todo1/member.txt
  63. 25
      content/1_members/17_51/member.txt
  64. 25
      content/1_members/18_todo3/member.txt
  65. 25
      content/1_members/19_4/member.txt
  66. 25
      content/1_members/1_32/member.txt
  67. 25
      content/1_members/20_7/member.txt
  68. 25
      content/1_members/21_77/member.txt
  69. 25
      content/1_members/22_285/member.txt
  70. 25
      content/1_members/23_todo/member.txt
  71. 29
      content/1_members/2_92/member.txt
  72. BIN
      content/1_members/2_92/whatsapp-image-2024-02-26-at-22.54.05-2.jpeg
  73. 1
      content/1_members/2_92/whatsapp-image-2024-02-26-at-22.54.05-2.jpeg.txt
  74. 25
      content/1_members/3_45/member.txt
  75. BIN
      content/1_members/3_45/whatsapp-image-2024-02-26-at-22.54.05-5.jpeg
  76. 1
      content/1_members/3_45/whatsapp-image-2024-02-26-at-22.54.05-5.jpeg.txt
  77. 29
      content/1_members/4_180/member.txt
  78. BIN
      content/1_members/4_180/whatsapp-image-2024-02-26-at-22.54.05-8.jpeg
  79. 1
      content/1_members/4_180/whatsapp-image-2024-02-26-at-22.54.05-8.jpeg.txt
  80. 25
      content/1_members/5_57/member.txt
  81. 25
      content/1_members/6_12/member.txt
  82. 25
      content/1_members/7_26/member.txt
  83. BIN
      content/1_members/7_26/whatsapp-image-2024-02-26-at-22.54.05.jpeg
  84. 1
      content/1_members/7_26/whatsapp-image-2024-02-26-at-22.54.05.jpeg.txt
  85. 25
      content/1_members/8_11/member.txt
  86. 25
      content/1_members/9_13/member.txt
  87. 5
      content/1_members/members.txt
  88. 52
      content/2_seasons/1_season-2024/1_summer-slam/54kd4kbk/xoi.txt
  89. 52
      content/2_seasons/1_season-2024/1_summer-slam/7nykca7i/xoi.txt
  90. 52
      content/2_seasons/1_season-2024/1_summer-slam/her1fj3r/xoi.txt
  91. 52
      content/2_seasons/1_season-2024/1_summer-slam/jm4ju7pi/xoi.txt
  92. 52
      content/2_seasons/1_season-2024/1_summer-slam/l0j8jkts/xoi.txt
  93. 52
      content/2_seasons/1_season-2024/1_summer-slam/lsng5wlg/xoi.txt
  94. 52
      content/2_seasons/1_season-2024/1_summer-slam/m7tb5bac/xoi.txt
  95. 52
      content/2_seasons/1_season-2024/1_summer-slam/mxkfs1yq/xoi.txt
  96. 52
      content/2_seasons/1_season-2024/1_summer-slam/osbkse46/xoi.txt
  97. 52
      content/2_seasons/1_season-2024/1_summer-slam/rtjuyvsv/xoi.txt
  98. 26
      content/2_seasons/1_season-2024/1_summer-slam/tournament.txt
  99. 52
      content/2_seasons/1_season-2024/1_summer-slam/z7nvffvf/xoi.txt
  100. 52
      content/2_seasons/1_season-2024/2_spring-break/13tntlfj/xoi.txt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,21 @@
[*.{css,scss,less,js,json,ts,sass,html,hbs,mustache,phtml,html.twig,md,yml}]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
indent_size = 4
trim_trailing_whitespace = false
[site/templates/**.php]
indent_size = 2
[site/snippets/**.php]
indent_size = 2
[package.json,.{babelrc,editorconfig,eslintrc,lintstagedrc,stylelintrc}]
indent_style = space
indent_size = 2

50
.gitignore vendored

@ -0,0 +1,50 @@
# System files
# ------------
Icon
.DS_Store
# Temporary files
# ---------------
/media/*
!/media/index.html
# Lock files
# ---------------
.lock
# Editors
# (sensitive workspace files)
# ---------------------------
*.sublime-workspace
/.vscode
/.idea
# -------------SECURITY-------------
# NEVER publish these files via Git!
# -------------SECURITY-------------
# Cache Files
# ---------------
/site/cache/*
!/site/cache/index.html
# Accounts
# ---------------
/site/accounts/*
!/site/accounts/index.html
# Sessions
# ---------------
/site/sessions/*
!/site/sessions/index.html
# License
# ---------------
/site/config/.license

@ -0,0 +1,67 @@
# Kirby .htaccess
# revision 2023-07-22
# rewrite rules
<IfModule mod_rewrite.c>
# enable awesome urls. i.e.:
# http://yourdomain.com/about-us/team
RewriteEngine on
# make sure to set the RewriteBase correctly
# if you are running the site in a subfolder;
# otherwise links or the entire site will break.
#
# If your homepage is http://yourdomain.com/mysite,
# set the RewriteBase to:
#
# RewriteBase /mysite
# In some environments it's necessary to
# set the RewriteBase to:
#
# RewriteBase /
# block files and folders beginning with a dot, such as .git
# except for the .well-known folder, which is used for Let's Encrypt and security.txt
RewriteRule (^|/)\.(?!well-known\/) index.php [L]
# block all files in the content folder from being accessed directly
RewriteRule ^content/(.*) index.php [L]
# block all files in the site folder from being accessed directly
RewriteRule ^site/(.*) index.php [L]
# block direct access to Kirby and the Panel sources
RewriteRule ^kirby/(.*) index.php [L]
# make site links work
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) index.php [L]
</IfModule>
# pass the Authorization header to PHP
SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1
# compress text file responses
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</IfModule>
# set security headers in all responses
<IfModule mod_headers.c>
# serve files as plain text if the actual content type is not known
# (hardens against attacks from malicious file uploads)
Header set Content-Type "text/plain" "expr=-z %{CONTENT_TYPE}"
Header set X-Content-Type-Options "nosniff"
</IfModule>

@ -0,0 +1,34 @@
<img src="http://getkirby.com/assets/images/github/plainkit.jpg" width="300">
**Kirby: the CMS that adapts to any project, loved by developers and editors alike.**
The Plainkit is a minimal Kirby setup with the basics you need to start a project from scratch. It is the ideal choice if you are already familiar with Kirby and want to start step-by-step.
You can learn more about Kirby at [getkirby.com](https://getkirby.com).
### Try Kirby for free
You can try Kirby and the Plainkit on your local machine or on a test server as long as you need to make sure it is the right tool for your next project. … and when you’re convinced, [buy your license](https://getkirby.com/buy).
### Get going
Read our guide on [how to get started with Kirby](https://getkirby.com/docs/guide/quickstart).
You can [download the latest version](https://github.com/getkirby/plainkit/archive/main.zip) of the Plainkit.
If you are familiar with Git, you can clone Kirby's Plainkit repository from Github.
git clone https://github.com/getkirby/plainkit.git
## What's Kirby?
- **[getkirby.com](https://getkirby.com)** – Get to know the CMS.
- **[Try it](https://getkirby.com/try)** – Take a test ride with our online demo. Or download one of our kits to get started.
- **[Documentation](https://getkirby.com/docs/guide)** – Read the official guide, reference and cookbook recipes.
- **[Issues](https://github.com/getkirby/kirby/issues)** – Report bugs and other problems.
- **[Feedback](https://feedback.getkirby.com)** – You have an idea for Kirby? Share it.
- **[Forum](https://forum.getkirby.com)** – Whenever you get stuck, don't hesitate to reach out for questions and support.
- **[Discord](https://chat.getkirby.com)** – Hang out and meet the community.
- **[Mastodon](https://mastodon.social/@getkirby)** – Spread the word.
- **[Instagram](https://www.instagram.com/getkirby/)** – Share your creations: #madewithkirby.
---
© 2009-2022 Bastian Allgeier
[getkirby.com](https://getkirby.com) · [License agreement](https://getkirby.com/license)

@ -0,0 +1,107 @@
/* open-sans-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url('../fonts/opensans/open-sans-v40-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: url('../fonts/opensans/open-sans-v40-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('../fonts/opensans/open-sans-v40-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url('../fonts/opensans/open-sans-v40-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: normal;
font-weight: 500;
src: url('../fonts/opensans/open-sans-v40-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: italic;
font-weight: 500;
src: url('../fonts/opensans/open-sans-v40-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: url('../fonts/opensans/open-sans-v40-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: url('../fonts/opensans/open-sans-v40-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url('../fonts/opensans/open-sans-v40-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: url('../fonts/opensans/open-sans-v40-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: url('../fonts/opensans/open-sans-v40-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* open-sans-800italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: url('../fonts/opensans/open-sans-v40-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

@ -0,0 +1,598 @@
////////////////////////////////////////////////////////////////////////////////
// SCSS
////////////////////////////////////////////////////////////////////////////////
$border: 0.5vh;
$orange: orange;
$shadow: 0.2rem 0.2rem 0.4rem #000;
$text-shadow: 0.1rem 0.1rem 0.2rem rgba(0,0,0,0.8);
$bg: #214d1d;
$darker: rgba(0,0,0,0.3);
$bgcolor: #2f6f2a;
$line: 0.2rem;
@mixin border {
border-radius: 0.25em;
border: 0.3em black solid;
}
@mixin button {
@include border;
cursor: pointer;
background-color: $darker;
}
@mixin button-text{
color: white;
font-weight: bold;
padding: 0.3em 0.5em;
text-align: center;
display: flex;
align-items: flex-end;
text-shadow: $shadow;
font-family: "Open Sans";
}
////////////////////////////////////////////////////////////////////////////////
// Meyerweb Reset
////////////////////////////////////////////////////////////////////////////////
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
// line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
////////////////////////////////////////////////////////////////////////////////
// General
////////////////////////////////////////////////////////////////////////////////
@import "./font";
*{
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root{
font-size: Min(1.111111111vw, 1.481481481vh);
}
body{
margin: 0;
font-family: "Open Sans";
background-color: black;
// background-color: $bgcolor;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
display: flex;
flex-direction: column;
overflow: hidden;
}
input{
font-family: "Open Sans";
}
main{
border-left: 0.2rem solid black;
border-right: 0.2rem solid black;
height: 67.5rem; /* 1080"px" */
width: 90rem; /* 1440"px" */
background-color: $bgcolor;
display: flex;
flex-direction: column;
}
h1,h2,h3,h4,h5,h6{
font-weight: bold;
}
.game .body{
height:48rem;
}
////////////////////////////////////////////////////////////////////////////////
// Simple class defs
////////////////////////////////////////////////////////////////////////////////
.center{
display: flex;
justify-content: center;
}
.hidden{
display: none !important;
}
.label{
font-size: 1.8em;
padding: 0.3em 0;
font-weight: bold;
display: block;
}
////////////////////////////////////////////////////////////////////////////////
// Content
////////////////////////////////////////////////////////////////////////////////
.overlay{
@include border;
position: absolute;
top: 50%;
left: 50%;
z-index: 1000;
transform: translate(-50%,-50%);
width: 45rem;
max-height: 60rem;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0; /* Safari and Chrome */
}
background-color: $bgcolor;
padding: 2em 1.7em;
}
.dialog{
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: auto;
h2{
font-size: 2.4em;
margin-bottom: 0.5em;
text-shadow: $shadow;
}
p{
font-size: 1.4em;
margin-bottom: 1em;
text-shadow: $shadow;
}
}
body.home{
header{
display: flex;
justify-content: center;
align-items: center;
height: 42%;
&>img{
display: block;
width: 50%;
height: auto;
}
}
h1{
text-align: center;
color: white;
font-size: 2.6rem;
text-shadow: $shadow;
}
.menu{
flex-grow: 1;
display: flex;
align-items: center;
padding-bottom: 7.5rem;
.mainmenu{
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
h1{
margin-bottom: 1em;
}
}
}
}
nav.list{
display: flex;
justify-content: center;
flex-grow: 1;
&.horizontal{
flex-direction: row;
justify-content: space-evenly;
& > *+*{
margin-left: 1em;
}
}
&.vertical{
flex-direction: column;
& > *+*{
margin-top: 1em;
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Elements
////////////////////////////////////////////////////////////////////////////////
.element{
@include border;
border-width: 0.3rem;
background-color: $bg;
cursor: pointer;
&:focus, &:hover{
outline: none;
border-color: $orange;
}
&.input{
font-size: 2em;
padding: 0.3em;
background-color: white;
color: black;
display: block;
width: 100%;
}
&.player{
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.7em;
h2{
font-size: 1.3em;
}
&>*{
margin-right: 1em;
}
img{
height: 3em;
aspect-ratio: 1/1;
object-position: top;
object-fit: cover;
}
}
&.plain{
@include button-text;
padding: 1rem 2rem;
justify-content: center;
align-items: center;
box-shadow: 0.25rem 0.25rem 1rem rgba(0,0,0, 0.3);
font-size: 1.6em;
&:hover,&:focus{
background-color: rgba(0, 0, 0, 0.2);
border-color: $orange;
outline: none;
}
&.back{
background-color: #5E716A;
}
&.new{
background-color: darken($bg, 5%);
}
}
&.player {
height: 4.5em;
}
&.square{
font-size: 1rem;
width: 20em;
height: 20em;
padding: 2.5em 1.25em;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: $bg;
box-shadow: 0.25em 0.25em 1em rgba(0,0,0, 0.3);
cursor: pointer;
h2{
@include button-text;
font-size: 2em;
padding: 0.6em 0 0 0;
}
&:hover,&:focus{
background-color: rgba(0, 0, 0, 0.2);
border-color: $orange;
outline: none;
}
.icon{
width: 65%;
height: auto;
svg{
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
fill: white;
filter: drop-shadow($shadow);
}
img{
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
object-position: top;
filter: drop-shadow($shadow);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Game Setup
////////////////////////////////////////////////////////////////////////////////
body.xoi{
main{
display: flex;
justify-content: center;
.gamesetup{
width: 65%;
margin: 0 auto;
h1{
font-size: 3em;
text-align: center;
margin-bottom: 0.5em;
}
.menu{
& > * {
margin-bottom: 1.3em;
}
}
}
.playerselect{
& > h2{
font-size: 1.8em;
margin-bottom: 0.7em;
text-align: center;
}
}
}
main .xoi{
display: grid;
height: 100%;
grid-template-columns: 3fr 3fr 2fr 3fr 3fr;
grid-template-rows: 11.5rem 52rem 4rem;
grid-template-areas:
"player1 toGo1 score toGo2 player2"
"player1 game game game player2"
"navi navi navi navi navi"
;
background-color: black;
.bigToGo{
margin: $line;
background-color: white;
border: 0.7rem solid black;
color: black;
font-weight: bold;
font-size: 7.5em;
display: flex;
justify-content: center;
align-items: center;
&.active{
border-color: $orange;
}
&.one{
grid-area: toGo1;
}
&.two{
grid-area: toGo2;
}
}
.score{
margin: $line 0;
grid-area: score;
color: white;
text-shadow: $text-shadow;
background-color: $bgcolor;
display: flex;
flex-direction: column;
justify-content: space-evenly;
&>div{
/*sets and legs*/
display: flex;
justify-content: space-evenly;
align-items: center;
h2,h3{
margin:0;
text-align: center;
font-size: 1.3rem;
}
h3{
font-size: 1.1rem;
}
&>h2{
font-size: 3.2rem;
}
h2:nth-child(2){
order:10;
}
}
}
.player{
background-color: $bgcolor;
margin-bottom: $line/2;
&.player1{
grid-area: player1;
}
&.player2{
grid-area: player2;
}
img{
width: 100%;
aspect-ratio: 35/45;
object-fit: cover;
}
h2, h3{
text-align: center;
margin: 0.2em;
text-shadow: $text-shadow;
}
h2{
font-size: 1.8em;
}
h3{
font-size: 1.2em;
}
.stats{
display: grid;
grid-template-columns: auto 1fr 1fr 1fr;
grid-auto-rows: auto;
margin: 1em 0.6em;
text-shadow: $text-shadow;
.row{
display: contents;
font-size: 1.4em;
&.header{
font-weight: bold;
}
:nth-child(2), :nth-child(3), :nth-child(4){
text-align: center;
}
}
}
}
.game{
grid-area: game;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 4.72727272rem 47.272727273rem;
&>div{
display: grid;
grid-template-columns: 4fr 4fr 2fr 4fr 4fr;
grid-auto-rows: 4.72727272rem;
overflow-y: scroll;
grid-auto-flow: dense;
height: 100%;
&::-webkit-scrollbar {
width: 0; /* Safari and Chrome */
}
&>div{
font-size: 2rem;
text-align: center;
background: white;
color: black;
margin: 0 0 $line $line;
width: calc(100% - $line);
height: calc(100% - $line);
border: 0.15em transparent solid;
display: flex;
justify-content: center;
align-items: center;
&.headding{
background-color: $bgcolor;
color: white;
text-shadow: $text-shadow;
}
&.rounds{
grid-column: 3;
background-color: $bgcolor;
color: white;
text-shadow: $text-shadow;
}
&.player1.points{
grid-column: 1;
}
&.player1.toGo{
grid-column: 2;
font-size: 1.6rem;
}
&.player2.points{
grid-column: 4;
}
&.player2.toGo{
grid-column: 5;
margin-right: $line;
// width: calc(100% - 2*$line);
// height: calc(100% - $line);
font-size: 1.6rem;
}
&.input{
padding: 0;
border: 0.15em orange solid;
input{
text-align: center;
box-sizing: border-box;
font-family: inherit;
padding: 0.1em;
border: none;
width: 100%;
height: 100%;
font-size: 1em;
&:focus{
outline: none;
}
}
}
}
&>div.headding{
&.rounds{
font-size: 1.4rem;
}
}
}
}
.nav{
grid-area: navi;
background-color: $bgcolor;
margin-top: $line/2;
display: flex;
justify-content: space-evenly;
align-items: center;
span{
font-size: 2em;
}
}
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 459.428 459.428" xml:space="preserve">
<g>
<path d="M349.792,157.708l-19.856,19.856c9.316,17.136,14.62,36.652,14.62,57.459c0,66.232-53.924,120.156-120.156,120.156
s-120.156-53.924-120.156-120.156c0-66.232,53.924-120.156,120.156-120.156c20.808,0,40.324,5.304,57.459,14.62l19.856-19.856
c-22.508-13.94-48.96-21.964-77.316-21.964c-81.26,0-147.356,66.096-147.356,147.356S143.14,382.38,224.4,382.38
s147.356-66.096,147.356-147.355C371.756,206.669,363.731,180.217,349.792,157.708z M294.644,212.925l-23.868,23.801
c-0.884,24.887-21.283,44.742-46.375,44.742c-25.636,0-46.444-20.807-46.444-46.443c0-25.092,19.856-45.492,44.744-46.375
l23.868-23.8c-7.004-2.244-14.416-3.468-22.167-3.468c-40.596,0-73.644,33.048-73.644,73.644s33.048,73.645,73.644,73.645
s73.644-33.049,73.644-73.645C298.044,227.34,296.888,219.861,294.644,212.925z M416.771,119.629l-19.855,19.856
c15.708,28.288,24.684,60.86,24.684,95.54c0,108.732-88.468,197.201-197.2,197.201S27.2,343.757,27.2,235.024
c0-108.732,88.468-197.2,197.2-197.2c34.68,0,67.251,8.976,95.54,24.684l19.856-19.856C306.067,22.321,266.56,10.625,224.4,10.625
C100.64,10.625,0,111.265,0,235.024s100.64,224.4,224.4,224.4s224.4-100.641,224.4-224.4
C448.8,192.865,437.104,153.357,416.771,119.629z M387.301,120.207l-25.963-2.883L233.431,245.226
c-5.311,5.311-13.92,5.311-19.231,0c-5.311-5.312-5.311-13.92,0-19.231L342.101,98.093l-2.883-25.962l72.128-72.128l9.615,38.468
l38.467,9.615L387.301,120.207z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<g>
<path class="st0" d="M83.644,366.639c-4.438,4.438-4.438,11.699,0,16.137l6.93,6.93l-88.76,88.758
c-0.932,0.934-1.781,2.812-1.807,4.133c-0.521,27.691,25.732,35.035,39.111,21.656l83.002-83l6.982,6.98
c4.426,4.426,11.604,4.426,16.029,0l34.805-34.805l-61.541-61.539L83.644,366.639z"/>
<rect x="129.881" y="318.794" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 56.7051 700.3479)" class="st0" width="87.03" height="39.273"/>
<path class="st0" d="M260.923,282.448l142.992-142.992c5.326-5.324,5.687-13.754,0.722-18.719l-13.549-13.551
c-4.965-4.965-13.394-4.601-18.72,0.727L229.376,250.901l-6.98-6.98c-4.396-4.395-11.844-4.188-16.602,0.57l-38.941,38.942
l61.541,61.539l38.941-38.941c4.758-4.758,5.018-12.152,0.57-16.598L260.923,282.448z"/>
<path class="st0" d="M220.013,227.026l122.041-122.039l-0.364-3.472c-0.939-8.902-10.084-87.316-42.414-98.769
c-11.484-4.066-23.646,0.207-36.166,12.722c-57.9,57.902-45.217,190.605-44.646,196.226L220.013,227.026z"/>
<path class="st0" d="M510.512,213.979c-11.453-32.328-89.871-41.476-98.773-42.414l-3.471-0.367L286.226,293.241l15.336,1.551
c5.621,0.57,138.33,13.254,196.23-44.648l0.014-0.012C510.304,237.628,514.577,225.464,510.512,213.979z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 512 512" xml:space="preserve">
<g>
<path class="st0" d="M485.234,116.625H261.906l69.719-69.719c1.75-1.75,1.75-4.563,0-6.313l-17.422-17.438
c-1.734-1.75-4.563-1.75-6.297,0l-89.688,89.688l-89.656-89.688c-1.75-1.75-4.563-1.75-6.313,0l-17.438,17.438
c-1.75,1.75-1.75,4.563,0,6.313l69.75,69.719H26.766c-14.781,0-26.766,12-26.766,26.781v319.969
c0,14.781,11.984,26.781,26.766,26.781h458.469c14.781,0,26.766-12,26.766-26.781V143.406
C512,128.625,500.016,116.625,485.234,116.625z M383.594,421.188c0,8.531-6.906,15.438-15.422,15.438H66.844 c-8.531,0-15.438-6.906-15.438-15.438V191.875c0-8.531,6.906-15.438,15.438-15.438h301.328c8.516,0,15.422,6.906,15.422,15.438
V421.188z M473.188,333.813h-45.125v-45.125h45.125V333.813z M449.047,234.156c-13.906,0-25.172-11.281-25.172-25.188
s11.266-25.188,25.172-25.188s25.172,11.281,25.172,25.188S462.953,234.156,449.047,234.156z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,105 @@
const html = (v) => { return v[0] };
export const renderer = {
props: ['stack'],
setup(props) {
},
template: html`
<Suspense>
<template v-for="states, i in stack">
<component @keydown.esc.stop="states[states.length-1].reject(-1)" v-if="states.length > 0" :is="states[states.length-1].component" :stack="stack" v-bind="states[states.length-1].properties" @resolve="(e) => states[states.length-1].resolve(e)" @reject="(e) => states[states.length-1].reject(e)" :active="stack.length-1 == i"></component>
</template>
</Suspense>
`
}
function reGet(stack) {
return safePromise(new Promise(function(resolve, reject) {
const lastState = stack[stack.length-1];
lastState[lastState.length-1].resolve = resolve;
lastState[lastState.length-1].reject = reject;
}));
}
export function popLastElem(stack) {
const elem = stack[stack.length-1].pop();
if (stack[stack.length-1].length == 0) {
stack.pop();
}
return elem;
}
function safePromise(promise) {
return promise.then(data => [ data, undefined ]).catch(error => [ null, error != undefined ? error : -1 ]);
}
export function overlayAndGet(component, properties, stack, reGetFlag=false) {
if (reGetFlag) return reGet(stack);
properties["stack"] = stack;
return safePromise(new Promise(function(resolve, reject) {
stack.push([{
"component": component,
"properties": properties,
"resolve": resolve,
"reject": reject
}])
}));
}
export function replaceAndGet(component, properties, stack, reGetFlag=false) {
if (reGetFlag) return reGet(stack);
properties["stack"] = stack;
return safePromise(new Promise(function(resolve, reject) {
stack[stack.length-1].push({
"component": component,
"properties": properties,
"resolve": resolve,
"reject": reject
})
}));
}
export async function overlayAndPop(component, properties, stack){
const ret = await overlayAndGet(component, properties, stack);
popLastElem(stack);
return ret;
}
export async function replaceAndPop(component, properties, stack){
const ret = await replaceAndGet(component, properties, stack);
popLastElem(stack);
return ret;
}
export const powerStateMachine = async (stateMachine, stack, initState=0, initInput=undefined) => {
let stateHistory = [initState];
let resultHistory = [initInput];
let reGet = false;
let newState, newResult;
while (stateHistory[stateHistory.length-1] != undefined) {
const state = stateHistory[stateHistory.length-1];
const result = resultHistory[resultHistory.length-1];
[newState, newResult] = await stateMachine[state](result, reGet);
if (newState == -1){
reGet = true;
stateHistory.pop();
resultHistory.pop();
if (newResult != undefined) {
resultHistory[resultHistory.length-1] = newResult;
}
} else if (newState == state){
reGet = true;
resultHistory[resultHistory.length-1] = newResult;
} else if (newState == undefined) {
return newResult;
} else {
reGet = false;
stateHistory.push(newState);
resultHistory.push(newResult);
}
}
}

@ -0,0 +1,25 @@
import { ref, computed } from "vue";
import { getId } from "../handlers.js";
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js";
const html = (v) => { return v[0] };
export default {
props: ['active', 'title', 'text', 'buttons', "withshortkey", "stack"],
setup(props, { emit }) {
const handlers = [ArrowHorizontalKeyHandler];
if (props.withshortkey) {
handlers.push(NumberKeyHandler);
}
handleActive(props, handlers);
const elements = computed(() => props.buttons.map((i) => { i.onClick = () => emit('resolve', i.result ); return i; }) );
return { elements }
},
template: html`
<div class="dialog overlay" v-if="active">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<d-list :elements="elements" type="horizontal" :withshortkey="withshortkey"></d-list>
</div>
`
}

@ -0,0 +1,22 @@
import list from "./list.js";
import playerElem from "./playerElem.js";
import plainElem from "./plainElem.js";
import squareElem from "./squareElem.js";
import inputElem from "./inputElem.js";
import playerselect from "./playerselect.js";
// import overlay from "./overlay.js";
import dialog from "./dialog.js";
export default (app) => {
app.component("d-list", list);
app.component("d-playerElem", playerElem);
app.component("d-plainElem", plainElem);
app.component("d-squareElem", squareElem);
app.component("d-inputElem", inputElem);
app.component("d-playerselect", playerselect);
app.component("d-dialog", dialog);
}

@ -0,0 +1,17 @@
import { onMounted, ref, computed, watch, nextTick } from "vue";
const html = (v) => { return v[0] };
export default {
props: ['modelValue'],
setup(props) {
return { window }
},
template: html`
<input class="input element" :value="modelValue"
@input="$emit('update:modelValue', $event.target.value)" @focus="window.setTimeout (function(){
$event.target.select();
},1);">
`
}

@ -0,0 +1,17 @@
import { ref, computed } from "vue";
import { getId } from "../handlers.js";
const html = (v) => { return v[0] };
export default {
props: ['elements', 'type', "withshortkey"],
setup(props) {
return { }
},
template: html`
<nav class="list" :class="{ horizontal: type == 'horizontal', vertical: type != 'horizontal'}">
<component :is="item.component" v-for="(item, index) in elements" v-index :withshortkey="withshortkey" @click="item.onClick" v-bind="item.props">
</component>
</nav>
`
}

@ -0,0 +1,14 @@
const html = (v) => { return v[0] };
// Not needed anymore
export default {
props: ['component', "props", "active", "stack"],
setup(props) {
return { }
},
template: html`
<div class="overlay">
<component :is="component" v-bind="props" :active="active" :stack="stack" @resolve="(e) => $emit('resolve', e)" @reject="(e) => $emit('reject', e)"></component>
</div>
`
}

@ -0,0 +1,16 @@
import { computed, ref } from "vue";
import { getId } from "../handlers.js";
const html = (v) => { return v[0] };
export default {
props: ['text', 'withshortkey', 'autofocus'],
setup(props) {
const el = ref(null);
const suffix = computed(() => props.withshortkey ? ` (${getId(el.value)})`: "");
return { suffix, el }
},
template: html`
<div class="plain element" ref="el" v-autofocus="autofocus">{{ text }}{{ suffix }}</div>
`
}

@ -0,0 +1,23 @@
import { onMounted, ref, computed, watch, nextTick } from "vue";
const html = (v) => { return v[0] };
export default {
props: ['player', 'id'],
setup(props) {
return { }
},
template: html`
<div class="player element">
<template v-if="player !== undefined">
<img :src="player.img ? player.img : '/assets/img/placeholder_person.png'">
<h2 class="name">{{ player.forename }} {{ player.surname }}</h2>
<h3 class="nickname">{{ player.nickname }}</h3>
</template>
<template v-if="player === undefined">
<h2 class="name">No Player</h2>
<h3 class="nickname">&nbsp;</h3>
</template>
</div>
`
}

@ -0,0 +1,29 @@
import { nextTick, reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js";
const html = (v) => { return v[0] };
export default {
props: ['players', 'stack', 'active', 'input'],
setup(props, context) {
handleActive(props, [ArrowVerticalKeyHandler]);
const children = computed(() => {
return props.players.map((p) => {
return {
component: "d-playerElem",
props: {"player": p},
onClick: () => {
context.emit('resolve', p);
}
}
})
});
return { children }
},
template: html`
<div class="playerselect">
<!-- <h2>Select Player</h2> -->
<d-list title="Select Player" :withshortkey="true" :elements="children"/>
</div>
`
}

@ -0,0 +1,22 @@
import { computed, ref } from "vue";
import { getId } from "../handlers.js";
const html = (v) => { return v[0] };
export default {
props: ["icon", "text", "withshortkey"],
setup(props) {
const el = ref(null);
const suffix = computed(() => props.withshortkey ? ` (${getId(el.value)})`: "");
return { suffix, el }
},
template: html`
<div class="square element" ref="el">
<div class="icon">
<inject-svg v-if="icon.endsWith('.svg')" :src="icon"></inject-svg>
<img v-if="!icon.endsWith('.svg')" :src="icon">
</div>
<h2>{{ text }}{{ suffix }}</h2>
</div>
`
}

@ -0,0 +1,155 @@
import { ref, watch, nextTick, onBeforeUnmount } from 'vue';
import { state } from "./stateMgr.js";
function getNumberFromKeyEvent(event) {
if (event.keyCode >= 96 && event.keyCode <= 105) {
return event.keyCode - 96;
} else if (event.keyCode >= 48 && event.keyCode <= 57) {
return event.keyCode - 48;
}
return null;
}
export const ArrowHorizontalKeyHandler = (event) => {
if (event.key == "ArrowRight") {
const idx = state.sortedElements.indexOf(String(state.activeIndex));
state.activeIndex = state.sortedElements[(idx+1)%state.sortedElements.length];
} else if (event.key == "ArrowLeft") {
const idx = state.sortedElements.indexOf(String(state.activeIndex))
state.activeIndex = state.sortedElements[(idx+state.sortedElements.length-1)%state.sortedElements.length];
}
}
export const ArrowVerticalKeyHandler = (event) => {
if (event.key == "ArrowDown") {
const idx = state.sortedElements.indexOf(String(state.activeIndex))
state.activeIndex = state.sortedElements[(idx+1)%state.sortedElements.length];
event.preventDefault();
} else if (event.key == "ArrowUp") {
const idx = state.sortedElements.indexOf(String(state.activeIndex))
state.activeIndex = state.sortedElements[(idx+state.sortedElements.length-1)%state.sortedElements.length];
event.preventDefault();
}
}
export const NumberKeyHandler = (event) => {
let i;
if ((i = getNumberFromKeyEvent(event)) != null) {
if (state.elements[i] != undefined) {
event.stopPropagation();
event.preventDefault();
state.activeIndex = i;
state.elements[i].focus();
state.elements[i].click();
}
}
}
export const EnterHandler = (event) => {
if (event.key == "Enter"){
if (state.hasOwnProperty("activeElement") && state.activeElement) {
event.stopPropagation();
if (state.activeElement.tagName != "BUTTON"){
event.preventDefault();
}
state.activeElement.click();
}
}
}
export const handleActive = (props, keydownHandlers) => {
const ownCopy = keydownHandlers.map((f) => (e) => f(e));
const currActive = ref(1);
state.activeIndex = 1;
const handle = () => {
if (props.active){
state.activeIndex = currActive.value;
for (var i = 0; i < keydownHandlers.length; i++) {
document.addEventListener("keydown", ownCopy[i]);
}
} else {
currActive.value = state.activeIndex;
for (var i = 0; i < ownCopy.length; i++) {
document.removeEventListener("keydown", ownCopy[i]);
}
}
}
onBeforeUnmount(() => {
// Cleanup
for (var i = 0; i < ownCopy.length; i++) {
document.removeEventListener("keydown", ownCopy[i]);
}
});
watch(() => props.active, handle);
handle();
}
export const vAutofocus = {
mounted(el, binding, vnode, prevVnode) {
if (binding.value == undefined || binding.value) {
state.activeIndex = getId(el);
}
}
}
export function getId(el) {
for (var key in state.elements) {
if (state.elements[key] == el){
return key;
}
}
}
const add = (el) => {
const focus = () => {
if (state.activeElement !== el) {
state.activeIndex = val;
state.activeElement = el;
}
}
const blur = (e) => {
if (e.relatedTarget === null) {
state.activeElement = undefined;
state.activeIndex = undefined;
}
}
let val;
if (el.dataset["tabindex"]) {
val = el.dataset["tabindex"];
} else {
val = Object.keys(state.elements).length+1;
}
el.tabIndex = val;
state.elements[val] = el;
el.addEventListener("focus", focus);
el.addEventListener("blur", blur);
el.blurcb = blur;
el.focuscb = focus;
};
const remove = (el) => {
delete el.removeAttribute("tabindex");
delete state.elements[getId(el)]
// el.removeEventListener("blur", el.blurfn);
// el.removeEventListener("focus", el.focusfn);
}
export const vIndex = {
updated(el, binding, vnode, prevVnode) {
if (binding.oldValue != binding.value){
if (binding.oldValue) {
remove(el);
} else {
add(el);
}
}
},
mounted(el, binding, vnode, prevVnode) {
add(el);
},
beforeUnmount(el, binding, vnode, prevVnode) {
remove(el);
}
}

@ -0,0 +1,152 @@
import { ref, reactive, watch, computed } from "vue";
function isIterable(obj) {
// checks for null and undefined
if (obj == null) {
return false;
}
return typeof obj[Symbol.iterator] === 'function';
}
export async function homeAction(options = {}) {
return await ( await fetch("/", {
method: "POST",
headers: {
Accept: "application/json",
},
body: JSON.stringify(options),
})).json();
}
export async function getQuery(query, options = {}) {
return (await ( await fetch("/api/query", {
method: "POST",
headers: {
Accept: "application/json",
// completely unsafe
// Authorization: "Basic "+btoa("api@api.de:H]RcScp];76!-PB")
},
body: JSON.stringify({
query: query,
...options
}),
})).json()).result;
}
async function getKirby(endpoint) {
return (await (await fetch("/api"+endpoint, {
method: "GET",
headers: {
// completely unsafe
Authorization: "Basic "+btoa("api@api.de:H]RcScp];76!-PB")
}
})).json()).data;
// .catch(error => {
// console.log("Error:", error);
// });
}
export async function setKirby(endpoint, data) {
const response = await fetch(`/${endpoint}`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "update",
data: data
}),
});
const ret = await response.json();
return ret;
}
function updateContent(endpoint, data) {
return fetch("/api"+endpoint, {
method: "PATCH",
headers: {
// "X-CSRF" : g_csrf,
// completely unsafe
"Authorization": "Basic "+btoa("api@api.de:H]RcScp];76!-PB")
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(response => {
// console.log(response.data);
})
.catch(error => {
console.log("Error:", error);
});
}
export const convertToPages = async (obj) => {
for(var key in obj){
if (obj[key].hasOwnProperty("link")) {
obj[key] = await kirbyPage(obj[key].link);
} else if (Array.isArray(obj[key]) || typeof obj[key] === 'object' ) {
obj[key] = convertToPages(obj[key]);
}
}
return obj;
}
export const kirbyPage = async (endpoint) => {
if (!endpoint.startsWith("/pages/")){
endpoint = "/pages/"+endpoint.replaceAll("/","+");
}
let patch = false;
const page = new Proxy(reactive({}), {
async set(target, key, value) {
if(value.hasOwnProperty("uuid") && value.uuid.startsWith("page://")){
value = await kirbyPage(value.link);
}
if (Array.isArray(value) || typeof value === 'object' ) {
for(var i in value){
if (value[i].hasOwnProperty("uuid") && value[i].uuid.startsWith("page://")) {
value[i] = await kirbyPage(value[i].link);
}
}
}
target[key] = value;
if (patch) {
const data = {};
if (value.isPage) {
data[key] = "page://"+value.uuid;
} else if (Array.isArray(value) || typeof value === 'object' ) {
data[key] = [];
for (var i in value) {
if (value[i].isPage) {
data[key].push("page://"+value[i].uuid);
} else {
data[key].push(value[i]);
}
}
} else {
data[key] = value;
}
updateContent(endpoint, data);
}
return true;
},
get(target, prop, receiver) {
if (prop == "isPage") {
return true;
}
return target[prop];
}
}
);
const lastData = ref({});
const kData = await getKirby(endpoint);
Object.assign(page, kData.content);
patch = true;
return page;
}

@ -0,0 +1,46 @@
import { reactive, nextTick, watch, computed } from "vue";
export const state = reactive({
_activeIndex: undefined,
get activeIndex(){
return state._activeIndex;
},
set activeIndex(val){
// if (val === state._activeIndex) return;
state._activeIndex = val;
if (val === undefined) return;
state._activeElement = document.querySelector(`[tabindex="${val}"]`);
nextTick(() => {
nextTick(() => {
state._activeElement?.focus();
})
})
},
_activeElement: undefined,
get activeElement(){
return state._activeElement;
},
set activeElement(val){
// if (val === state._activeElement) return;
state._activeElement = val;
if (val === undefined) return;
state.activeIndex = val.tabIndex;
},
elements: {},
sortedElements: computed(() => Object.keys(state.elements)),
init: true,
back: [],
backActive: []
});
watch(() => state.sortedElements, () => {
if (state._activeIndex !== undefined && state.sortedElements.length > 0){
state.activeIndex = state._activeIndex;
}
})
watch(() => state.activeIndex, () => {
if (state.activeIndex === undefined && state.sortedElements.length > 0){
state.activeIndex = state.sortedElements[0];
}
})

@ -0,0 +1,229 @@
import { nextTick, watch, reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { getQuery, homeAction } from "../kirby.js";
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js";
import { overlayAndGet, powerStateMachine, popLastElem } from "../componentPromise.js";
const html = (v) => { return v[0] };
////////////////////////////////////////////////////////////////////////////////
// Components
////////////////////////////////////////////////////////////////////////////////
export const selectMode = {
props: ['active', 'stack'],
setup(props, { emit }) {
handleActive(props, [NumberKeyHandler, ArrowHorizontalKeyHandler]);
const children = reactive([
{
// component: "d-squareElem",
// props:{
// text: "Create Game",
// icon: "/assets/img/dart-board.svg",
// },
// onClick: () => {
// emit("resolve", "create");
// }
// }, {
component: "d-squareElem",
props:{
text: "Play Game",
icon: "/assets/img/dart.svg",
},
onClick: () => {
emit("resolve", "play");
}
}
]);
const canvas = ref([]);
return { children }
},
template: html`
<d-list v-if="active" type="horizontal" :withshortkey="true" :elements="children" />
`
}
export const selectTournament = {
props: ['tournaments', 'active', 'stack'],
setup(props, { emit }) {
handleActive(props, [NumberKeyHandler, ArrowVerticalKeyHandler]);
const children = computed(() => {
const items = [];
for (let i in props.tournaments){
const tournament = props.tournaments[i];
items.push({
component: "d-plainElem",
props: {
text: tournament.title,
},
onClick: () => {
emit("resolve", tournament.id);
}
})
}
items.push({
component: "d-plainElem",
props: {
text: "Back",
class: "back"
},
onClick: () => {
emit("reject");
}
});
return items;
});
return { children }
},
template: html`
<div v-if="active" class="mainmenu">
<h1>Select Tournament</h1>
<d-list style="width: 65%" :withshortkey="true" :elements="children"/>
</div>
`
}
export const selectGame = {
props: ['games', 'active', 'stack'],
setup(props, { emit }) {
handleActive(props, [NumberKeyHandler, ArrowVerticalKeyHandler]);
const children = computed(() => {
const items = [];
items.push({
component: "d-plainElem",
props: {
text: "Create Game",
class: "new"
},
onClick: () => {
emit("resolve", "create");
}
});
for (let i in props.games){
const game = props.games[i];
items.push({
component: "d-plainElem",
props: {
text: game.player[0]?.forename + " vs. " + game.player[1]?.forename,
},
onClick: () => {
emit("resolve", game.id);
}
})
}
items.push({
component: "d-plainElem",
props: {
text: "Back",
class: "back"
},
onClick: () => {
emit("reject");
}
});
return items;
});
return { children }
},
template: html`
<div v-if="active" class="mainmenu">
<h1>Select Game</h1>
<d-list style="width: 65%" :withshortkey="true" :elements="children"/>
</div>
`
}
////////////////////////////////////////////////////////////////////////////////
// State Machine
////////////////////////////////////////////////////////////////////////////////
const stateMachine = (stack) => {
return {
0: async (input, reGet) => {
const [result, error] = await overlayAndGet("d-selectMode", { }, stack, reGet);
if (error != undefined) {
return [0, error];
}
if (result == "play") {
return [1, result];
}
if (result == "create") {
// TODO:
return [1, result];
}
},
1: async (input, reGet) => {
const tournaments = await getQuery("site.find('seasons').getRunningTournaments", {
select: {
title: "page.title",
id: "page.id",
}
});
const [result, error] = await overlayAndGet("d-selectTournament", { "tournaments": tournaments}, stack, reGet);
if (error != undefined) {
popLastElem(stack);
return [-1, error];
}
return [2, result];
},
2: async (input, reGet) => {
const games = await getQuery(`site.find('${input}').getRunningGames`, {
select: {
title: "page.title",
id: "page.id",
uuid: "page.uuid",
player: {
query: "page.players.toPages",
select:{
forename: "page.forename"
}
}
}
});
const [result, error] = await overlayAndGet("d-selectGame", {"games": games}, stack, reGet);
if (error != undefined) {
popLastElem(stack);
return [-1, error];
}
if (result == "create") {
// Create Game and redirect
const ret = await homeAction({
"action": "createGame",
"tournament": input
})
if (ret.status == "ok") {
window.setTimeout(() => {
window.location.href = ret.url;
return false;
},1);
}
return [undefined, undefined];
}
window.setTimeout(() => {
window.location.href = result;
return false;
},1);
return [undefined, undefined];
}
};
}
////////////////////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////////////////////
export const homeHandler = async (stack) => {
const sm = stateMachine(stack);
powerStateMachine(sm, stack);
}
export const initHomeView = (app) => {
app.component("d-selectMode", selectMode).component("d-selectTournament", selectTournament).component('d-selectGame', selectGame)
}

@ -0,0 +1,499 @@
import { nextTick, reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { state } from "../stateMgr.js";
import { getQuery } from "../kirby.js";
import { initSubState, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../handlers.js";
const html = (v) => { return v[0] };
function initView(view){
const back = state.view;
state.back.push(() => { state.view = back });
state.backActive.push(state.activeIndex);
state.view = view;
}
const togo = {
props: ['togo'],
setup(props) {
return { }
},
template: html`
<div class="bigToGo">{{ togo }}</div>
`
}
const game = {
props: ['players', 'currentleg', 'max', 'sendvisit', 'modelValue', 'overlay'],
setup(props, context) {
const length = computed(() => props.currentleg?.visits.length );
const visits = computed(() => props.currentleg? props.currentleg.visits:undefined );
const loop = computed(() => Math.max(length.value?length.value+(length.value?length.value%2:0):0,18) );
const getPlayerVisits = (uuid) => {
const vs = visits.value.filter((v) => v.player == uuid);
if (vs.length < 9) {
for (var i = vs.length; i < 9; i++) {
vs.push({ "sum": "", "toGo":["",""]})
}
} else if (vs.length*2 < visits.value.length) {
vs.push({ "sum": "", "toGo":["",""]});
}
return vs;
}
const check_remove = (event) => {
if (!event.repeat && event.target.value.length < 1) {
context.emit('removeLastVisit', event.target);
}
}
const setup = ref(false);
const setupEnter = () => {
setup.value = true;
}
const confirmEnter = (evt) => {
if (setup.value) {
context.emit('sendvisit', evt.target)
}
setup.value = false;
}
return { length, visits, loop, getPlayerVisits, check_remove, confirmEnter, setupEnter }
},
template: html`
<div class="game">
<div class="headding">
<div class="headding points player1">Points</div>
<div class="headding toGo player1">ToGo</div>
<div class="headding rounds">Round</div>
<div class="headding points player2">Points</div>
<div class="headding toGo player2">ToGo</div>
</div>
<div class="body">
<div class="points player1"></div>
<div class="toGo player1">{{ max }}</div>
<div class="rounds">0</div>
<div class="points player2"></div>
<div class="toGo player2">{{ max }}</div>
<template v-for="j in [1,2]" v-if="players">
<template v-for="visit, i in getPlayerVisits(players[j-1].uuid)">
<template v-if="visit.toGo === undefined">
<div :class="'points player'+j+' input'"><input v-autofocus :value="modelValue"
@input="$emit('update:modelValue', $event.target.value)" @keyup.enter="confirmEnter($event)" @keydown.enter="setupEnter($event)" @keydown.backspace="check_remove($event)" v-if="!overlay"></div>
<div :class="'toGo player'+j"></div>
</template>
<template v-if="visit.toGo !== undefined">
<div :class="'points player'+j">{{ visit.sum }}</div>
<div :class="'toGo player'+j">{{ visit.toGo[j-1] }}</div>
</template>
<div class="rounds" v-if="j == 1">{{ (i+1)*3 }}</div>
</template>
</template>
</div>
</div>
`
}
const overlay = {
props: ['data'],
setup(props, context) {
const keyhandler = (event) => {
ArrowHorizontalKeyHandler(event);
NumberKeyHandler(event);
if (event.key == "Escape") {
context.emit("closeOverlay")
}
}
return { keyhandler }
},
template: html`
<div @keyup="keyhandler" class="overlay" v-if="data !== undefined" :class="{active: data.title != '' && data.text != ''}">
<div class="box">
<h1>{{ data.title }}</h1>
<p>{{ data.text }}</p>
<div class="buttons">
<component v-for="(item, index) in data.buttons" :is="item.type" v-index="index+1" @click="item.onClick" :ref="(el) => { item.ref = el }" v-bind="item.props" v-autofocus="item.autofocus"></component>
</div>
</div>
</div>
`
}
const score = {
props: ['page', 'justlegs', 'currentset', 'currentleg'],
setup(props) {
return { }
},
template: html`
<div class="score">
<div v-if="!justlegs" class="sets">
<h2 v-for="(idx) in [0,1]">{{ currentset?.points[idx] }}</h2>
<div class="info">
<h3>Best of {{ page?.sets }}</h3>
<h2>Sets</h2>
</div>
</div>
<div class="legs">
<h2 v-for="(idx) in [0,1]">{{ currentleg?.points[idx] }}</h2>
<div class="info">
<h3>Best of {{ page?.legs }}</h3>
<h2>Legs</h2>
</div>
</div>
</div>
`
}
const player = {
props: ['player', 'stats', "id"],
setup(props) {
const current_set = computed( () => props.stats?.sets[props.stats.sets.length-1]);
const current_leg = computed( () => current_set.value?.legs[current_set.value.legs.length-1]);
const getAverage = (avg) => {
return avg && avg[1] != 0 ? ((3*avg[0])/avg[1]).toFixed(1) : "-";
}
const getCheckout = (checkout) => {
return checkout && checkout[1] != 0 ? Math.round(1000*checkout[0]/checkout[1])/10 : "- "
}
const getMax = (checkouts) => {
if (checkouts && checkouts.length != 0) {
return Math.max(...checkouts);
}
return "-"
}
return { current_set, current_leg, getAverage, getCheckout, getMax }
},
template: html`
<div class="player">
<img style="width: 100%" :src="player?.img? player.img : '/assets/img/placeholder_person.png'">
<h2 class="name">{{ player?.forename }} {{ player?.surname }}</h2>
<h3 class="nickname">{{ player?.nickname }}</h3>
<div class="stats">
<div class="row header">
<div>Stat</div><div>Match</div><div>Leg</div>
</div>
<div class="row">
<div>Average:</div><div>{{ getAverage(stats?.stats[id].average) }}</div><div>{{ getAverage(current_leg?.stats[id].average) }}</div>
</div>
<div class="row">
<div>First 9:</div><div>{{ getAverage(stats?.stats[id].first9) }}</div><div>{{ getAverage(current_leg?.stats[id].first9) }}</div>
</div>
<div class="row">
<div>60+:</div><div>{{ stats?.stats[id]["60+"] }}</div><div>{{ current_leg?.stats[id]["60+"] }}</div>
</div>
<div class="row">
<div>100+:</div><div>{{ stats?.stats[id]["100+"] }}</div><div>{{ current_leg?.stats[id]["100+"] }}</div>
</div>
<div class="row">
<div>140+:</div><div>{{ stats?.stats[id]["140+"] }}</div><div>{{ current_leg?.stats[id]["140+"] }}</div>
</div>
<div class="row">
<div>180:</div><div>{{ stats?.stats[id]["180"] }}</div><div>{{ current_leg?.stats[id]["180"] }}</div>
</div>
<div class="row">
<div>Checkouts:</div><div>{{ getCheckout(stats?.stats[id].checkouts) }}%</div><div></div>
</div>
<div class="row">
<div>Best Checkout:</div><div>{{ getMax(stats?.stats[id].checkoutPoints) }}</div><div></div>
</div>
</div>
</div>
`
}
export const xoi = {
components: {
"d-togo": togo,
"d-score": score,
"d-game": game,
"d-player": player,
"d-overlay": overlay
},
setup(props, context) {
let page = ref();
const updateGame = (reset) => {
getQuery(`site.find('${state.id}')`, {
select: {
title: "page.title",
id: "page.id",
modus: "page.max",
game: "page.rounds.parseJSON",
stats: "page.stats.parseJSON",
sets: "page.sets",
legs: "page.legs",
players: {
query: "page.players.toPages",
select:{
forename: "page.forename",
surname: "page.surname",
nickname: "page.nickname",
uuid: "page.uuid",
img: "page.pic.toFile?.url"
}
}
}
}).then((res) => {
page.value = res;
if (reset != undefined) {
current_input.value = reset;
}
if (res.stats.winner){
console.log(res.stats);
let winner = "Draw";
if (res.players[0].uuid == res.stats.winner){
winner = res.players[0].forename;
} else if (res.players[1].uuid == res.stats.winner) {
winner = res.players[1].forename;
}
overlay.value = {
"title": "Game Ended",
"text": `The winner is ${winner}`,
"buttons": []
};
}
});
};
updateGame();
let game = computed(() => page.value ? page.value.game: undefined)
let current_set = computed(() => {
if (game.value != undefined) {
return game.value.sets[game.value.sets.length-1]
}
return undefined;
})
let current_leg = computed(() => {
if (current_set.value != undefined) {
return current_set.value.legs[current_set.value.legs.length-1]
}
return undefined;
})
let current_toGo = computed(() => {
if (current_leg.value != undefined) {
if (current_leg.value.visits.length < 2) {
return [page.value.modus, page.value.modus]
} else {
return current_leg.value.visits[current_leg.value.visits.length-2].toGo;
}
}
return [0,0];
})
let current_set_points = computed(() => {
if (current_set.value != undefined) {
return current_set.value.points;
}
return undefined;
});
let current_leg_points = computed(() => {
if (current_leg.value != undefined) {
return current_leg.value.points;
}
return undefined;
});
let current_player = computed(() => {
if (current_leg.value != undefined) {
const len = current_leg.value.visits.length;
return current_leg.value.visits[len-1].player == page.value.players[1].uuid;
}
return undefined;
});
const current_input = ref("");
const overlay = ref();
return { page, state, game, current_set, current_leg, current_toGo, current_set_points, current_leg_points, current_player, updateGame, current_input, overlay }
},
methods: {
async removeLastVisit(){
let last = this.current_leg.visits.length-2 >= 0 ? this.current_leg.visits[this.current_leg.visits.length-2].throws : [""];
last = last.join(",");
const response = await fetch(`/${state.id}`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"action": "deleteLastThrow",
"visit": true,
}),
});
const ret = await response.json();
if (ret.status == "ok"){
this.updateGame(last);
} else {
console.log(ret);
}
},
sum(tr){
const val = tr.trim();;
if (val == "") {
return 0;
}
if (val == "SB"){
return 25;
}
if (val == "DB"){
return 50;
}
if (val[0] == "S" || val[0] == "O" || val[0] == "I"){
return parseFloat(val.substring(1));
}
if (val[0] == "D"){
return 2*parseFloat(val.substring(1));
}
if (val[0] == "T"){
return 3*parseFloat(val.substring(1));
}
if (val[0] == "M"){
return 0;
} else {
// TODO: Check for Na
return parseFloat(val);
}
},
verify(sum){
if (this.current_toGo[this.current_player*1]-sum == 0){
return 0;
} else if (sum > 180 || [179, 178, 176, 175, 173, 172, 169, 166, 163].indexOf(sum) > -1) {
return -1
} else if (this.current_toGo[this.current_player*1]-sum <= 50) {
return 1
}
},
openOverlay(overlay){
this.overlay = overlay;
},
closeOverlay(){
this.overlay = undefined;
},
checkout_question(throws){
const buttons = [];
for (var i = 1; i <= 3; i++) {
const x = i;
buttons.push({
"type": "input",
"props" : {
"type": "button",
"value": `${x} (${x})`
},
"onClick": () => {
this.closeOverlay();
this.checkouttries_question(throws, x, false);
}
})
}
this.openOverlay({
"title": "Congratulations!",
"text": `How many darts did you need?` ,
"buttons": buttons
});
},
checkouttries_question(throws, numDarts, zero=true){
const buttons = [];
for (var i = 1; i <= numDarts; i++) {
const x = i;
buttons.push({
"type": "input",
"props" : {
"type": "button",
"value": `${x} (${x})`
},
"autofocus": (i==1 && !zero),
"onClick": () => {
this.closeOverlay();
this.send_visit(throws, numDarts, x)
}
})
}
if (zero) {
buttons.push({
"type": "input",
"props" : {
"type": "button",
"value": `0 (${numDarts+1})`
},
"autofocus": true,
"onClick": () => {
this.closeOverlay();
this.send_visit(throws, numDarts, 0)
}
})
}
this.openOverlay({
"title": "Checkout Tries",
"text": `How many tries on a Checkout?` ,
"buttons": buttons
});
},
async send_visit(throws, numDarts=3, checkoutTries=0){
const response = await fetch(`/${state.id}`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"action": "addThrows",
"throws": throws.value.split(","),
"checkoutTries": checkoutTries,
"numDarts": numDarts,
"done": true
}),
});
const ret = await response.json();
if (ret.status == "ok"){
this.updateGame("");
} else {
console.log(ret);
}
},
async preprocess_visit(throws){
const tr = throws.value.split(",");
let sum = 0;
tr.forEach((t, i) => {
sum += this.sum(t);
});
const res = this.verify(sum);
if (res == 0){
// Ask for num Darts
this.checkout_question(throws);
return;
} else if (res == 1){
// Ask for checkoutTries
this.checkouttries_question(throws, 3);
return;
} else if (res == -1){
this.openOverlay({
"title": "Impossible",
"text": `A score of ${sum} is not possible`,
"buttons": [{
"type": "input",
"props" : {
"type": "button",
"value": "ok"
},
"onClick" : this.closeOverlay
}]
});
return;
}
this.send_visit(throws);
}
},
template: html`
<div class="xoi">
<d-overlay @closeOverlay="closeOverlay" :data="overlay"></d-overlay>
<d-togo :togo="current_toGo[0]" class="one" :class="{'active' : current_player==0 }"></d-togo>
<d-togo :togo="current_toGo[1]" class="two" :class="{'active' : current_player==1 }"></d-togo>
<d-score :page="page" :justlegs="page && page.sets == 1" :currentset="current_set" :currentleg="current_leg"></d-score>
<d-game :overlay="overlay!=undefined" :players="page?.players" :max="page?.modus" @sendvisit="preprocess_visit" @removeLastVisit="removeLastVisit" :currentleg="current_leg" v-model="current_input"></d-game>
<d-player class="player1" :player="page?.players[0]" :stats="page?.stats" :id="0"></d-player>
<d-player class="player2" :player="page?.players[1]" :stats="page?.stats" :id="1"></d-player>
<div class="navi"></div>
</div>
`
}
export const initXoiView = (app) => {
app.component('d-xoi', xoi)
}

@ -0,0 +1,463 @@
import { computed } from "vue";
function setPoints(page, set){
let points = [0, 0]; // need to get updated for more player
let ret;
const m = page.game.sets.length-1;
for (const i in page.game.sets) {
const set = page.game.sets[i];
const points = legPoints(page, set, set.legs[set.legs.length-1]);
const winner = getWinner(points, page.legs);
if (winner > -1)
points[winner] += 1;
else
return points;
if (set == set){
return points;
}
}
return points;
}
function legPoints(page, set, leg){
let points = [0, 0]; // need to get updated for more player
let ret;
const m = set.legs.length-1;
for (const i in set.legs) {
const l = set.legs[i];
if (l.visits[l.visits.length-1].toGo === undefined) {
return points;
}
if (l.visits[l.visits.length-1].toGo[0] == 0)
points[0] += 1;
else if (l.visits[l.visits.length-1].toGo[1] == 0)
points[1] += 1;
if (l == leg){
return points;
}
}
return points;
}
export const getFinalWinner = (page) => {
const points = setPoints(page, page.sets[page.sets.length-1]);
return getWinner(points, page.sets);
};
export function getGameProps(page, current_set, current_leg) {
const ret = {};
if (current_set === undefined) {
current_set = computed(() => {
if (page.game != undefined) {
return page.game.sets[page.game.sets.length - 1]
}
return undefined;
});
}
ret.current_set = current_set;
if (current_leg === undefined) {
current_leg = computed(() => {
if (current_set.value != undefined) {
return current_set.value.legs[current_set.value.legs.length - 1]
}
return undefined;
});
}
ret.current_leg = current_leg;
const current_toGo = computed(() => {
if (current_leg.value != undefined) {
if (current_leg.value.visits.length < 2) {
return [page.modus, page.modus]
} else {
const end = current_leg.value.visits[current_leg.value.visits.length - 1].toGo;
if (end) return end;
return current_leg.value.visits[current_leg.value.visits.length - 2].toGo;
}
}
return [0, 0]; // need to get updated for more player
});
ret.current_toGo = current_toGo;
const current_set_points = computed(() => {
if (current_set.value != undefined) {
return setPoints(page, current_set.value);
}
return undefined;
});
ret.current_set_points = current_set_points;
const current_leg_points = computed(() => {
if (current_set.value != undefined || current_leg.value != undefined) {
return legPoints(page, current_set.value, current_leg.value);
}
return undefined;
});
ret.current_leg_points = current_leg_points;
const current_player = computed(() => {
if (current_leg.value != undefined) {
const len = current_leg.value.visits.length;
return current_leg.value.visits[len - 1].player == page.players[1].uuid;
}
return undefined;
});
ret.current_player = current_player;
const getVal = (tr) => {
const val = tr.trim();
if (val == "") {
return 0;
}
if (val == "SB") {
return 25;
}
if (val == "DB") {
return 50;
}
if (val[0] == "S" || val[0] == "O" || val[0] == "I") {
return parseFloat(val.substring(1));
}
if (val[0] == "D") {
return 2 * parseFloat(val.substring(1));
}
if (val[0] == "T") {
return 3 * parseFloat(val.substring(1));
}
if (val[0] == "M") {
return 0;
} else {
// TODO: Check for Na
return parseFloat(val);
}
}
const verifySum = (sum) => {
if (sum > 180 || [179, 178, 176, 175, 173, 172, 169, 166, 163].indexOf(sum) > -1) {
return -1;
} else if (current_toGo.value[current_player.value * 1] - sum < 0) {
return -2;
} if (current_toGo.value[current_player.value * 1] - sum == 0) {
// bogey numbers
if (page.out == "Double" && (sum > 170 || [169, 168, 166, 165, 163, 162, 159].indexOf(sum) > -1)) {
return -1;
}
return 0;
} else if (current_toGo.value[current_player.value * 1] - sum <= 50) {
return 1;
}
return 2;
}
const checkVisit = (throws) => {
const tr = throws.split(",");
let sum = 0;
tr.forEach((t, i) => {
sum += getVal(t);
});
const res = verifySum(sum);
return [res, sum];
}
ret.checkVisit = checkVisit;
return ret;
}
function newPlayerStats(player) {
return {
player: player.uuid,
average: [0, 0],
first9: [0, 0],
checkouts: [0, 0],
checkoutPoints: [],
"60+": 0,
"100+": 0,
"140+": 0,
"180": 0
};
}
function addNewSetStats(stats, players) {
stats["sets"].push(newStats(players));
stats["sets"][0]["legs"] = [];
return stats;
}
function addNewLegStats(stats, players) {
stats["sets"].at(-1)["legs"].push(newStats(players));
return stats;
}
function newStats(players) {
const stats = {
stats: []
}
players.forEach((player, i) => {
const n = newPlayerStats(player);
stats.stats.push(n);
});
return stats;
}
export function initStats(page) {
const stats = newStats(page.players);
stats.sets = [];
addNewSetStats(stats, page.players);
addNewLegStats(stats, page.players);
page.stats = stats;
}
function newVisit(playerUUID, round) {
return {
player: playerUUID,
throws: [],
visit: round,
checkoutTries: 0,
numDarts: 0,
};
}
export function initGame(page) {
page.game = {
sets: [{
points: new Array(page.players.length).fill(0),
legs: [{
points: new Array(page.players.length).fill(0),
visits: [newVisit(page.players[0].uuid, 1)]
}]
}]
}
}
function updateStats(page, visit){
const playerUUIDs = page.players.map((p) => p.uuid);
const k = playerUUIDs.indexOf(visit.player);
const todos = [page.stats["stats"][k]];
todos.push(page.stats["sets"].at(-1)["stats"][k]);
todos.push(page.stats["sets"].at(-1)["legs"].at(-1)["stats"][k]);
todos.forEach((value, i) => {
todos[i]["average"][0] += visit["sum"];
todos[i]["average"][1] += visit["numDarts"];
if (visit["visit"] < 4) {
todos[i]["first9"][0] += visit["sum"];
todos[i]["first9"][1] += visit["numDarts"];
}
if (visit["toGo"][k] == 0) {
todos[i]["checkouts"][0] += 1;
todos[i]["checkoutPoints"].push(visit["sum"]);
}
todos[i]["checkouts"][1] += visit["checkoutTries"];
if (visit["sum"] == 180) {
todos[i]["180"] += 1;
} else if (visit["sum"] >= 140) {
todos[i]["140+"] += 1;
} else if (visit["sum"] >= 100) {
todos[i]["100+"] += 1;
} else if (visit["sum"] >= 60) {
todos[i]["60+"] += 1;
}
});
}
function getWinner(points, mode) {
const sorted = [...points].map((e,i) => [e,i]).sort((a, b) => b[0] - a[0]);
const k = points.length;
const sum = points.reduce((a,c) => a+c, 0);
if (sum < mode && sorted[0][0] <= mode/k){
// not over yet
return -1;
}
if (sorted[0][0] == sorted[1][0]){
// Draw
return -2;
}
// winner id
return sorted[0][1];
}
export const formatDate = (d) => {
return `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
};
function addNewSet(page, points){
const playerUUIDs = page.players.map((p) => p.uuid);
const set = page.game.sets[page.game.sets.length-1];
const leg = set.legs[set.legs.length-1];
const k = playerUUIDs.indexOf(set.legs[0].visits[0].player);
const p = playerUUIDs[(k+1)%playerUUIDs.length];
page.game.sets.push({
// points: [...points],
legs: [{
points: Array(playerUUIDs.length).fill(0),
visits: [newVisit(p, 1)]
}]
});
}
function addNewLeg(page){
const playerUUIDs = page.players.map((p) => p.uuid);
const set = page.game.sets[page.game.sets.length-1];
const leg = set.legs[set.legs.length-1];
const k = playerUUIDs.indexOf(leg.visits[0].player);
const p = playerUUIDs[(k+1)%playerUUIDs.length];
page.game.sets[page.game.sets.length-1].legs.push({
// points: [...points],
visits: [newVisit(p, 1)]
});
}
function clearLastVisit(page){
const set = page.game.sets[page.game.sets.length-1];
const leg = set.legs[set.legs.length-1];
const visit = leg.visits[leg.visits.length-1];
const ret = visit["throws"];
visit["throws"] = [];
visit["checkoutTries"] = 0;
visit["numDarts"] = 0;
delete visit["sum"];
delete visit["toGo"];
return ret;
}
export function recalcStats(page){
const set = page.game.sets[page.game.sets.length-1];
// const leg = set.legs[set.legs.length-1];
// const visit = leg.visits[leg.visits.length-1];
initStats(page);
const lastset = page.game["sets"].length-1;
page.game["sets"].forEach((set, i) => {
const lastleg = set.legs.length-1;
set.legs.forEach((leg, j) => {
const lastvisit = leg["visits"].length-1;
leg.visits.forEach((visit, k) => {
if (!(k == lastvisit && j == lastleg && i == lastset)) {
updateStats(page, visit);
}
if (k == lastvisit && j != lastleg) {
addNewLegStats(page.stats, page.players);
}
});
if (j == lastleg && i != lastset) {
addNewSetStats(page.stats, page.players);
}
});
});
}
export function removeLastVisit(page){
const set = page.game.sets[page.game.sets.length-1];
const leg = set.legs[set.legs.length-1];
const visit = leg.visits[leg.visits.length-1];
// delete last visit.
leg["visits"].pop();
if (leg["visits"].length == 0){
set["legs"].pop();
if (set["legs"].length == 0){
page.game["sets"].pop();
if (page.game["sets"].length == 0){
initGame(page);
return;
}
}
}
// clearLastVisit
const ret = clearLastVisit(page);
recalcStats(page);
return ret;
}
export function storeVisit(page, throws, sum, numDarts, tries) {
const set = page.game.sets[page.game.sets.length-1];
const leg = set.legs[set.legs.length-1];
const visit = leg.visits[leg.visits.length-1];
visit["numDarts"] = numDarts;
visit["throws"] = throws;
visit["checkoutTries"] = tries;
visit["sum"] = sum
const playerUUIDs = page.players.map((p) => p.uuid);
const k = playerUUIDs.indexOf(visit["player"]);
let toGo;
if (leg["visits"].length-2 < 0) {
toGo = Array(playerUUIDs.length).fill(page.modus);
} else {
toGo = [...leg["visits"][leg["visits"].length-2]["toGo"]];
}
const rest = toGo[k] - visit["sum"];
if (rest < 0 || (page.out == "Double" && rest == 1)) {
visit["sum"] = 0;
} else {
toGo[k] = rest;
}
visit["toGo"] = toGo;
// update stats
updateStats(page, visit);
let update = [];
if (rest != 0) {
// Normal case...next players turn
const nextPlayer = playerUUIDs[(k+1)%playerUUIDs.length];
const nVisit = newVisit(nextPlayer, visit["visit"]+1*(leg.visits[0]["player"] == nextPlayer));
leg.visits.push(nVisit);
}
else {
// rest == 0 leg finished
const newlegp = legPoints(page, set, leg);
let winner = getWinner(newlegp, page.legs);
if (winner == -1) {
// new Leg
addNewLeg(page);
addNewLegStats(page.stats, page.players);
} else {
// new set?
const newsetp = setPoints(page, set);
winner = getWinner(newsetp, page.sets);
if (winner == -1) {
// new Set
addNewSet(page);
addNewSetStats(page.stats, page.players);
addNewLegStats(page.stats, page.players);
} else {
// ask for continue or game over
if (winner == -2) {
page.stats["winner"] = "DRAW";
} else {
page.stats["winner"] = visit["player"];
}
page.enddate = formatDate(new Date(Date.now()));
return [newsetp, newlegp];
}
}
}
return false;
}
export function endGame(page){
// const set = page.game.sets[page.game.sets.length-1];
// const leg = set.legs[set.legs.length-1];
// const visit = leg.visits[leg.visits.length-1];
// const winner = getWinner(newsetp, page.sets);
// set["points"] = newsetp;
// leg["points"] = newlegp;
}
export function extension(page){
const set = page.game.sets[page.game.sets.length-1];
const leg = set.legs[set.legs.length-1];
// leg["points"] = newlegp;
// new Leg
addNewLeg(page);
addNewLegStats(page.stats, page.players);
}

@ -0,0 +1,698 @@
import { nextTick, watch, reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { getQuery, setKirby } from "../../kirby.js";
import { state } from "../../stateMgr.js";
import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../../handlers.js";
import { overlayAndGet, powerStateMachine, overlayAndPop, popLastElem } from "../../componentPromise.js";
import { getGameProps, initGame, initStats, storeVisit, formatDate, removeLastVisit, extension, getFinalWinner } from "./logic.js";
const html = (v) => { return v[0] };
////////////////////////////////////////////////////////////////////////////////
// Components
////////////////////////////////////////////////////////////////////////////////
const pregame = {
props: ['page','active', 'stack'],
setup(props, context) {
handleActive(props, [ArrowVerticalKeyHandler]);
const selectPlayer = async (i) => {
if (props.active) {
const [result, error] = await overlayAndPop("d-playerselect", { players: props.page.participants, class:"overlay"}, props.stack);
if (error === undefined) {
props.page.players[i] = result;
}
}
}
return { selectPlayer }
},
template: html`
<nav class="gamesetup" :disabled="!active">
<h1>Game Setup</h1>
<div class="menu">
<!-- <div><span class="label">Name:</span><d-inputElem v-index="active" v-model="game.title"></d-inputElem></div> -->
<div><span class="label">Player 1:</span><d-playerElem v-index="active" @click="selectPlayer(0)" :player="page.players[0]"></d-playerElem></div>
<div><span class="label">Player 2:</span><d-playerElem v-index="active" @click="selectPlayer(1)" :player="page.players[1]"></d-playerElem></div>
<div><span class="label">Best of Sets:</span><d-inputElem v-index="active" v-model="page.sets"></d-inputElem></div>
<div><span class="label">Best of Legs (per Set):</span><d-inputElem v-index="active" v-model="page.legs"></d-inputElem></div>
<div><d-plainElem text="Start" v-index="active" @click="$emit('resolve', page)"></d-plainElem></div>
</div>
</nav>
`
}
export const bullselect = {
props: ['players','active', 'stack'],
setup(props, { emit }) {
handleActive(props, [ArrowHorizontalKeyHandler, NumberKeyHandler]);
const children = computed(() => {
const items = [];
for (let i in props.players){
const player = props.players[i];
items.push({
component: "d-squareElem",
props: {
text: player.forename + " " + player.surname,
icon: player.img ? player.img : '/assets/img/placeholder_person.png'
},
onClick: () => {
emit("resolve", player);
}
})
}
return items;
});
return { children }
},
template: html`
<div class="gamesetup">
<h1 class="bull">Who won bull?</h1>
<d-list :withshortkey="true" :elements="children" type="horizontal" />
</div>
`
}
const score = {
props: ['page', 'justlegs', 'current_set_points', 'current_leg_points'],
setup(props) {
return { }
},
template: html`
<div class="score">
<div v-if="!justlegs" class="sets">
<h2 v-for="(idx) in [0,1]">{{ current_set_points[idx] }}</h2>
<div class="info">
<h3>Best of {{ page.sets }}</h3>
<h2>Sets</h2>
</div>
</div>
<div class="legs">
<h2 v-for="(idx) in [0,1]">{{ current_leg_points[idx] }}</h2>
<div class="info">
<h3>Best of {{ page.legs }}</h3>
<h2>Legs</h2>
</div>
</div>
</div>
`
}
const player = {
props: ['page', 'id', 'current_stat'],
setup(props) {
const addStats = (stat1, stat2) => {
return {
average: [stat1.average[0]+stat2.average[0], stat1.average[1]+stat2.average[1]],
first9: [stat1.first9[0]+stat2.first9[0], stat1.first9[1]+stat2.first9[1]],
"60+": stat1["60+"]+stat2["60+"],
"100+": stat1["100+"]+stat2["100+"],
"140+": stat1["140+"]+stat2["140+"],
"180": stat1["180"]+stat2["180"],
"checkouts": [stat1.checkouts[0]+stat2.checkouts[0], stat1.checkouts[1]+stat2.checkouts[1]],
"checkoutPoints": [...stat1.checkoutPoints, ...stat2.checkoutPoints]
}
}
const inspect = !!props.page.enddate;
const tourStats = computed(() => {
if (props.page.tournamentStats.length != 2){
return [
props.page.stats.stats[0],
props.page.stats.stats[1]
];
}
if (inspect) {
return [
props.page.tournamentStats[0],
props.page.tournamentStats[1]
];
}
return [
addStats(props.page.tournamentStats[0], props.page.stats.stats[0]),
addStats(props.page.tournamentStats[1], props.page.stats.stats[1])
];
});
const getAverage = (avg) => {
return avg && avg[1] != 0 ? ((3*avg[0])/avg[1]).toFixed(1) : "-";
}
const getCheckout = (checkout) => {
return checkout && checkout[1] != 0 ? Math.round(1000*checkout[0]/checkout[1])/10 : "- "
}
const getMax = (checkouts) => {
if (checkouts && checkouts.length != 0) {
return Math.max(...checkouts);
}
return "-"
}
const player = computed(() => props.page.players[props.id])
return { tourStats, getAverage, getCheckout, getMax, player }
},
template: html`
<div class="player">
<img style="width: 100%" :src="player.img ? player.img : '/assets/img/placeholder_person.png'">
<h2 class="name">{{ player.forename }} {{ player.surname }}</h2>
<h3 class="nickname">{{ player.nickname }}</h3>
<div class="stats">
<div class="row header">
<div>Stat</div><div>Tnm</div><div>Match</div><div>Leg</div>
</div>
<div class="row">
<div>Avg:</div><div>
{{ getAverage(tourStats[id].average) }}
</div><div>
{{ getAverage(page.stats.stats[id].average) }}
</div><div>
{{ getAverage(current_stat.stats[id].average) }}
</div>
</div>
<div class="row">
<div>First 9:</div><div>
{{ getAverage(tourStats[id].first9) }}
</div><div>
{{ getAverage(page.stats.stats[id].first9) }}
</div><div>
{{ getAverage(current_stat.stats[id].first9) }}
</div>
</div>
<div class="row">
<div>60+:</div><div>{{ tourStats[id]["60+"] }}</div><div>{{ page.stats.stats[id]["60+"] }}</div><div>{{ current_stat.stats[id]["60+"] }}</div>
</div>
<div class="row">
<div>100+:</div><div>{{ tourStats[id]["100+"] }}</div><div>{{ page.stats.stats[id]["100+"] }}</div><div>{{ current_stat.stats[id]["100+"] }}</div>
</div>
<div class="row">
<div>140+:</div><div>{{ tourStats[id]["140+"] }}</div><div>{{ page.stats.stats[id]["140+"] }}</div><div>{{ current_stat.stats[id]["140+"] }}</div>
</div>
<div class="row">
<div>180:</div><div>{{ tourStats[id]["180"] }}</div><div>{{ page.stats.stats[id]["180"] }}</div><div>{{ current_stat.stats[id]["180"] }}</div>
</div>
<div class="row">
<div>Ch. %:</div><div>{{ getCheckout(tourStats[id].checkouts) }}%</div><div>{{ getCheckout(page.stats.stats[id].checkouts) }}%</div><div></div>
</div>
<div class="row">
<div>Best Ch.:</div><div>{{ getMax(tourStats[id].checkoutPoints) }}</div><div>{{ getMax(page.stats.stats[id].checkoutPoints) }}</div><div></div>
</div>
</div>
</div>
`
}
const game = {
props: ['active', 'stack', 'players','current_leg', 'max'],
setup(props, context) {
const visits = computed(() => props.current_leg ? props.current_leg.visits:undefined );
const getPlayerVisits = (uuid) => {
const vs = visits.value.filter((v) => v.player == uuid);
if (vs.length < 9) {
for (var i = vs.length; i < 9; i++) {
vs.push({ "sum": "", "toGo":["",""]})
}
} else if (vs.length*2 < visits.value.length) {
vs.push({ "sum": "", "toGo":["",""]});
}
return vs;
}
return { getPlayerVisits }
},
template: html`
<div class="game">
<div class="headding">
<div class="headding points player1">Points</div>
<div class="headding toGo player1">ToGo</div>
<div class="headding rounds">Round</div>
<div class="headding points player2">Points</div>
<div class="headding toGo player2">ToGo</div>
</div>
<div class="body">
<div class="points player1"></div>
<div class="toGo player1">{{ max }}</div>
<div class="rounds">0</div>
<div class="points player2"></div>
<div class="toGo player2">{{ max }}</div>
<template v-for="j in [1,2]" v-if="players">
<template v-for="visit, i in getPlayerVisits(players[j-1].uuid)">
<template v-if="visit.toGo === undefined">
<div :class="'points player'+j+' input'">
<slot />
</div>
<div :class="'toGo player'+j"></div>
</template>
<template v-if="visit.toGo !== undefined">
<div :class="'points player'+j">{{ visit.sum }}</div>
<div :class="'toGo player'+j">{{ visit.toGo[j-1] }}</div>
</template>
<div class="rounds" v-if="j == 1">{{ (i+1)*3 }}</div>
</template>
</template>
</div>
</div>
`
}
const gameinput = {
props: ['input','active', 'stack'],
setup(props, context) {
handleActive(props, []);
const check_remove = (event) => {
if (!event.repeat && event.target.value.length < 1) {
event.preventDefault();
context.emit('reject', -2);
}
}
const keyhandler = (e) => {
if (e.key == "F1" || e.keyCode == 112) {
e.preventDefault();
context.emit('resolve', "60");
} else if (e.key == "F2" || e.keyCode == 113) {
e.preventDefault();
context.emit('resolve', "45");
} else if (e.key == "F3" || e.keyCode == 114) {
e.preventDefault();
context.emit('resolve', "41");
} else if (e.key == "F4" || e.keyCode == 115) {
e.preventDefault();
context.emit('resolve', "26");
}
}
return { check_remove, keyhandler }
},
template: html`
<input v-index="active" :value="input" @keydown="keyhandler" @keydown.enter.stop="$emit('resolve', $event.target.value)" @keydown.backspace="check_remove($event)">
`
}
const xoi = {
props: ['page', 'active', 'stack', 'inspect'],
components: {
"d-player": player,
"d-score": score,
"d-game": game
},
setup(props, context) {
const gamestack = reactive([]);
const inspectstack = reactive([]);
let current_set, current_leg;
const set_id = ref(props.page.game.sets.length - 1);
const leg_id = ref(props.page.game.sets[set_id.value].legs.length - 1);
let current_stat;
if (props.inspect) {
current_set = computed(() => props.page.game.sets[set_id.value]);
current_leg = computed(() => current_set.value.legs[leg_id.value]);
current_stat = computed( () => props.page.stats?.sets[set_id.value].legs[leg_id.value]);
}
const computedProps = getGameProps(props.page, current_set, current_leg);
if (!props.inspect) {
current_stat = computed( () => props.page.stats?.sets[props.page.stats.sets.length-1].legs[computedProps.current_set.value.legs.length-1]);
}
const mounted = onMounted(async () => {
if (props.inspect) {
} else {
const winner = await gameHandler(gamestack, props.stack, props.page, computedProps)
context.emit('resolve', winner);
}
})
return { ...computedProps, set_id, leg_id, gamestack, inspectstack, current_stat }
},
template: html`
<div class="xoi">
<div class="bigToGo one" :class="{'active' : current_player==0 }">{{ current_toGo[0] }}</div>
<d-score :page="page" :justlegs="page && page.sets == 1" :current_set_points="current_set_points" :current_leg_points="current_leg_points"></d-score>
<div class="bigToGo two" :class="{'active' : current_player==1 }">{{ current_toGo[1] }}</div>
<d-player class="player1" :page="page" :id="0" :current_stat="current_stat"></d-player>
<d-game :active="active" :stack="stack" :players="page.players" :max="page.modus" :current_leg="current_leg" >
<d-renderer :stack="gamestack"></d-renderer>
</d-game>
<d-player class="player2" :page="page" :id="1" :current_stat="current_stat"></d-player>
<div class="nav" v-if="inspect">
<span class="label" v-if="page.sets != 1">Set:</span><d-plainElem :text="set_id+1" v-if="page.sets != 1" v-index="active" @click="set_id = (set_id+1)%page.game.sets.length; leg_id=0"></d-plainElem><span class="label">Leg:</span><d-plainElem style="padding:0.2em 0.5em" :text="leg_id+1" v-index="active" v-autofocus="true" @click="leg_id = (leg_id+1)%current_set.legs.length"></d-plainElem>
</div>
<div class="nav" v-if="!!!inspect">
<span>Shortkeys:</span><span>60 (F1)</span><span>45 (F2)</span><span>41 (F3)</span><span>26 (F4)</span>
</div>
</div>
`
}
////////////////////////////////////////////////////////////////////////////////
// Dialogs
////////////////////////////////////////////////////////////////////////////////
const numDartsDialog = () => {
const buttons = [];
for (var i = 1; i <= 3; i++) {
buttons.push({
"component": "d-plainElem",
"props" : {
"text": `${i}`
},
"result" : i
})
}
return {
"withshortkey": true,
"title": "Congratulations!",
"text": `How many darts did you need?`,
"buttons": buttons
};
}
const numCheckoutTriesDialog = (numDarts, start=1) => {
const buttons = [];
for (var i = start; i <= numDarts; i++) {
buttons.push({
"component": "d-plainElem",
"props" : {
"text": `${i}`,
"autofocus": i == 0,
"data-tabindex": i
},
"result" : i
})
}
return {
"withshortkey": true,
"title": "Checkout Tries",
"text": `How many tries on a Checkout?` ,
"buttons": buttons
};
}
const impossibleDialog = (sum) => {
return {
"withshortkey": true,
"title": "Impossible",
"text": `A score of ${sum} is not possible`,
"buttons": [{
"component": "d-plainElem",
"props" : {
"text": "ok"
},
"result" : "ok"
}]}
}
const gameOverDialog = (winner, points) => {
return {
"withshortkey": true,
"title": `Game Over`,
"text": `${winner != "DRAW" ? "The winner is": ""} ${winner} with ${points[0]}-${points[1]}`,
"buttons": [
{
"component": "d-plainElem",
"props" : {
"text": "End Game"
},
"result" : "end"
},{
"component": "d-plainElem",
"props" : {
"text": "Continue"
},
"result" : "continue"
}]
}
}
////////////////////////////////////////////////////////////////////////////////
// API
////////////////////////////////////////////////////////////////////////////////
function getGame(id){
return getQuery(`site.find('${id}')`, {
select: {
title: "page.title",
id: "page.id",
modus: "page.max.toInt",
out: "page.out",
game: "page.rounds.parseJSON",
stats: "page.stats.parseJSON",
sets: "page.sets.toInt",
legs: "page.legs.toInt",
startdate: "page.Startdate",
enddate: "page.Enddate",
tournamentStats: "page.tournamentStats",
participants: {
query: "page.parent.participants.toPages.sortBy('forename')",
select:{
forename: "page.forename",
surname: "page.surname",
nickname: "page.nickname",
uuid: "page.uuid",
img: "page.pic.toFile?.url"
}
},
players: {
query: "page.players.toPages",
select:{
forename: "page.forename",
surname: "page.surname",
nickname: "page.nickname",
uuid: "page.uuid",
img: "page.pic.toFile?.thumbnail(350).url"
}
}
}
})
}
function savePregame(page){
return setKirby(page.id, {
sets: page.sets,
legs: page.legs,
players: page.players.map((p) => p.uuid),
startdate: page.startdate,
rounds: page.game ? JSON.stringify(page.game) : "",
stats: page.stats ? JSON.stringify(page.stats) : ""
});
}
function saveGame(page){
return setKirby(page.id, {
rounds: page.game ? JSON.stringify(page.game) : "",
stats: page.stats ? JSON.stringify(page.stats) : "",
enddate: page.enddate,
});
}
////////////////////////////////////////////////////////////////////////////////
// State Machine
////////////////////////////////////////////////////////////////////////////////
// Checkout Pipeline
const checkoutPipeline = (stack) => {
return {
0: async (sum, reGet) => {
// Ask for num Darts
const [numDarts, error] = await overlayAndGet("d-dialog", numDartsDialog(), stack, reGet);
if (error != undefined) {
popLastElem(stack);
return [undefined, [-1, -1]];
}
return [1, [numDarts, 1]];
},
1: async ([numDarts, start], reGet) => {
// Ask for checkoutTries
if (numDarts <= start) {
// Store with one checkout try
popLastElem(stack);
return [undefined, [1, 1]];
}
const [tries, error] = await overlayAndPop("d-dialog", numCheckoutTriesDialog(numDarts, start), stack);
if (error != undefined) {
if (start == 0) {
return [undefined, [-1, -1]];
}
return [-1, undefined];
}
if (start == 1) {
popLastElem(stack);
}
return [undefined, [numDarts, tries]];
}
};
};
// Game State Machine
const gameStateMachine = (gamestack, stack, page, computedProps) => {
return {
0: async (input, reGet) => {
// Dispatcher
if (page.stats.winner){
return [undefined, page.stats.winner];
}
// Check for Game State
return [1, undefined];
},
1: async (input, reGet) => {
// Normal Game Loop
// Get Game Input
const [visit, error] = await overlayAndPop("d-gameinput", { input: input }, gamestack);
// back/delete last throw
if (error != undefined) {
const val = removeLastVisit(page);
saveGame(page);
if (val == undefined) return [1, val];
return [1, val.join(",")];
}
// Validate throw
let numDarts, tries;
const [ret, sum] = computedProps.checkVisit(visit);
if (ret == -1) {
// Impossible
const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack);
return [1, visit];
} else if (ret == -2) {
// Bust TODO
const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack);
return [1, visit];
} else if (ret == 2) {
// Normal
storeVisit(page, visit.split(","), sum, 3, 0);
saveGame(page);
return [1, undefined]
}
// checkout:
if (ret == 0) {
// Checkout: Ask for num Darts
[numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/0, /*initInput=*/sum);
} else if (ret == 1) {
// <=50: Ask for checkout tries
[numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/1, /*initInput=*/[3,0]);
}
if (numDarts == -1) {
// Error/Back
return [1, visit];
} else {
const points = storeVisit(page, visit.split(","), sum, numDarts, tries);
if (points){
// Game Over
const winner = getFinalWinner(page);
let name = "DRAW"
if (winner != -2){
name = page.players[winner].forename;
}
const [answer, error] = await overlayAndPop("d-dialog", gameOverDialog(name, points[1]), stack);
if (answer == "end") {
saveGame(page);
return [undefined, page.stats.winner];
} else {
extension(page);
saveGame(page);
}
} else {
saveGame(page);
}
return [1, undefined];
}
},
};
}
export const gameHandler = async (gamestack, stack, page, computedProps) => {
const sm = gameStateMachine(gamestack, stack, page, computedProps);
return powerStateMachine(sm, gamestack);
}
// General State Machine
const stateMachine = (stack, page) => {
return {
0: async (input, reGet) => {
// Dispatcher
// Check Game State:
if (page.players.length != 2 || page.startdate === undefined || page.startdate === ""){
// Pre Game
return [1, page];
} else if (page.enddate === undefined || page.enddate === ""){
if (page.game === "" || page.game === undefined || page.game === null) {
// Who won Bull?
return [2, page];
} else {
// In Game
return [3, page];
}
} else {
// Post Game
return [4, undefined];
}
},
1: async (page, reGet) => {
// Pre Game
const [result, error] = await overlayAndPop("d-pregame", { page: page}, stack);
if (error != undefined) {
var re = /^https?:\/\/[^/]+/i;
window.setTimeout(() => {
window.location.href = re.exec(window.location.href)[0];
return false;
},1);
return [undefined, undefined];
}
page.startdate = formatDate(new Date(Date.now()));
// Update game in database
const ret = await savePregame(page);
if (ret.status != "ok") {
console.error("Error save page:", ret.status, ret.error);
}
return [0, page];
},
2: async (page, reGet) => {
// Ask for Bull
const [result, error] = await overlayAndPop("d-bullselect", { players: page.players}, stack);
if (error != undefined) {
return [1, page];
}
// reorderPlayer
if (result !== page.players[0]){
page.players = [page.players[1], page.players[0]];
}
// Setup Game
initGame(page);
initStats(page);
const ret = await savePregame(page);
return [3, page];
},
3: async (page, reGet) => {
// In Game
const [result, error] = await overlayAndPop("d-xoi", { page: page, inspect: false }, stack);
return [4, result];
},
4: async (winnerUUID, reGet) => {
const [res, e] = await overlayAndPop("d-xoi", { page: page, inspect: true }, stack);
var re = /^https?:\/\/[^/]+/i;
window.setTimeout(() => {
window.location.href = re.exec(window.location.href)[0];
return false;
},1);
return [4, res];
}
};
}
////////////////////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////////////////////
export const initXoiView = (app) => {
app.component('d-pregame', pregame).component('d-xoi', xoi).component('d-bullselect', bullselect).component('d-gameinput', gameinput)
}
export const xoiHandler = async (stack, id) => {
const page = reactive(await getGame(id));
const sm = stateMachine(stack, page);
await powerStateMachine(sm, stack);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,18 @@
export function connect(endpoint, callback){
const socket = new WebSocket("ws://localhost:4040/subscribe");
// Connection opened
socket.addEventListener("open", (event) => {
socket.send(JSON.stringify({
"action": "subscribe",
"value": endpoint
}));
});
// Listen for messages
socket.addEventListener("message", callback);
window.onbeforeunload = function() {
socket.close();
};
}

@ -0,0 +1,40 @@
{
"name": "getkirby/plainkit",
"description": "Kirby Plainkit",
"type": "project",
"keywords": [
"kirby",
"cms",
"starterkit"
],
"authors": [
{
"name": "Bastian Allgeier",
"email": "bastian@getkirby.com",
"homepage": "https://getkirby.com"
}
],
"homepage": "https://getkirby.com",
"support": {
"email": "support@getkirby.com",
"issues": "https://github.com/getkirby/starterkit/issues",
"forum": "https://forum.getkirby.com",
"source": "https://github.com/getkirby/starterkit"
},
"require": {
"php": ">=8.0.0 <8.3.0",
"getkirby/cms": "^3.9"
},
"config": {
"allow-plugins": {
"getkirby/composer-installer": true
},
"optimize-autoloader": true
},
"scripts": {
"start": [
"Composer\\Config::disableProcessTimeout",
"@php -S localhost:8000 kirby/router.php"
]
}
}

@ -0,0 +1,25 @@
Title: 23
----
Forename: Tobias
----
Surname: Grosser
----
Nickname: The Galvanizer
----
Number: 23
----
Pic: - file://PiQjrMnI09NJAqCt
----
Uuid: x8tm8FzBBY95HqIi

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

@ -0,0 +1,25 @@
Title: 8
----
Forename: Tim
----
Surname: Aguirre
----
Nickname: The Chainmaker
----
Number: 8
----
Pic: - file://doN0XBYRAfmbXAXD
----
Uuid: gQUWNqwvWYkT41Ql

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

@ -0,0 +1,25 @@
Title: 99
----
Forename: Kerem
----
Surname: Akkaya
----
Nickname: The Turkster
----
Number: 99
----
Pic: - file://6ifthAJzq86i5mNn
----
Uuid: 2ymjLWSMV1xXil3P

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

@ -0,0 +1,25 @@
Title: 18
----
Forename: Jonas
----
Surname: Milek
----
Nickname: Jonny from the Block
----
Number: 18
----
Pic: - file://EonmAdCPuztUnzjI
----
Uuid: PGHC3ukSd89IJvZ0

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

@ -0,0 +1,25 @@
Title: 001
----
Forename: Marvin
----
Surname: Zmiewski
----
Nickname: Warranty
----
Number: 1
----
Pic:
----
Uuid: X9VLqkbzJPrGw0tW

@ -0,0 +1,25 @@
Title: 14
----
Forename: Dennis
----
Surname: Eibach
----
Nickname: Dart'agnan
----
Number: 14
----
Pic: - file://ZF9zbwGwAx7v0sjE
----
Uuid: 90CUYLfokdOm9Gw0

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

@ -0,0 +1,25 @@
Title: 9
----
Forename: Julian
----
Surname: Milek
----
Nickname: The Beast from the East
----
Number: 9
----
Pic:
----
Uuid: CRQRBY9Ac3tXXQse

@ -0,0 +1,25 @@
Title: 51
----
Forename: Nils
----
Surname: Knörnschild
----
Nickname: The Psycho
----
Number: 51
----
Pic:
----
Uuid: j0DfiLQO2GgJ9RN1

@ -0,0 +1,25 @@
Title: 116
----
Forename: Christian
----
Surname: Motzek
----
Nickname: ?
----
Number: 116
----
Pic:
----
Uuid: 9S2pH2C0vG8XUrye

@ -0,0 +1,25 @@
Title: 4
----
Forename: Finn
----
Surname: Gonschior
----
Nickname: Finguin
----
Number: 4
----
Uuid: aHO5FUnIphBwYEjS
----
Pic:

@ -0,0 +1,25 @@
Title: 32
----
Forename: Ugo
----
Surname: Finnendahl
----
Nickname: The Program
----
Number: 32
----
Image:
----
Uuid: TbZF36aHK27hZvnK

@ -0,0 +1,25 @@
Title: 7
----
Forename: Robert
----
Surname: Hilprecht
----
Nickname: The Pathfinder
----
Number: 7
----
Pic:
----
Uuid: XDZibCkgJ9repYEx

@ -0,0 +1,25 @@
Title: 77
----
Forename: Thorsten
----
Surname: Wassermeyer
----
Nickname: Sisy Phos
----
Number: 77
----
Uuid: 5bw1wQY8DAfxUVAw
----
Pic:

@ -0,0 +1,25 @@
Title: 285
----
Forename: Simon
----
Surname: Zeiter
----
Nickname: Der Zar
----
Number: 285
----
Pic:
----
Uuid: 8TlbKlF7fVBwS24c

@ -0,0 +1,25 @@
Title: TODO
----
Forename: Thomas
----
Surname: Engel
----
Nickname: Only 19
----
Number: 19
----
Uuid: 5qeooGppOFqpprW5
----
Pic:

@ -0,0 +1,29 @@
Title: 92
----
Forename: Jacob
----
Surname: Otto
----
Nickname: Cool Crab
----
Number: 92
----
Pic: - file://4cGjeP4QOkq6a46R
----
Image:
----
Uuid: 7GAUHPdaKdD2sHm9

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

@ -0,0 +1,25 @@
Title: 45
----
Forename: Vincent
----
Surname: Weigelt
----
Nickname: Unimpossible
----
Number: 45
----
Pic: - file://ZLhSSU1MU9Hy0Ru3
----
Uuid: ZUPj0Nw2DcaIQ1H1

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

@ -0,0 +1,29 @@
Title: 180
----
Forename: Alexander
----
Surname: Ecke
----
Nickname: One Hundred and Ecki
----
Number: 180
----
Pic: - file://YXZh1UDA03dJwFYT
----
Uuid: fShTMgFob20fogyr
----
Image:

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

@ -0,0 +1,25 @@
Title: 57
----
Forename: Patrick
----
Surname: Wendhaus
----
Nickname: The Roman Sage
----
Number: 57
----
Uuid: 4CPpeIGeeK8UHTf8
----
Image:

@ -0,0 +1,25 @@
Title: 5
----
Forename: Melvin
----
Surname: Malinowsky
----
Nickname: Diver
----
Number: 5
----
Pic:
----
Uuid: Ue0F1SstBqE7xVKr

@ -0,0 +1,25 @@
Title: 26
----
Forename: Hannes
----
Surname: Gonschior
----
Nickname: The Answer
----
Number: 26
----
Pic: - file://uQkuW135VcDc2lRV
----
Uuid: UkQNJytohCGIXySl

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 KiB

@ -0,0 +1,25 @@
Title: 11
----
Forename: Nicolai
----
Surname: Jelitto
----
Nickname: El Sid
----
Number: 11
----
Uuid: xRomcRk5I5n4gkq5
----
Pic:

@ -0,0 +1,25 @@
Title: 13
----
Forename: Felix
----
Surname: Meißner
----
Nickname: The Droog
----
Number: 13
----
Uuid: Jz127lCNtxHgUj2b
----
Pic:

@ -0,0 +1,5 @@
Title: Members
----
Uuid: dKjmIGtTxipFe6jf

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,26 @@
Title: Summer Slam
----
Participants:
- page://TbZF36aHK27hZvnK
- page://7GAUHPdaKdD2sHm9
- page://XDZibCkgJ9repYEx
- page://4CPpeIGeeK8UHTf8
- page://aHO5FUnIphBwYEjS
- page://90CUYLfokdOm9Gw0
- page://5bw1wQY8DAfxUVAw
- page://fShTMgFob20fogyr
- page://UkQNJytohCGIXySl
- page://5qeooGppOFqpprW5
- page://ZUPj0Nw2DcaIQ1H1
- page://9S2pH2C0vG8XUrye
----
Date: 2024-06-15
----
Uuid: w7RmvzZeMb8NzkeX

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save