Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ APP_CONNECT_FOUR_DOCTRINE_DBAL_SHARDS='connect-four'
APP_CONNECT_FOUR_PUBLISH_TO_BROWSER_SHARDS='1'
APP_CONNECT_FOUR_PREDIS_CLIENT_URL='redis://redis:6379?persistent=1'

############################
# Tic Tac Toe Context #
############################
APP_TIC_TAC_TOE_DOCTRINE_DBAL_URL='mysqli://root:password@localhost/tic-tac-toe?persistent=1&unix_socket=/var/run/proxysql/proxysql.sock'
APP_TIC_TAC_TOE_PREDIS_CLIENT_URL='redis://redis:6379?persistent=1'

############################
# Identity Context #
############################
Expand Down
2 changes: 2 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "tic-tac-toe.css";

html {
scroll-padding-top: 10px;
}
Expand Down
171 changes: 171 additions & 0 deletions assets/css/tic-tac-toe.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
.gp-ttt-game {
--grid-cols: 3;
display: grid;
grid-template-columns: repeat(var(--grid-cols), 1fr);
grid-gap: var(--tblr-spacer-1);
background-color: var(--tblr-yellow);
border: var(--tblr-border-width) solid var(--tblr-yellow-darken);
border-radius: var(--tblr-border-radius);
padding: var(--tblr-spacer-2);
}

.gp-ttt-game--disabled {
pointer-events: none;
}

.gp-ttt-game__field {
position: relative;
text-align: initial;
width: 100%;
aspect-ratio: 1;
background-color: var(--tblr-yellow-lt);
border: var(--tblr-border-width) solid var(--tblr-yellow-darken);
border-radius: calc(var(--tblr-border-radius) / 2);
cursor: pointer;
}

.gp-ttt-game__field:after {
content: "";
position: absolute;
top: calc(var(--tblr-border-width) * -1);
left: calc(var(--tblr-border-width) * -1 + var(--tblr-spacer-1) / 2 * -1);
width: calc(100% + var(--tblr-border-width) * 2 + var(--tblr-spacer-1));
height: calc(100% + var(--tblr-border-width) * 2 + var(--tblr-spacer-1));
}

.gp-ttt-game__field--highlight.gp-ttt-game__field--current {
box-shadow: 0 0 .5em var(--tblr-light);
}

.gp-ttt-game__field--highlight {
border-color: var(--tblr-dark);
}

.gp-ttt-game__field .gp-ttt-token-o {
position: absolute;
top: 10%;
left: 10%;
width: 80%;
}

.gp-ttt-game-list .gp-ttt-token-o,
.gp-ttt-token-loading .gp-ttt-token-o {
border-width: 5px;
}

.gp-ttt-token-o {
position: relative;
border-radius: 50%;
aspect-ratio: 1;
box-sizing: border-box;
border: 15px solid var(--tblr-orange);
}

.gp-ttt-game__field .gp-ttt-token-x {
position: absolute;
}

.gp-ttt-game__field .gp-ttt-token-x::before,
.gp-ttt-game__field .gp-ttt-token-x::after {
width: 80%;
}

.gp-ttt-game-list .gp-ttt-token-x::before,
.gp-ttt-game-list .gp-ttt-token-x::after,
.gp-ttt-token-loading .gp-ttt-token-x::before,
.gp-ttt-token-loading .gp-ttt-token-x::after {
height: 5px;
}

.gp-ttt-token-x {
position: relative;
display: inline-block;
width: 100%;
aspect-ratio: 1;
}

.gp-ttt-token-x::before,
.gp-ttt-token-x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 120%;
height: 15px;
background-color: var(--tblr-gray);
}

.gp-ttt-token-x::before {
transform: translate(-50%, -50%) rotate(45deg);
}

.gp-ttt-token-x::after {
transform: translate(-50%, -50%) rotate(-45deg);
}

.gp-ttt-game__field--highlight .gp-ttt-token-o,
.gp-ttt-game__field--highlight .gp-ttt-token-x {
animation: gp-ttt-token-bounce 3s infinite;
}

@keyframes gp-ttt-token-bounce {
20%, 40%, 60% {
scale: 1;
}
10% {
scale: .9;
}
30% {
scale: .95;
}
50% {
scale: .98;
}
}

.gp-ttt-token-loading {
display: flex;
gap: var(--tblr-spacer-1);
margin: 0 auto var(--tblr-spacer-3);
}

.gp-ttt-token-loading .gp-ttt-token-o,
.gp-ttt-token-loading .gp-ttt-token-x {
--token-index: 0;
scale: .9;
animation: gp-ttt-token-loading 1.4s linear calc(var(--token-index) * .1s) infinite;
}

@keyframes gp-ttt-token-loading {
0%, 100% {
scale: .9;
}
15% {
scale: 1;
}
30% {
scale: .9;
}
}


.gp-ttt-token-loading > * {
flex: 1 0 10%;
}

[data-bs-theme=dark] .gp-ttt-game {
background-color: var(--tblr-bg-surface);
}

[data-bs-theme=dark] .gp-ttt-game__field {
background-color: var(--tblr-bg-surface);
}

[data-bs-theme=dark] .gp-ttt-game__field--highlight {
border-color: var(--tblr-light);
}

[data-bs-theme=dark] .gp-ttt-token-x::before,
[data-bs-theme=dark] .gp-ttt-token-x::after {
background-color: var(--tblr-gray-300);
}
84 changes: 84 additions & 0 deletions assets/js/TicTacToe/AnimatedGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {html} from 'uhtml/node.js'

customElements.define('tic-tac-toe-animated-game', class extends HTMLElement {
#plainElement; #moveElements; #abortController;

connectedCallback() {
this.#plainElement = html`<div class="gp-ttt-game__field"></div>`;
this.#moveElements = Array.from(this.querySelectorAll('[data-move]')).sort((a, b) => {
return parseInt(a.dataset.move) - parseInt(b.dataset.move);
});
this.#abortController = new AbortController();

if (typeof AbortSignal?.timeout !== 'function' || typeof AbortSignal?.any !== 'function') {
return;
}

this.addEventListener('mouseenter', this.#onMouseEnter);
this.addEventListener('mouseleave', this.#onMouseLeave);

if (this.matches(':hover')) {
this.#onMouseEnter();
}
}

disconnectedCallback() {
this.#abortController.abort();
}

#onMouseEnter = () => {
this.#abortController = new AbortController();
this.#runAnimation();
}

#onMouseLeave = () => {
this.#abortController.abort();
this.#showFinalState();
}

#runAnimation = async () => {
this.#clear();

for (const [i, moveElement] of this.#moveElements.entries()) {
this.#moveElements.forEach(e => {
e.classList.remove('gp-ttt-game__field--highlight', 'gp-ttt-game__field--current')
});
moveElement.classList.add('gp-ttt-game__field--highlight', 'gp-ttt-game__field--current');
this.querySelector('[data-move="' + moveElement.dataset.move + '"]').replaceWith(moveElement);

const isLastMove = i === this.#moveElements.length - 1;
isLastMove && this.#showFinalState();

try {
await new Promise((resolve, reject) => {
AbortSignal.any([this.#abortController.signal, AbortSignal.timeout(isLastMove ? 1500 : 350)])
.addEventListener('abort', reject);
});
} catch {
if (this.#abortController.signal.aborted) return;
}
}

this.#runAnimation();
}

#clear() {
this.#moveElements.forEach(e => {
e.classList.remove('gp-ttt-game__field--highlight', 'gp-ttt-game__field--current');
const clone = this.#plainElement.cloneNode();
clone.dataset.move = e.dataset.move;
if (e.hasAttribute('data-win')) clone.dataset.win = e.dataset.win;
this.querySelector('[data-move="' + e.dataset.move + '"]').replaceWith(clone);
});
}

#showFinalState() {
this.#moveElements.forEach((e, k) => {
e.classList.remove('gp-ttt-game__field--highlight', 'gp-ttt-game__field--current');
k === this.#moveElements.length - 1 && e.classList.add('gp-ttt-game__field--current');
e.hasAttribute('data-win') && e.classList.add('gp-ttt-game__field--highlight');
this.querySelector('[data-move="' + e.dataset.move + '"]').replaceWith(e);
});
}
});

Loading