From f9629af6c11ee2009597dc75298a8ff6a7b945d7 Mon Sep 17 00:00:00 2001 From: Yuriy <43875549+faizov@users.noreply.github.com> Date: Thu, 29 Aug 2024 22:19:50 +0300 Subject: [PATCH 01/94] Fy varaman optimization (#439) --- frontend/apps/vara-man/package.json | 3 +- .../apps/vara-man/public/sprites/icons.svg | 17 ++ frontend/apps/vara-man/src/app.tsx | 14 +- .../layout/header/Header.module.scss | 78 +++---- .../src/components/layout/header/header.tsx | 24 +-- .../components/layout/header/logo/index.ts | 1 + .../layout/header/logo/logo.module.scss | 43 ++++ .../components/layout/header/logo/logo.tsx | 20 ++ .../sections/levels/levels-select-mode.tsx | 2 +- .../sections/tournament/tournament-find.tsx | 2 +- .../components/game-canvas/game-canvas.tsx | 30 ++- .../mobile-controller/mobile-controller.tsx | 4 +- .../vara-man/src/feature/game/models/Game.ts | 155 ++++++++------ .../game/models/renders/CharacterRenderer.ts | 196 ++++++++---------- .../game/models/renders/MapRenderer.ts | 171 +++++++-------- .../src/feature/single-game/GameLayout.tsx | 2 +- .../src/feature/tournament-game/Game.tsx | 15 +- .../feature/tournament-game/GameLayout.tsx | 13 +- .../components/game-canvas/game-canvas.tsx | 10 +- .../components/game-players/index.tsx | 27 ++- .../components/modals/game-canceled.tsx | 2 +- .../components/modals/game-over.tsx | 16 +- frontend/yarn.lock | 13 +- 23 files changed, 480 insertions(+), 378 deletions(-) create mode 100644 frontend/apps/vara-man/src/components/layout/header/logo/index.ts create mode 100644 frontend/apps/vara-man/src/components/layout/header/logo/logo.module.scss create mode 100644 frontend/apps/vara-man/src/components/layout/header/logo/logo.tsx diff --git a/frontend/apps/vara-man/package.json b/frontend/apps/vara-man/package.json index e2b2756db..c023d822a 100644 --- a/frontend/apps/vara-man/package.json +++ b/frontend/apps/vara-man/package.json @@ -17,7 +17,8 @@ "@dapps-frontend/ui": "workspace:*", "@gear-js/api": "0.38.1", "@gear-js/react-hooks": "0.12.1", - "@gear-js/ui": "0.5.21", + "@gear-js/ui": "0.5.26", + "@gear-js/vara-ui": "0.0.6", "@headlessui/react": "1.7.13", "@mantine/form": "6.0.19", "@polkadot/api": "11.0.2", diff --git a/frontend/apps/vara-man/public/sprites/icons.svg b/frontend/apps/vara-man/public/sprites/icons.svg index d89942075..4b1432e0e 100644 --- a/frontend/apps/vara-man/public/sprites/icons.svg +++ b/frontend/apps/vara-man/public/sprites/icons.svg @@ -210,4 +210,21 @@ + diff --git a/frontend/apps/vara-man/src/app.tsx b/frontend/apps/vara-man/src/app.tsx index 81f5950e3..12b3fc9c1 100644 --- a/frontend/apps/vara-man/src/app.tsx +++ b/frontend/apps/vara-man/src/app.tsx @@ -5,19 +5,27 @@ import { useApi, useAccount } from '@gear-js/react-hooks'; import { Container, Footer } from '@dapps-frontend/ui'; import { Routing } from './pages'; import { ApiLoader } from './components/loaders/api-loader'; +import { useLocation } from 'react-router-dom'; import { Header } from '@/components/layout'; import { withProviders } from '@/app/hocs'; import '@gear-js/vara-ui/dist/style.css'; +import { useGame } from './app/context/ctx-game'; +import { useMediaQuery } from './hooks/use-mobile-device'; +import { MOBILE_BREAKPOINT } from './app/consts'; const Component = () => { const { isApiReady } = useApi(); const { isAccountReady } = useAccount(); + const { pathname } = useLocation(); + const { tournamentGame } = useGame(); + const isMobile = useMediaQuery(MOBILE_BREAKPOINT); + + const isHeader = pathname === '/game' || (tournamentGame && tournamentGame[0].stage !== 'Registration'); + return (
-
-
-
+ {isHeader && isMobile ? null :
}
{isApiReady && isAccountReady ? : }
diff --git a/frontend/apps/vara-man/src/components/layout/header/Header.module.scss b/frontend/apps/vara-man/src/components/layout/header/Header.module.scss index d8f289047..d6d1a0d09 100644 --- a/frontend/apps/vara-man/src/components/layout/header/Header.module.scss +++ b/frontend/apps/vara-man/src/components/layout/header/Header.module.scss @@ -1,65 +1,43 @@ .header { - height: 100px; - width: 100%; - box-sizing: border-box; - background: transparent; -} - -.container { - height: 100%; - padding: 20px 0; - max-width: 1200px; - display: flex; - justify-content: space-between; - align-items: center; - margin: auto; -} - -.walletBalance { - color: #000000; -} + position: relative; + z-index: 10; + padding: 20px; + + &__container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0; + } -.menuIcon { - & svg path { - fill: #000000; - stroke: #000000; + &__logo { + flex-shrink: 0; } -} -.vara-logo { - margin-right: 22px; + &__wallet { + @media screen and (max-width: 767px) { + display: none; + } + } } -.headerContent { - display: flex; - align-items: center; - gap: 48px; +.wallet > button { + font-size: 16px; + padding-right: 22px; + padding-left: 22px; } -.cancelGameButton { - background: #f24a4a12; - color: #f24a4a; - border-radius: 0; +.mobile_balance { + display: none; - svg path { - fill: #f24a4a; - stroke: #f24a4a; - } -} - -.dropdown { @media screen and (max-width: 767px) { display: block; + display: flex; + margin-right: 14px; } } -.dropdown { - @media screen and (max-width: 767px) { - z-index: 4; - display: block; - - button { - display: flex !important; - } - } +.menu_wrapper { + display: flex; + flex-direction: row-reverse; } diff --git a/frontend/apps/vara-man/src/components/layout/header/header.tsx b/frontend/apps/vara-man/src/components/layout/header/header.tsx index 56a5aef77..eac5c22a2 100644 --- a/frontend/apps/vara-man/src/components/layout/header/header.tsx +++ b/frontend/apps/vara-man/src/components/layout/header/header.tsx @@ -8,36 +8,26 @@ import { EzGaslessTransactions, EzSignlessTransactions } from '@dapps-frontend/e import styles from './Header.module.scss'; import { SIGNLESS_ALLOWED_ACTIONS } from '@/app/consts'; +import { Logo } from './logo'; +import clsx from 'clsx'; +import { useAccount } from '@gear-js/react-hooks'; export const Header = () => { const { isAdmin } = useGame(); + const { account } = useAccount(); return ( - - - } + logo={} menu={ , - }, + { key: 'signless', option: }, { key: 'gasless', option: }, ]} /> } - className={{ header: styles.header, content: styles.container }}> + className={{ header: styles.header, content: styles.header__container }}> {isAdmin && } ); diff --git a/frontend/apps/vara-man/src/components/layout/header/logo/index.ts b/frontend/apps/vara-man/src/components/layout/header/logo/index.ts new file mode 100644 index 000000000..cfdf7a76b --- /dev/null +++ b/frontend/apps/vara-man/src/components/layout/header/logo/index.ts @@ -0,0 +1 @@ +export { Logo } from './logo'; diff --git a/frontend/apps/vara-man/src/components/layout/header/logo/logo.module.scss b/frontend/apps/vara-man/src/components/layout/header/logo/logo.module.scss new file mode 100644 index 000000000..b9def072c --- /dev/null +++ b/frontend/apps/vara-man/src/components/layout/header/logo/logo.module.scss @@ -0,0 +1,43 @@ +.link { + display: inline-flex; + transition: opacity 300ms ease; + + @media screen and (max-width: 767px) { + position: relative; + } + + &:not(.active):hover { + opacity: 0.7; + } + + .title { + --gradient-to: #0ed3a3; + + align-self: flex-start; + margin-top: -4px; + font-size: 20px; + line-height: 24px; + white-space: nowrap; + user-select: none; + + @media screen and (max-width: 767px) { + display: none; + position: absolute; + left: 100%; + font-size: 10px; + line-height: 18px; + } + } +} + +.logo { + width: 100%; + height: 100%; + max-height: 60px; + max-width: 92px; + + @media screen and (max-width: 767px) { + max-height: 40px; + max-width: 62px; + } +} diff --git a/frontend/apps/vara-man/src/components/layout/header/logo/logo.tsx b/frontend/apps/vara-man/src/components/layout/header/logo/logo.tsx new file mode 100644 index 000000000..b53210d7a --- /dev/null +++ b/frontend/apps/vara-man/src/components/layout/header/logo/logo.tsx @@ -0,0 +1,20 @@ +import { NavLink } from 'react-router-dom'; +import clsx from 'clsx'; +import styles from './logo.module.scss'; +import { SpriteIcon } from '@/components/ui/sprite-icon'; +// import { ROUTES } from '@/app/consts'; +// import { TextGradient } from '@/components/ui/text-gradient'; +// import { Sprite } from '@/components/ui/sprite'; +// import type { BaseComponentProps } from '@/app/types'; + +type LogoProps = BaseComponentProps & { + label?: string; +}; + +export function Logo({ className, label }: LogoProps) { + return ( + clsx(styles.link, isActive && styles.active, className)}> + + + ); +} diff --git a/frontend/apps/vara-man/src/components/sections/levels/levels-select-mode.tsx b/frontend/apps/vara-man/src/components/sections/levels/levels-select-mode.tsx index bdd805dd2..e7f040fd9 100644 --- a/frontend/apps/vara-man/src/components/sections/levels/levels-select-mode.tsx +++ b/frontend/apps/vara-man/src/components/sections/levels/levels-select-mode.tsx @@ -74,7 +74,7 @@ export function LevelsSelectMode() {
- {item.title === 'Hard' && ( + {item.title === 'Hardcore' && (
Blind mode
diff --git a/frontend/apps/vara-man/src/components/sections/tournament/tournament-find.tsx b/frontend/apps/vara-man/src/components/sections/tournament/tournament-find.tsx index 5672bc8a1..b3d3d1b1a 100644 --- a/frontend/apps/vara-man/src/components/sections/tournament/tournament-find.tsx +++ b/frontend/apps/vara-man/src/components/sections/tournament/tournament-find.tsx @@ -92,8 +92,8 @@ export const TournamentFind = () => {
setFindAddress(e.target.value)} diff --git a/frontend/apps/vara-man/src/feature/game/components/game-canvas/game-canvas.tsx b/frontend/apps/vara-man/src/feature/game/components/game-canvas/game-canvas.tsx index 7e51de1de..c18390576 100644 --- a/frontend/apps/vara-man/src/feature/game/components/game-canvas/game-canvas.tsx +++ b/frontend/apps/vara-man/src/feature/game/components/game-canvas/game-canvas.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import BackgroundMapImg from '@/assets/images/border.png'; import MobileController from '../mobile-controller/mobile-controller'; import { GameEngine } from '../../models/Game'; @@ -10,13 +10,17 @@ type GameCanvasProps = { isPause?: boolean; }; -export const GameCanvas = ({ canvasRef, fogCanvasRef, gameInstanceRef, isPause }: GameCanvasProps) => { +const useResizeCanvas = ( + canvasRef: GameCanvasProps['canvasRef'], + fogCanvasRef: GameCanvasProps['fogCanvasRef'], + gameInstanceRef: GameCanvasProps['gameInstanceRef'], +) => { useEffect(() => { const resizeCanvas = () => { const canvas = canvasRef.current; const fogCanvas = fogCanvasRef.current; if (canvas && fogCanvas) { - const dpr = window.devicePixelRatio || 1; + const dpr = window.devicePixelRatio; canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; fogCanvas.width = fogCanvas.clientWidth * dpr; @@ -31,20 +35,26 @@ export const GameCanvas = ({ canvasRef, fogCanvasRef, gameInstanceRef, isPause } } }; + if (gameInstanceRef.current) { + resizeCanvas(); + } else { + const timeoutId = setTimeout(resizeCanvas, 100); + return () => clearTimeout(timeoutId); + } + window.addEventListener('resize', resizeCanvas); - resizeCanvas(); return () => { window.removeEventListener('resize', resizeCanvas); }; - }, [canvasRef, fogCanvasRef, gameInstanceRef]); + }, [canvasRef, fogCanvasRef, gameInstanceRef.current]); +}; + +export const GameCanvas = ({ canvasRef, fogCanvasRef, gameInstanceRef, isPause }: GameCanvasProps) => { + useResizeCanvas(canvasRef, fogCanvasRef, gameInstanceRef); return ( -
+
{ }, []); const preventDefault = (e: TouchEvent) => { - e.preventDefault(); + if (e.cancelable) { + e.preventDefault(); + } }; const handleShiftTouchStart = () => { diff --git a/frontend/apps/vara-man/src/feature/game/models/Game.ts b/frontend/apps/vara-man/src/feature/game/models/Game.ts index 780c8b8ba..0dff477b6 100644 --- a/frontend/apps/vara-man/src/feature/game/models/Game.ts +++ b/frontend/apps/vara-man/src/feature/game/models/Game.ts @@ -1,13 +1,10 @@ import { Character } from './Character'; - import { CharacterRenderer } from './renders/CharacterRenderer'; import { MapRenderer } from './renders/MapRenderer'; import { EnemyRenderer } from './renders/EnemyRenderer'; import { EnemyWithVision } from './EnemyWithVision'; - import { findEnemyStartPositions } from '../utils/findEnemyStartPositions'; import { findCharacterStartPosition } from '../utils/findCharacterStartPosition'; - import { IGameLevel } from '@/app/types/game'; import { TileMap } from '../types'; import { HEIGHT_CANVAS, WIDTH_CANVAS, gameLevels } from '../consts'; @@ -19,6 +16,7 @@ export class GameEngine { private character: Character | undefined; private enemies: EnemyWithVision[] = []; private animationFrameId: number | null = null; + private resizeTimeout: number | undefined; private isUp = false; private isDown = false; @@ -26,6 +24,11 @@ export class GameEngine { private isRight = false; private isShift = false; + private incrementCoins: (coin: 'silver' | 'gold') => void; + + private lastUpdateTime: number = 0; // Добавлено для ограничения FPS + private readonly frameDuration: number = 1000 / 60; // 60 FPS + map: TileMap; level: IGameLevel; @@ -51,25 +54,30 @@ export class GameEngine { this.setGameOver = setGameOver; this.gameOver = gameOver; this.pause = pause; - + this.incrementCoins = incrementCoins; + this.init(); this.resize(); + } + init() { MapRenderer.initTilesets(this.map).then(() => { const startPosition = findCharacterStartPosition(this.map); const enemyStartPositions = findEnemyStartPositions(this.map); if (startPosition) { - this.character = new Character(startPosition.x, startPosition.y, true, this.map, incrementCoins, () => + this.character = new Character(startPosition.x, startPosition.y, true, this.map, this.incrementCoins, () => this.setGameOver(true), ); this.initEventListeners(); + + this.resize(); } else { console.error('The character starting position was not found.'); } const levelData = gameLevels.find((l) => { - return l.level === level; + return l.level === this.level; }); enemyStartPositions.forEach(({ position, zone }) => { @@ -105,8 +113,8 @@ export class GameEngine { return this.character; } - resize() { - const dpr = window.devicePixelRatio || 1; + resize = () => { + const dpr = Math.min(window.devicePixelRatio, 1.5); const width = WIDTH_CANVAS * dpr; const height = HEIGHT_CANVAS * dpr; @@ -119,118 +127,139 @@ export class GameEngine { this.fogContext.scale(dpr, dpr); this.render(); - } + }; + + private handleResize = () => { + if (this.resizeTimeout) { + clearTimeout(this.resizeTimeout); + } + this.resizeTimeout = window.setTimeout(() => { + this.resize(); + }, 200); + }; private initEventListeners() { window.addEventListener('keydown', this.handleKeyDown); window.addEventListener('keyup', this.handleKeyUp); - window.addEventListener('resize', this.resize.bind(this)); + window.addEventListener('resize', this.handleResize); } public handleKeyDown = (event: { keyCode: number }) => { switch (event.keyCode) { - case 38: + case 38: // Arrow Up + case 87: // W this.isUp = true; break; - case 40: + case 40: // Arrow Down + case 83: // S this.isDown = true; break; - case 37: + case 37: // Arrow Left + case 65: // A this.isLeft = true; break; - case 39: + case 39: // Arrow Right + case 68: // D this.isRight = true; break; - case 16: + case 16: // Shift this.isShift = true; break; } }; - public handleKeyUp = (event: { keyCode: number }) => { - switch (event.keyCode) { - case 38: + public handleKeyUp = (event: { key: string }) => { + switch (event.key) { + case 'ArrowUp': + case 'w': + case 'W': this.isUp = false; break; - case 40: + case 'ArrowDown': + case 's': + case 'S': this.isDown = false; break; - case 37: + case 'ArrowLeft': + case 'a': + case 'A': this.isLeft = false; break; - case 39: + case 'ArrowRight': + case 'd': + case 'D': this.isRight = false; break; - case 16: + case 'Shift': this.isShift = false; break; } }; - private update = () => { + update = () => { if (this.gameOver) { this.cleanup(); return; } - if (this.animationFrameId !== null) { - if (!this.pause) { - if (this.character) { - if (window.innerWidth > 768) { - this.character.updateMovement(this.isLeft, this.isRight, this.isUp, this.isDown, this.isShift); - } - } + + const now = performance.now(); + const deltaTime = now - this.lastUpdateTime; + + if (deltaTime >= this.frameDuration) { + this.lastUpdateTime = now - (deltaTime % this.frameDuration); + + if (!this.pause && this.character) { + this.character.updateMovement(this.isLeft, this.isRight, this.isUp, this.isDown, this.isShift); this.enemies.forEach((enemy) => { - if (this.character) { - enemy.update({ - mapData: this.map, - playerPosition: this.character.position, - }); - } + enemy.update({ mapData: this.map, playerPosition: this.character!.position }); }); if (this.checkCollisions()) { this.setGameOver(true); return; } + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.render(); } } this.animationFrameId = requestAnimationFrame(this.update); - this.render(); }; - private render() { - if (this.character) { - let offsetX = 0; - let offsetY = 0; + render() { + if (!this.character) { + requestAnimationFrame(this.update); + return; + } - if (window.innerWidth < 768) { - offsetX = WIDTH_CANVAS / 3.5 - this.character.position.x; - offsetY = HEIGHT_CANVAS / 3.5 - this.character.position.y; + let offsetX = 0; + let offsetY = 0; + if (window.innerWidth < 768) { + offsetX = window.innerWidth / 2.8 - this.character.position.x; + offsetY = window.innerHeight / 4 - this.character.position.y; - this.context.save(); - this.context.translate(offsetX, offsetY); - } + this.context.save(); + this.context.translate(offsetX, offsetY); + } - this.context.fillStyle = '#000000ad'; - this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + this.context.fillStyle = '#000000ad'; + this.context.fillRect(0, 0, WIDTH_CANVAS, HEIGHT_CANVAS); - MapRenderer.render(this.context, this.map); - CharacterRenderer.render(this.context, this.character); + MapRenderer.render(this.context, this.map); + CharacterRenderer.render(this.context, this.character); - this.enemies.forEach((enemy) => EnemyRenderer.render(this.context, enemy)); + this.enemies.forEach((enemy) => EnemyRenderer.render(this.context, enemy)); - this.context.restore(); + this.context.restore(); - if (this.level === 'Hard') { - this.fogContext.save(); - this.fogContext.translate(offsetX, offsetY); - const radiusFogOfWar = window.innerWidth < 768 ? 120 : 150; - MapRenderer.renderFogOfWar(this.fogContext, this.character.position, radiusFogOfWar); - this.fogContext.restore(); - } + if (this.level === 'Hard') { + this.fogContext.save(); + this.fogContext.translate(offsetX, offsetY); + const radiusFogOfWar = window.innerWidth < 768 ? 120 : 150; + MapRenderer.renderFogOfWar(this.fogContext, this.character.position, radiusFogOfWar); + this.fogContext.restore(); } } @@ -242,7 +271,11 @@ export class GameEngine { window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keyup', this.handleKeyUp); - window.removeEventListener('resize', this.resize.bind(this)); + window.removeEventListener('resize', this.handleResize); + + if (this.resizeTimeout) { + clearTimeout(this.resizeTimeout); + } } checkCollisions() { diff --git a/frontend/apps/vara-man/src/feature/game/models/renders/CharacterRenderer.ts b/frontend/apps/vara-man/src/feature/game/models/renders/CharacterRenderer.ts index 3f009b9b0..6405b8868 100644 --- a/frontend/apps/vara-man/src/feature/game/models/renders/CharacterRenderer.ts +++ b/frontend/apps/vara-man/src/feature/game/models/renders/CharacterRenderer.ts @@ -1,128 +1,115 @@ -import { Character } from '../Character' +import { Character } from "../Character"; export class CharacterRenderer { - static cloakImage: HTMLImageElement | null = null + static cloakImage: HTMLImageElement | null = null; static loadCloakImage(src: string): Promise { + if (this.cloakImage) { + return Promise.resolve(this.cloakImage); + } + return new Promise((resolve, reject) => { - const img = new Image() - img.onload = () => resolve(img) - img.onerror = reject - img.src = src - }) + const img = new Image(); + img.onload = () => { + this.cloakImage = img; + resolve(img); + }; + img.onerror = reject; + img.src = src; + }); } static render(context: CanvasRenderingContext2D, character: Character): void { - const { - position, - rotation, - torsoWidth, - torsoHeight, - legWidth, - legs, - armWidth, - arms, - headRadius, - } = character - - context.save() - context.translate(position.x, position.y) - context.rotate(rotation) - - // Legs - legs.forEach((leg: { limb: string; height: number }) => { - context.strokeStyle = '#1B4138' - context.fillStyle = '#1B4138' - context.beginPath() + const { position, rotation } = character; + + context.save(); + context.translate(position.x, position.y); + context.rotate(rotation); + + this.renderLegs(context, character); + this.renderArms(context, character); + this.renderTorso(context, character); + + if (this.cloakImage) { + this.renderCloak(context, character); + } + + this.renderHead(context, character); + + + + context.restore(); + } + + static renderLegs(context: CanvasRenderingContext2D, character: Character): void { + const { legs, torsoWidth, legWidth } = character; + context.fillStyle = '#1B4138'; + context.strokeStyle = '#1B4138'; + + legs.forEach((leg) => { + context.beginPath(); context.roundRect( - leg.limb === 'left' - ? -torsoWidth / 2 + legWidth / 2 - : torsoWidth / 2 - legWidth - legWidth / 2, + leg.limb === 'left' ? -torsoWidth / 2 + legWidth / 2 : torsoWidth / 2 - legWidth - legWidth / 2, 0, legWidth, leg.height / 2, 5 - ) - context.stroke() - context.fill() - }) - - - if (this.cloakImage) { - this.renderCloak(context, character) - } + ); + context.stroke(); + context.fill(); + }); + } - const radius = 5 + static renderArms(context: CanvasRenderingContext2D, character: Character): void { + const { arms, torsoWidth, armWidth, torsoHeight } = character; + context.fillStyle = '#00E3AE'; + context.strokeStyle = '#00E3AE'; - // Hands - arms.forEach((arm: { limb: string; height: number }) => { - context.strokeStyle = '#00E3AE' - context.fillStyle = '#00E3AE' - context.beginPath() + arms.forEach((arm) => { + context.beginPath(); context.roundRect( arm.limb === 'left' ? -torsoWidth / 2 : torsoWidth / 2 - armWidth, -torsoHeight / 4, armWidth, arm.height, 5 - ) - context.stroke() - context.fill() - }) - - // Torso - context.beginPath() - context.fillStyle = '#00FFC4' - context.moveTo(-torsoWidth / 2 + radius, -torsoHeight / 2) - context.lineTo(torsoWidth / 2 - radius, -torsoHeight / 2) - context.quadraticCurveTo( - torsoWidth / 2, - -torsoHeight / 2, - torsoWidth / 2, - -torsoHeight / 2 + radius - ) - context.lineTo(torsoWidth / 2, torsoHeight / 2 - radius) - context.quadraticCurveTo( - torsoWidth / 2, - torsoHeight / 2, - torsoWidth / 2 - radius, - torsoHeight / 2 - ) - context.lineTo(-torsoWidth / 2 + radius, torsoHeight / 2) - context.quadraticCurveTo( - -torsoWidth / 2, - torsoHeight / 2, - -torsoWidth / 2, - torsoHeight / 2 - radius - ) - context.lineTo(-torsoWidth / 2, -torsoHeight / 2 + radius) - context.quadraticCurveTo( - -torsoWidth / 2, - -torsoHeight / 2, - -torsoWidth / 2 + radius, - -torsoHeight / 2 - ) - context.closePath() - context.fill() - - // Head - context.beginPath() - context.fillStyle = '#000000' - context.arc(0, headRadius * 0.75 * -1, headRadius, 0, Math.PI * 2, false) - context.fill() - - // Hat or Head - context.beginPath() - context.fillStyle = '#ffffff' - context.arc(0, 0, headRadius, 0, Math.PI * 2, false) - context.fill() - - context.restore() - - // Drawing a border for debug - // const bounds = character.getBounds() - // context.strokeStyle = 'rgba(255, 0, 0, 1)' - // context.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height) + ); + context.stroke(); + context.fill(); + }); + } + + static renderTorso(context: CanvasRenderingContext2D, character: Character): void { + const { torsoWidth, torsoHeight } = character; + const radius = 5; + context.fillStyle = '#00FFC4'; + + context.beginPath(); + context.moveTo(-torsoWidth / 2 + radius, -torsoHeight / 2); + context.lineTo(torsoWidth / 2 - radius, -torsoHeight / 2); + context.quadraticCurveTo(torsoWidth / 2, -torsoHeight / 2, torsoWidth / 2, -torsoHeight / 2 + radius); + context.lineTo(torsoWidth / 2, torsoHeight / 2 - radius); + context.quadraticCurveTo(torsoWidth / 2, torsoHeight / 2, torsoWidth / 2 - radius, torsoHeight / 2); + context.lineTo(-torsoWidth / 2 + radius, torsoHeight / 2); + context.quadraticCurveTo(-torsoWidth / 2, torsoHeight / 2, -torsoWidth / 2, torsoHeight / 2 - radius); + context.lineTo(-torsoWidth / 2, -torsoHeight / 2 + radius); + context.quadraticCurveTo(-torsoWidth / 2, -torsoHeight / 2, -torsoWidth / 2 + radius, -torsoHeight / 2); + context.closePath(); + context.fill(); + } + + static renderHead(context: CanvasRenderingContext2D, character: Character): void { + const { headRadius } = character; + context.fillStyle = '#000000'; + + context.beginPath(); + context.arc(0, headRadius * 0.75 * -1, headRadius, 0, Math.PI * 2, false); + context.fill(); + + context.fillStyle = '#ffffff'; + context.beginPath(); + context.arc(0, 0, headRadius, 0, Math.PI * 2, false); + context.fill(); } static renderCloak( @@ -147,4 +134,5 @@ export class CharacterRenderer { context.restore() } } + } diff --git a/frontend/apps/vara-man/src/feature/game/models/renders/MapRenderer.ts b/frontend/apps/vara-man/src/feature/game/models/renders/MapRenderer.ts index a26281094..6c9bfd1be 100644 --- a/frontend/apps/vara-man/src/feature/game/models/renders/MapRenderer.ts +++ b/frontend/apps/vara-man/src/feature/game/models/renders/MapRenderer.ts @@ -1,5 +1,5 @@ -import { TileMap } from '../../types'; -import { Vec2 } from '../Vec2'; +import { TileMap } from "../../types"; +import { Vec2 } from "../Vec2"; class Tileset { image: HTMLImageElement; @@ -9,6 +9,7 @@ class Tileset { imageHeight: number; firstgid: number; tilecount: number; + cachedTiles: { tx: number; ty: number }[] = []; constructor( src: string, @@ -27,6 +28,21 @@ class Tileset { this.imageHeight = imageHeight; this.firstgid = firstgid; this.tilecount = tilecount; + + this.cacheTilePositions(); + } + + private cacheTilePositions() { + const cols = this.imageWidth / this.tileWidth; + for (let i = 0; i < this.tilecount; i++) { + const tx = (i % cols) * this.tileWidth; + const ty = Math.floor(i / cols) * this.tileHeight; + this.cachedTiles.push({ tx, ty }); + } + } + + getTilePosition(index: number) { + return this.cachedTiles[index]; } } @@ -49,90 +65,86 @@ export class MapRenderer { ); await Promise.all( - this.tilesets.map( - (tileset) => - new Promise((resolve) => { - tileset.image.onload = () => resolve(true); - }), - ), + this.tilesets.map((tileset) => { + const src = tileset.image.src; + if (!this.loadedImages[src]) { + return new Promise((resolve) => { + tileset.image.onload = () => { + this.loadedImages[src] = tileset.image; + resolve(true); + }; + }); + } + return Promise.resolve(true); + }), ); } public static render(context: CanvasRenderingContext2D, mapData: TileMap) { const tileLayer = mapData.layers.find((layer) => layer.name === 'main'); + if (!tileLayer || !tileLayer.visible) return; - if (!tileLayer || !tileLayer.visible) { - return; - } + this.renderLayer(context, tileLayer, mapData); + this.renderImageLayer(context, mapData); + this.renderCoins(context, mapData); + } - const { width, height, data } = tileLayer; + private static renderLayer(context: CanvasRenderingContext2D, layer: any, mapData: TileMap) { + const { width, height, data } = layer; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const tileIndex = data[y * width + x] - 1; - if (tileIndex < 0) continue; - - for (const tileset of this.tilesets) { - if (tileIndex < (tileset.imageWidth / tileset.tileWidth) * (tileset.imageHeight / tileset.tileHeight)) { - const cols = tileset.imageWidth / tileset.tileWidth; - const tx = (tileIndex % cols) * tileset.tileWidth; - const ty = Math.floor(tileIndex / cols) * tileset.tileHeight; - context.drawImage( - tileset.image, - tx, - ty, - tileset.tileWidth, - tileset.tileHeight, - x * mapData.tilewidth, - y * mapData.tileheight, - mapData.tilewidth, - mapData.tileheight, - ); - break; - } - } + const tileIndex = data[y * width + x]; + if (tileIndex <= 0) continue; + + const tileset = this.getTilesetForTile(tileIndex); + if (!tileset) continue; + + const localTileIndex = tileIndex - tileset.firstgid; + const { tx, ty } = tileset.getTilePosition(localTileIndex); + + context.drawImage( + tileset.image, + tx, + ty, + tileset.tileWidth, + tileset.tileHeight, + x * mapData.tilewidth, + y * mapData.tileheight, + mapData.tilewidth, + mapData.tileheight, + ); } } - - this.renderImageLayer(context, mapData); - this.renderCoins(context, mapData); + } + + private static getTilesetForTile(tileIndex: number): Tileset | undefined { + return this.tilesets.find( + (ts) => tileIndex >= ts.firstgid && tileIndex < ts.firstgid + ts.tilecount, + ); } public static renderCoins(context: CanvasRenderingContext2D, mapData: TileMap) { const coinLayer = mapData.layers.find((layer) => layer.name === 'coins'); - if (!coinLayer || !coinLayer.visible) { - return; - } + if (!coinLayer || !coinLayer.visible) return; - const { width, height, data } = coinLayer; + this.renderLayer(context, coinLayer, mapData); + } - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const tileIndex = data[y * width + x]; - if (tileIndex > 0) { - const tileset = this.tilesets.find( - (ts) => tileIndex >= ts.firstgid && tileIndex < ts.firstgid + ts.tilecount, - ); - if (!tileset) continue; - - const localTileIndex = tileIndex - tileset.firstgid; - const cols = tileset.imageWidth / tileset.tileWidth; - const tx = (localTileIndex % cols) * tileset.tileWidth; - const ty = Math.floor(localTileIndex / cols) * tileset.tileHeight; - - context.drawImage( - tileset.image, - tx, - ty, - tileset.tileWidth, - tileset.tileHeight, - x * mapData.tilewidth, - y * mapData.tileheight, - mapData.tilewidth, - mapData.tileheight, - ); - } - } + public static renderImageLayer(context: CanvasRenderingContext2D, mapData: TileMap) { + const imageLayer = mapData.layers.find((layer) => layer.type === 'imagelayer'); + if (!imageLayer || !imageLayer.visible || !imageLayer.image) return; + + const imageSrc = imageLayer.image; + if (!this.loadedImages[imageSrc]) { + const image = new Image(); + image.src = imageSrc; + image.onload = () => { + context.drawImage(image, 0, 0); + this.loadedImages[imageSrc] = image; + }; + } else { + context.drawImage(this.loadedImages[imageSrc], 0, 0); } } @@ -174,27 +186,4 @@ export class MapRenderer { context.closePath(); context.fill(); } - - public static renderImageLayer(context: CanvasRenderingContext2D, mapData: TileMap) { - const imageLayer = mapData.layers.find((layer) => layer.type === 'imagelayer'); - - if (!imageLayer || !imageLayer.visible) { - return; - } - - if (imageLayer.image) { - if (!this.loadedImages[imageLayer.image]) { - const image = new Image(); - image.src = imageLayer.image; - image.onload = () => { - context.drawImage(image, 0, 0); - if (imageLayer.image) { - this.loadedImages[imageLayer.image] = image; - } - }; - } else { - context.drawImage(this.loadedImages[imageLayer.image], 0, 0); - } - } - } } diff --git a/frontend/apps/vara-man/src/feature/single-game/GameLayout.tsx b/frontend/apps/vara-man/src/feature/single-game/GameLayout.tsx index 093da5d06..a0211c951 100644 --- a/frontend/apps/vara-man/src/feature/single-game/GameLayout.tsx +++ b/frontend/apps/vara-man/src/feature/single-game/GameLayout.tsx @@ -44,7 +44,7 @@ export const GameLayout = () => { {score}
-
setGameOver(true)}> +
navigate('/')}> Exit
diff --git a/frontend/apps/vara-man/src/feature/tournament-game/Game.tsx b/frontend/apps/vara-man/src/feature/tournament-game/Game.tsx index fef64d4ae..b6f30ceca 100644 --- a/frontend/apps/vara-man/src/feature/tournament-game/Game.tsx +++ b/frontend/apps/vara-man/src/feature/tournament-game/Game.tsx @@ -43,13 +43,14 @@ export const Game = () => { const isAdmin = admin === account?.decodedAddress; if (previousGame && !tournamentGame) { + setGameOver(false); if (!isAdmin) { setCanceledModal(true); } else { setPreviousGame(null); } } - }, [tournamentGame]); + }, [account?.decodedAddress, previousGame, tournamentGame]); useEffect(() => { if (playGame || isStarted) { @@ -62,7 +63,7 @@ export const Game = () => { }, [activeTab]); return ( -
+
{isMobile && (
@@ -92,8 +93,8 @@ export const Game = () => { isStarted={isStarted} isRegistration={isRegistration} isFinished={isFinished} + isCanceledModal={isCanceledModal} gameOver={gameOver} - setGameOver={setGameOver} score={score} /> @@ -102,25 +103,25 @@ export const Game = () => {
)} -
+
{isRegistration && previousGame && } {isStarted && }
{!isMobile && ( -
+
)} - {isFinished && tournamentGame && } + {isFinished && tournamentGame && !isRegistration && } {isCanceledModal && }
); diff --git a/frontend/apps/vara-man/src/feature/tournament-game/GameLayout.tsx b/frontend/apps/vara-man/src/feature/tournament-game/GameLayout.tsx index 17a351280..d71eb8c55 100644 --- a/frontend/apps/vara-man/src/feature/tournament-game/GameLayout.tsx +++ b/frontend/apps/vara-man/src/feature/tournament-game/GameLayout.tsx @@ -17,9 +17,10 @@ import { GameCanvas } from '../game/components/game-canvas/game-canvas'; type Props = { isPause: boolean; + isCanceledModal: boolean; }; -export const GameLayout = ({ isPause }: Props) => { +export const GameLayout = ({ isPause, isCanceledModal }: Props) => { const { tournamentGame, previousGame } = useGame(); const [coins, setCoins] = useAtom(COINS); const [gameOver, setGameOver] = useAtom(GAME_OVER); @@ -94,7 +95,7 @@ export const GameLayout = ({ isPause }: Props) => { }, [isPause]); useEffect(() => { - gameInstanceRef.current?.updateGameOver(gameOver); + // gameInstanceRef.current?.updateGameOver(gameOver); if (!messageSent && gameOver && timeGameOver > 0) { setIsOpenPlayAgain(true); @@ -130,12 +131,16 @@ export const GameLayout = ({ isPause }: Props) => { setGameOver(false); setMessageSent(false); gameInstanceRef.current?.updateGameOver(gameOver); + gameInstanceRef.current?.cleanup(); gameInstanceRef.current = null; + mapRef.current = null; }; return ( -
- {isOpenPlayAgain && } +
+ {isOpenPlayAgain && !isCanceledModal && !isPause && ( + + )} void; + isCanceledModal: boolean; score: number | undefined | null; }; @@ -19,7 +19,7 @@ export const GameInfoCanvas = ({ isRegistration, isFinished, gameOver, - setGameOver, + isCanceledModal, score, }: GameInfoCanvasProps) => { const isMobile = useMediaQuery(MOBILE_BREAKPOINT); @@ -38,13 +38,9 @@ export const GameInfoCanvas = ({ {score}
-
setGameOver(true)}> - - Exit -
)} - + {!isMobile && (
diff --git a/frontend/apps/vara-man/src/feature/tournament-game/components/game-players/index.tsx b/frontend/apps/vara-man/src/feature/tournament-game/components/game-players/index.tsx index b18abe9c5..bfb7e51cf 100644 --- a/frontend/apps/vara-man/src/feature/tournament-game/components/game-players/index.tsx +++ b/frontend/apps/vara-man/src/feature/tournament-game/components/game-players/index.tsx @@ -38,8 +38,6 @@ export const GamePlayers = () => { const [timeLeft, setTimeLeft] = useState(endTime - Date.now()); - const onSuccess = () => setIsPending(false); - const isAdmin = tournamentGame?.[0].admin === account?.decodedAddress; const onCancelGame = () => { @@ -51,8 +49,15 @@ export const GamePlayers = () => { payload: { CancelTournament: {} }, voucherId: gasless.voucherId, gasLimit, - onSuccess, - onError: onSuccess, + onSuccess: () => { + setIsPending(false); + setTimeLeft(0); + setGameOver(false); + }, + onError: () => { + setIsPending(false); + setTimeLeft(0); + }, }), ); } @@ -65,17 +70,19 @@ export const GamePlayers = () => { const updateTimer = () => { const now = Date.now(); const timeLeft = endTime - now; - setTimeLeft(Math.max(timeLeft, 0)); - - if (timeLeft <= 0) { + if (timeLeft <= 0 && tournamentGame?.[0]?.stage === 'Started') { setGameOver(true); + } else if (timeLeft <= 0) { + setTimeLeft(0); + } else { + setTimeLeft(Math.max(timeLeft, 0)); } }; const timerId = setInterval(updateTimer, 1000); return () => clearInterval(timerId); - }, [endTime]); + }, [endTime, tournamentGame]); const minutes = Math.floor(timeLeft / 60000); const seconds = Math.floor((timeLeft % 60000) / 1000); @@ -171,9 +178,9 @@ export const GamePlayers = () => { {(index === 1 || index === 2) && }
-

{participant.name}

+

{participant.name}

-
+

{timeFormatted}

diff --git a/frontend/apps/vara-man/src/feature/tournament-game/components/modals/game-canceled.tsx b/frontend/apps/vara-man/src/feature/tournament-game/components/modals/game-canceled.tsx index bb9555f8b..2997ecd4a 100644 --- a/frontend/apps/vara-man/src/feature/tournament-game/components/modals/game-canceled.tsx +++ b/frontend/apps/vara-man/src/feature/tournament-game/components/modals/game-canceled.tsx @@ -10,7 +10,7 @@ export const GameCanceledModal = () => {

The game has been canceled by the administrator

- Game administrator Samovit has ended the game. All spent VARA tokens for the entry fee will be refunded. + Game administrator has ended the game. All spent VARA tokens for the entry fee will be refunded.

+ +
+
); } diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx b/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx index 202fe312e..8e9741e64 100644 --- a/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx +++ b/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Button } from '@gear-js/vara-ui'; import { decodeAddress } from '@gear-js/api'; -import { useAccount, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks'; +import { useAccount, useAlert, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks'; import { TextField } from '@/components/layout/text-field'; import { isNotEmpty, useForm } from '@mantine/form'; import { HexString } from '@gear-js/api'; @@ -33,6 +33,7 @@ function JoinGameForm({ onCancel }: Props) { const { getFormattedBalanceValue } = useBalanceFormat(); const { triggerGame } = useMultiplayerGame(); const { joinGameMessage } = useJoinGameMessage(); + const alert = useAlert(); const gameQuery = useMultiGameQuery(); const [foundState, setFoundState] = useState(null); const { pending, setPending } = usePending(); @@ -83,14 +84,16 @@ function JoinGameForm({ onCancel }: Props) { setPending(true); try { - const transaction = await joinGameMessage(foundGame, values.name); - const withFee = await transaction.withValue(BigInt(foundState.bid)); - const { response } = await withFee.signAndSend(); + const transaction = await joinGameMessage(foundGame, values.name, BigInt(foundState.bid)); + const { response } = await transaction.signAndSend(); await response(); await triggerGame(); } catch (err) { console.log(err); + const { message, docs } = err as Error & { docs: string }; + const errorText = message || docs || 'Create game error'; + alert.error(errorText); } finally { setPending(false); } diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-create-game-message.ts b/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-create-game-message.ts index bcdb4dc7e..484d2914c 100644 --- a/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-create-game-message.ts +++ b/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-create-game-message.ts @@ -11,11 +11,12 @@ export const useCreateGameMessage = () => { }); const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); - const createGameMessage = async (name: string) => { + const createGameMessage = async (name: string, value: bigint) => { const { sessionForAccount, ...params } = await prepareEzTransactionParams(true); const { transaction } = await prepareTransactionAsync({ args: [name, sessionForAccount], ...params, + value, }); return transaction; }; diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-join-game-message.ts b/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-join-game-message.ts index 3ba4d0e8f..c7281510a 100644 --- a/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-join-game-message.ts +++ b/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-join-game-message.ts @@ -11,11 +11,12 @@ export const useJoinGameMessage = () => { }); const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); - const joinGameMessage = async (game_id: string, name: string) => { - const { sessionForAccount, ...params } = await prepareEzTransactionParams(); + const joinGameMessage = async (game_id: string, name: string, value: bigint) => { + const { sessionForAccount, ...params } = await prepareEzTransactionParams(true); const { transaction } = await prepareTransactionAsync({ args: [game_id, name, sessionForAccount], ...params, + value, }); return transaction; }; diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-verify-placement-message.ts b/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-verify-placement-message.ts index c281c508e..bf0454440 100644 --- a/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-verify-placement-message.ts +++ b/frontend/apps/battleship-zk/src/features/multiplayer/sails/messages/use-verify-placement-message.ts @@ -1,6 +1,6 @@ import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; import { usePrepareEzTransactionParams } from '@dapps-frontend/ez-transactions'; -import { useProgram } from '@/app/utils/sails'; +import { useConfigurationQuery, useProgram } from '@/app/utils/sails'; import { ProofBytes, PublicStartInput } from '@/app/utils/sails/lib/lib'; export const useVerifyPlacementMessage = () => { @@ -11,6 +11,7 @@ export const useVerifyPlacementMessage = () => { functionName: 'verifyPlacement', }); const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); + const { data: config } = useConfigurationQuery(); const verifyPlacementMessage = async (proof: ProofBytes, public_input: PublicStartInput, game_id: string) => { const { sessionForAccount, ...params } = await prepareEzTransactionParams(); @@ -18,6 +19,14 @@ export const useVerifyPlacementMessage = () => { args: [proof, public_input, sessionForAccount, game_id], ...params, }); + const calculatedGas = BigInt(transaction.extrinsic.args[2].toString()); + + // When calculating gas for two players simultaneously, + // make sure to account for the gas_for_check_time allocated in the contract for a delayed message, + // which will be deducted from the last signing player. + const requiredGas = calculatedGas + BigInt(config?.gas_for_check_time || 0); + + await transaction.withGas(requiredGas); return transaction; }; diff --git a/frontend/packages/ez-transactions/src/context/index.tsx b/frontend/packages/ez-transactions/src/context/index.tsx index d08d63f7b..5b3a1a322 100644 --- a/frontend/packages/ez-transactions/src/context/index.tsx +++ b/frontend/packages/ez-transactions/src/context/index.tsx @@ -22,7 +22,8 @@ function EzTransactionsProvider({ children }: Props) { const signlessContext = useSignlessTransactions(); - const onSessionCreate = async (signlessAccountAddress: string) => gasless.requestVoucher(signlessAccountAddress); + const onSessionCreate = async (signlessAccountAddress: string) => + gasless.requestVoucher(signlessAccountAddress, false); const signless = { ...signlessContext, diff --git a/frontend/packages/ez-transactions/src/hooks/use-prepare-ez-transaction-params.ts b/frontend/packages/ez-transactions/src/hooks/use-prepare-ez-transaction-params.ts index df8e43e57..5d1fae98f 100644 --- a/frontend/packages/ez-transactions/src/hooks/use-prepare-ez-transaction-params.ts +++ b/frontend/packages/ez-transactions/src/hooks/use-prepare-ez-transaction-params.ts @@ -13,7 +13,7 @@ const usePrepareEzTransactionParams = () => { const sessionForAccount = sendFromPair ? account.decodedAddress : null; let voucherId = sendFromPair ? voucher?.id : gasless.voucherId; - if (account && gasless.isEnabled && !gasless.voucherId && !signless.isActive) { + if (account && gasless.isEnabled && !gasless.voucherId && (sendFromBaseAccount || !signless.isActive)) { voucherId = await gasless.requestVoucher(account.address); } diff --git a/frontend/packages/gasless-transactions/src/context/index.tsx b/frontend/packages/gasless-transactions/src/context/index.tsx index 69278fe6f..049267189 100644 --- a/frontend/packages/gasless-transactions/src/context/index.tsx +++ b/frontend/packages/gasless-transactions/src/context/index.tsx @@ -35,11 +35,13 @@ function GaslessTransactionsProvider({ backendAddress, programId, voucherLimit, [voucherId, voucherStatus], ); - const requestVoucher = async (_accountAddress: string) => + const requestVoucher = async (_accountAddress: string, isSaveContext = true) => withLoading( getVoucherId(backendAddress, _accountAddress, programId).then((result) => { - setAccountAddress(_accountAddress); - setVoucherId(result); + if (isSaveContext) { + setAccountAddress(_accountAddress); + setVoucherId(result); + } return result; }), diff --git a/frontend/packages/gasless-transactions/src/context/types.ts b/frontend/packages/gasless-transactions/src/context/types.ts index 4ac795f8e..2b8d7287a 100644 --- a/frontend/packages/gasless-transactions/src/context/types.ts +++ b/frontend/packages/gasless-transactions/src/context/types.ts @@ -7,7 +7,7 @@ export type GaslessContext = { isActive: boolean; voucherStatus: VoucherStatus | null; expireTimestamp: number | null; - requestVoucher: (accountAddress: string) => Promise<`0x${string}`>; + requestVoucher: (accountAddress: string, isSaveContext?: boolean) => Promise<`0x${string}`>; setIsEnabled: (value: boolean) => void; }; diff --git a/frontend/packages/signless-transactions/src/context/hooks.ts b/frontend/packages/signless-transactions/src/context/hooks.ts index 6417b5da8..06745d49e 100644 --- a/frontend/packages/signless-transactions/src/context/hooks.ts +++ b/frontend/packages/signless-transactions/src/context/hooks.ts @@ -3,6 +3,7 @@ import { getTypedEntries, useAccount, useBalance, + useProgramEvent, useProgramQuery, useReadFullState, useVouchers, @@ -31,12 +32,29 @@ function useMetadataSession(programId: HexString, metadata: ProgramMetadata | un function useSailsSession(program: BaseProgram) { const { account } = useAccount(); - const { data: responseSession } = useProgramQuery({ + const { data: responseSession, refetch } = useProgramQuery({ program, serviceName: 'session', functionName: 'sessionForTheAccount', args: [account?.decodedAddress || ''], - watch: true, + }); + + useProgramEvent({ + program, + serviceName: 'session', + functionName: 'subscribeToSessionCreatedEvent', + onData: () => { + refetch(); + }, + }); + + useProgramEvent({ + program, + serviceName: 'session', + functionName: 'subscribeToSessionDeletedEvent', + onData: () => { + refetch(); + }, }); const isSessionReady = responseSession !== undefined; diff --git a/frontend/packages/signless-transactions/src/context/types.ts b/frontend/packages/signless-transactions/src/context/types.ts index ffbc45230..2fee5b655 100644 --- a/frontend/packages/signless-transactions/src/context/types.ts +++ b/frontend/packages/signless-transactions/src/context/types.ts @@ -63,6 +63,8 @@ type BaseProgram = sessionForTheAccount: (account: ActorId, ...arg2: BaseProgramQueryProps) => Promise; createSession: (signatureData: SignatureData, signature: `0x${string}` | null) => TransactionBuilder; deleteSessionFromAccount: () => TransactionBuilder; + subscribeToSessionCreatedEvent: (callback: (data: null) => void | Promise) => Promise<() => void>; + subscribeToSessionDeletedEvent: (callback: (data: null) => void | Promise) => Promise<() => void>; }; registry: TypeRegistry; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8d8c18085..17b67f82c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2611,16 +2611,6 @@ __metadata: languageName: node linkType: hard -"@gear-js/vara-ui@npm:0.0.10": - version: 0.0.10 - resolution: "@gear-js/vara-ui@npm:0.0.10" - peerDependencies: - react: ^18.2.0 - react-dom: ^18.2.0 - checksum: 10c0/f8cb7cbf1e6183085386752d86d3e4ad067d308545bfed0c27e22944e11cd74cc469c57e16a288f69b9c18077285825b25c899c630028caf408662bb366b5900 - languageName: node - linkType: hard - "@gear-js/vara-ui@npm:0.0.6": version: 0.0.6 resolution: "@gear-js/vara-ui@npm:0.0.6" @@ -23916,7 +23906,7 @@ __metadata: "@gear-js/api": "npm:0.38.1" "@gear-js/react-hooks": "npm:0.12.1" "@gear-js/ui": "npm:0.5.26" - "@gear-js/vara-ui": "npm:0.0.10" + "@gear-js/vara-ui": "npm:0.0.6" "@headlessui/react": "npm:1.7.13" "@mantine/form": "npm:6.0.19" "@polkadot/api": "npm:11.0.2" From 556f1c93c964e2c0ccc96b0ec25f6e8e2ad2aac1 Mon Sep 17 00:00:00 2001 From: MedovTimur <62596970+MedovTimur@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:56:34 +0300 Subject: [PATCH 08/94] Update tic-tac-toe to sails (#424) --- contracts/Cargo.lock | 693 +++++++++++------- contracts/Cargo.toml | 7 +- contracts/tic-tac-toe/{ => gstd}/Cargo.toml | 0 contracts/tic-tac-toe/{ => gstd}/README.md | 0 .../{ => gstd}/assets/accounts_10k.txt | 0 .../{ => gstd}/assets/accounts_pr_keys.txt | 0 contracts/tic-tac-toe/{ => gstd}/build.rs | 0 .../tic-tac-toe/{ => gstd}/io/Cargo.toml | 0 .../tic-tac-toe/{ => gstd}/io/src/lib.rs | 0 contracts/tic-tac-toe/{ => gstd}/src/lib.rs | 0 .../{ => gstd}/tests/ttt_node_test.rs | 0 contracts/tic-tac-toe/sails/app/Cargo.toml | 17 + contracts/tic-tac-toe/sails/app/src/lib.rs | 26 + .../sails/app/src/services/game/funcs.rs | 488 ++++++++++++ .../sails/app/src/services/game/mod.rs | 208 ++++++ .../sails/app/src/services/game/utils.rs | 72 ++ .../tic-tac-toe/sails/app/src/services/mod.rs | 3 + .../sails/app/src/services/session/funcs.rs | 141 ++++ .../sails/app/src/services/session/mod.rs | 80 ++ .../sails/app/src/services/session/utils.rs | 51 ++ .../sails/app/src/services/utils.rs | 13 + contracts/tic-tac-toe/sails/app/tests/test.rs | 165 +++++ contracts/tic-tac-toe/sails/wasm/Cargo.toml | 19 + contracts/tic-tac-toe/sails/wasm/build.rs | 20 + contracts/tic-tac-toe/sails/wasm/src/lib.rs | 6 + .../tic-tac-toe/sails/wasm/tic-tac-toe.idl | 96 +++ 26 files changed, 1844 insertions(+), 261 deletions(-) rename contracts/tic-tac-toe/{ => gstd}/Cargo.toml (100%) rename contracts/tic-tac-toe/{ => gstd}/README.md (100%) rename contracts/tic-tac-toe/{ => gstd}/assets/accounts_10k.txt (100%) rename contracts/tic-tac-toe/{ => gstd}/assets/accounts_pr_keys.txt (100%) rename contracts/tic-tac-toe/{ => gstd}/build.rs (100%) rename contracts/tic-tac-toe/{ => gstd}/io/Cargo.toml (100%) rename contracts/tic-tac-toe/{ => gstd}/io/src/lib.rs (100%) rename contracts/tic-tac-toe/{ => gstd}/src/lib.rs (100%) rename contracts/tic-tac-toe/{ => gstd}/tests/ttt_node_test.rs (100%) create mode 100644 contracts/tic-tac-toe/sails/app/Cargo.toml create mode 100644 contracts/tic-tac-toe/sails/app/src/lib.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/game/funcs.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/game/mod.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/game/utils.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/mod.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/session/funcs.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/session/mod.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/session/utils.rs create mode 100644 contracts/tic-tac-toe/sails/app/src/services/utils.rs create mode 100644 contracts/tic-tac-toe/sails/app/tests/test.rs create mode 100644 contracts/tic-tac-toe/sails/wasm/Cargo.toml create mode 100644 contracts/tic-tac-toe/sails/wasm/build.rs create mode 100644 contracts/tic-tac-toe/sails/wasm/src/lib.rs create mode 100644 contracts/tic-tac-toe/sails/wasm/tic-tac-toe.idl diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 1315dfc47..5cddd7dbd 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -45,12 +45,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aead" version = "0.5.2" @@ -107,7 +101,7 @@ dependencies = [ "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -149,55 +143,12 @@ dependencies = [ "winapi", ] -[[package]] -name = "anstream" -version = "0.6.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" -[[package]] -name = "anstyle-parse" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - [[package]] name = "anyhow" version = "1.0.86" @@ -247,7 +198,7 @@ dependencies = [ "ark-std", "derivative", "hashbrown 0.13.2", - "itertools", + "itertools 0.10.5", "num-traits", "rayon", "zeroize", @@ -265,7 +216,7 @@ dependencies = [ "ark-std", "derivative", "digest 0.10.7", - "itertools", + "itertools 0.10.5", "num-bigint", "num-traits", "paste", @@ -438,6 +389,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -508,9 +468,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ "async-lock 3.4.0", "cfg-if", @@ -518,11 +478,11 @@ dependencies = [ "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.2", + "polling 3.7.3", "rustix 0.38.34", "slab", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -575,11 +535,11 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.3.3", + "async-io 2.3.4", "async-lock 3.4.0", "atomic-waker", "cfg-if", @@ -588,7 +548,7 @@ dependencies = [ "rustix 0.38.34", "signal-hook-registry", "slab", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -605,7 +565,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -676,8 +636,8 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", - "object 0.36.2", + "miniz_oxide", + "object 0.36.3", "rustc-demangle", ] @@ -778,6 +738,21 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -978,9 +953,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" dependencies = [ "serde", ] @@ -1097,9 +1072,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.7" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189" +dependencies = [ + "shlex", +] [[package]] name = "cfg-expr" @@ -1167,18 +1145,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.12" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53aa12ec67affac065e7c7dd20a42fa2a4094921b655711d5d3107bb3d52bed" +checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.12" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbdf2dd5fe10889e0c61942ff5d948aaf12fd0b4504408ab0cbb1916c2cffa9" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstyle", "clap_lex", @@ -1199,12 +1177,6 @@ dependencies = [ "cc", ] -[[package]] -name = "colorchoice" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" - [[package]] name = "colored" version = "2.1.0" @@ -1334,9 +1306,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-processor" @@ -1381,9 +1353,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -1666,7 +1638,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -1792,7 +1764,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -1814,7 +1786,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -1993,7 +1965,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2109,6 +2081,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -2120,6 +2102,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "document-features" version = "0.2.10" @@ -2338,6 +2331,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "enum-iterator" version = "0.7.0" @@ -2375,7 +2377,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2396,17 +2398,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.72", -] - -[[package]] -name = "env_filter" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" -dependencies = [ - "log", - "regex", + "syn 2.0.74", ] [[package]] @@ -2422,19 +2414,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "env_logger" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "environmental" version = "1.1.4" @@ -2549,7 +2528,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2688,14 +2667,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" -version = "1.0.32" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -2782,12 +2767,12 @@ dependencies = [ "derive-syn-parse", "expander", "frame-support-procedural-tools", - "itertools", + "itertools 0.10.5", "macro_magic", "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2800,7 +2785,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2811,7 +2796,7 @@ checksum = "0c3562da4b7b8e24189036c58d459b260e10c8b7af2dd180b7869ae29bb66277" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2963,7 +2948,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3150,7 +3135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16bb9f537b64855446386f8c4cb86ecba2b64425e142a16663eca8bf97aa783e" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3276,7 +3261,7 @@ version = "1.1.0" dependencies = [ "ahash 0.8.11", "gstd", - "indexmap 2.3.0", + "indexmap 2.4.0", "primitive-types", ] @@ -3285,7 +3270,7 @@ name = "gear-lib-derive" version = "1.1.0" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3406,7 +3391,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca1c5b08aa64916b9719f821ef61f52d262548ec5e5a7d9d42cf6b035fa4e04" dependencies = [ - "env_logger 0.10.2", + "env_logger", "gear-core", "hex", "nonempty", @@ -3458,6 +3443,28 @@ dependencies = [ "gwasm-instrument", ] +[[package]] +name = "genco" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afac3cbb14db69ac9fef9cdb60d8a87e39a7a527f85a81a923436efa40ad42c6" +dependencies = [ + "genco-macros", + "relative-path", + "smallvec", +] + +[[package]] +name = "genco-macros" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553630feadf7b76442b0849fd25fdf89b860d933623aec9693fed19af0400c78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -3576,7 +3583,7 @@ checksum = "15d6d25b8590e98e927a4f32830ee3099f717086fd9a377a6b2aab0e99a10b10" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3620,7 +3627,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3680,7 +3687,7 @@ dependencies = [ "gear-utils", "gsdk-codegen", "hex", - "indexmap 2.3.0", + "indexmap 2.4.0", "jsonrpsee 0.16.3", "log", "parity-scale-codec", @@ -3704,7 +3711,7 @@ checksum = "88249fd15fb5fd9dd00c5037cc8afd41a57d93a7fcdc2b004f346b1070617ded" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3735,7 +3742,7 @@ dependencies = [ "gprimitives", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3754,7 +3761,7 @@ dependencies = [ "colored", "core-processor", "derive_more", - "env_logger 0.10.2", + "env_logger", "etc", "gear-common", "gear-core", @@ -3766,7 +3773,7 @@ dependencies = [ "gear-wasm-instrument", "gsys", "hex", - "indexmap 2.3.0", + "indexmap 2.4.0", "log", "parity-scale-codec", "path-clean", @@ -3821,7 +3828,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.3.0", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", @@ -4204,9 +4211,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -4275,16 +4282,19 @@ dependencies = [ ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -4297,9 +4307,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -4515,6 +4525,36 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.8.4", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata 0.4.7", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -4647,6 +4687,38 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "logos" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 2.0.74", +] + +[[package]] +name = "logos-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" +dependencies = [ + "logos-codegen", +] + [[package]] name = "lru" version = "0.10.1" @@ -4680,7 +4752,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4694,7 +4766,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4705,7 +4777,7 @@ checksum = "d710e1214dffbab3b5dacb21475dde7d6ed84c69ff722b3a47a782668d44fbac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4716,7 +4788,7 @@ checksum = "b8fb85ec1620619edf2984a7693497d4ec88a9665d8b87e942856884c92dbf2a" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4842,20 +4914,11 @@ dependencies = [ "adler", ] -[[package]] -name = "miniz_oxide" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" -dependencies = [ - "adler2", -] - [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", @@ -4920,6 +4983,12 @@ dependencies = [ "multisig-wallet-io", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nft" version = "1.1.0" @@ -5218,7 +5287,7 @@ checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5247,9 +5316,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.2" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -5530,7 +5599,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5544,6 +5613,25 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.4.0", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -5561,7 +5649,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5605,9 +5693,9 @@ dependencies = [ [[package]] name = "piper" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -5642,9 +5730,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.2" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", @@ -5652,7 +5740,7 @@ dependencies = [ "pin-project-lite", "rustix 0.38.34", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5680,13 +5768,19 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy 0.6.6", + "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.20" @@ -5694,7 +5788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5761,7 +5855,7 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5954,7 +6048,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5971,9 +6065,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -6025,6 +6119,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.4.2" @@ -6410,6 +6510,19 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "sails-client-gen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e49447619b6576fa0c6ced2fb976db38f6afd310feaf1c28463e49edb59e024" +dependencies = [ + "anyhow", + "convert_case 0.6.0", + "genco", + "parity-scale-codec", + "sails-idl-parser", +] + [[package]] name = "sails-idl-gen" version = "0.3.0" @@ -6425,6 +6538,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sails-idl-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabeb783688efdc0bfd7682af969b3c08ca9d67c413f40e5908078ec786279d8" +dependencies = [ + "lalrpop", + "lalrpop-util", + "logos", + "thiserror", +] + [[package]] name = "sails-macros" version = "0.3.0" @@ -6446,7 +6571,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -6471,6 +6596,15 @@ dependencies = [ "thiserror-no-std", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scale-bits" version = "0.4.0" @@ -6615,7 +6749,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -6783,9 +6917,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.204" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] @@ -6813,13 +6947,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -6830,14 +6964,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" dependencies = [ "itoa", "memchr", @@ -6860,7 +6994,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.4.0", "itoa", "ryu", "serde", @@ -7086,6 +7220,12 @@ dependencies = [ "memmap2 0.6.2", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -7185,7 +7325,7 @@ dependencies = [ "hashbrown 0.14.5", "hex", "hmac 0.12.1", - "itertools", + "itertools 0.10.5", "libsecp256k1", "merlin 3.0.0", "no-std-net", @@ -7229,7 +7369,7 @@ dependencies = [ "futures-util", "hashbrown 0.14.5", "hex", - "itertools", + "itertools 0.10.5", "log", "lru", "parking_lot", @@ -7327,7 +7467,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7475,7 +7615,7 @@ checksum = "8dc707d9f5bf155d584900783e328cb3dc79c950f898a18a8f24066f41f040a5" dependencies = [ "quote", "sp-core-hashing 10.0.0", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7486,7 +7626,7 @@ checksum = "c7f531814d2f16995144c74428830ccf7d94ff4a7749632b83ad8199b181140c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7497,7 +7637,7 @@ checksum = "f12dae7cf6c1e825d13ffd4ce16bd9309db7c539929d0302b4443ed451a9f4e5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7672,7 +7812,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7685,7 +7825,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7842,7 +7982,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -7979,6 +8119,19 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceb97b7225c713c2fd4db0153cb6b3cab244eb37900c3f634ed4d43310d8c34" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.10.0" @@ -8094,7 +8247,7 @@ dependencies = [ "quote", "scale-info", "subxt-metadata", - "syn 2.0.72", + "syn 2.0.74", "thiserror", "tokio", ] @@ -8125,7 +8278,7 @@ dependencies = [ "darling 0.20.10", "proc-macro-error", "subxt-codegen", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -8212,9 +8365,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -8344,14 +8497,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand 2.1.0", + "once_cell", "rustix 0.38.34", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8375,6 +8529,17 @@ dependencies = [ "scale-info", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -8410,7 +8575,7 @@ checksum = "e4c60d69f36615a077cc7663b9cb8e42275722d23e58a7fa3d2c7f2915d09d04" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -8421,7 +8586,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -8469,6 +8634,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tic-tac-toe-app" +version = "1.1.0" +dependencies = [ + "gclient", + "gear-core", + "gstd", + "gtest", + "sails-rs", + "schnorrkel 0.10.2", + "tic-tac-toe-wasm", + "tokio", +] + [[package]] name = "tic-tac-toe-io" version = "1.1.0" @@ -8480,6 +8659,17 @@ dependencies = [ "scale-info", ] +[[package]] +name = "tic-tac-toe-wasm" +version = "1.1.0" +dependencies = [ + "gear-wasm-builder", + "sails-client-gen", + "sails-idl-gen", + "sails-rs", + "tic-tac-toe-app", +] + [[package]] name = "tiny-bip39" version = "1.0.0" @@ -8547,7 +8737,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -8612,7 +8802,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.4.0", "toml_datetime", "winnow 0.5.40", ] @@ -8623,7 +8813,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.4.0", "toml_datetime", "winnow 0.5.40", ] @@ -8634,7 +8824,7 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.4.0", "serde", "serde_spanned", "toml_datetime", @@ -8658,15 +8848,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -8688,7 +8878,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -8896,12 +9086,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.10.0" @@ -9018,6 +9202,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -9041,34 +9235,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9076,22 +9271,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-encoder" @@ -9221,7 +9416,7 @@ dependencies = [ "bytesize", "derive_builder", "hex", - "indexmap 2.3.0", + "indexmap 2.4.0", "schemars", "semver", "serde", @@ -9377,7 +9572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" dependencies = [ "bitflags 2.6.0", - "indexmap 2.3.0", + "indexmap 2.4.0", "semver", ] @@ -9608,11 +9803,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9960,34 +10155,14 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff4524214bc4629eba08d78ceb1d6507070cc0bcbbed23af74e19e6e924a24cf" -[[package]] -name = "zerocopy" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", -] - [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy-derive" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", + "byteorder", + "zerocopy-derive", ] [[package]] @@ -9998,7 +10173,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -10018,7 +10193,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -10027,7 +10202,7 @@ version = "1.1.0" dependencies = [ "ark-groth16", "ark-std", - "env_logger 0.11.5", + "env_logger", "gbuiltin-bls381", "gclient", "gear-core", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index be184336e..616d53901 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -91,7 +91,8 @@ members = [ "tamagotchi-battle", "tamagotchi-battle/state", "tequila-train", - "tic-tac-toe", + "tic-tac-toe/gstd", + "tic-tac-toe/sails/wasm", "vara-man", "varatube", "w3bstreaming", @@ -206,7 +207,7 @@ syndote-player-io.path = "syndote/player/io" tamagotchi-io.path = "tamagotchi/io" tamagotchi-battle-io.path = "tamagotchi-battle/io" tequila-train-io.path = "tequila-train/io" -tic-tac-toe-io.path = "tic-tac-toe/io" +tic-tac-toe-io.path = "tic-tac-toe/gstd/io" vara-man-io.path = "vara-man/io" varatube-io.path = "varatube/io" w3bstreaming-io.path = "w3bstreaming/io" @@ -230,6 +231,8 @@ gtest = "1.5.0" gear-core = "1.5.0" sails-idl-gen = "0.3.0" sails-rs = "0.3.0" +sails-client-gen = "0.3.0" + # External diff --git a/contracts/tic-tac-toe/Cargo.toml b/contracts/tic-tac-toe/gstd/Cargo.toml similarity index 100% rename from contracts/tic-tac-toe/Cargo.toml rename to contracts/tic-tac-toe/gstd/Cargo.toml diff --git a/contracts/tic-tac-toe/README.md b/contracts/tic-tac-toe/gstd/README.md similarity index 100% rename from contracts/tic-tac-toe/README.md rename to contracts/tic-tac-toe/gstd/README.md diff --git a/contracts/tic-tac-toe/assets/accounts_10k.txt b/contracts/tic-tac-toe/gstd/assets/accounts_10k.txt similarity index 100% rename from contracts/tic-tac-toe/assets/accounts_10k.txt rename to contracts/tic-tac-toe/gstd/assets/accounts_10k.txt diff --git a/contracts/tic-tac-toe/assets/accounts_pr_keys.txt b/contracts/tic-tac-toe/gstd/assets/accounts_pr_keys.txt similarity index 100% rename from contracts/tic-tac-toe/assets/accounts_pr_keys.txt rename to contracts/tic-tac-toe/gstd/assets/accounts_pr_keys.txt diff --git a/contracts/tic-tac-toe/build.rs b/contracts/tic-tac-toe/gstd/build.rs similarity index 100% rename from contracts/tic-tac-toe/build.rs rename to contracts/tic-tac-toe/gstd/build.rs diff --git a/contracts/tic-tac-toe/io/Cargo.toml b/contracts/tic-tac-toe/gstd/io/Cargo.toml similarity index 100% rename from contracts/tic-tac-toe/io/Cargo.toml rename to contracts/tic-tac-toe/gstd/io/Cargo.toml diff --git a/contracts/tic-tac-toe/io/src/lib.rs b/contracts/tic-tac-toe/gstd/io/src/lib.rs similarity index 100% rename from contracts/tic-tac-toe/io/src/lib.rs rename to contracts/tic-tac-toe/gstd/io/src/lib.rs diff --git a/contracts/tic-tac-toe/src/lib.rs b/contracts/tic-tac-toe/gstd/src/lib.rs similarity index 100% rename from contracts/tic-tac-toe/src/lib.rs rename to contracts/tic-tac-toe/gstd/src/lib.rs diff --git a/contracts/tic-tac-toe/tests/ttt_node_test.rs b/contracts/tic-tac-toe/gstd/tests/ttt_node_test.rs similarity index 100% rename from contracts/tic-tac-toe/tests/ttt_node_test.rs rename to contracts/tic-tac-toe/gstd/tests/ttt_node_test.rs diff --git a/contracts/tic-tac-toe/sails/app/Cargo.toml b/contracts/tic-tac-toe/sails/app/Cargo.toml new file mode 100644 index 000000000..8aaba12e6 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tic-tac-toe-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gstd = { workspace = true, features = ["debug"] } +sails-rs.workspace = true +schnorrkel.workspace = true + +[dev-dependencies] +gtest.workspace = true +gear-core.workspace = true +gclient.workspace = true +tic-tac-toe-wasm = { path = "../wasm" } +tokio = "1" diff --git a/contracts/tic-tac-toe/sails/app/src/lib.rs b/contracts/tic-tac-toe/sails/app/src/lib.rs new file mode 100644 index 000000000..558fc1867 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/lib.rs @@ -0,0 +1,26 @@ +#![no_std] +#![allow(clippy::new_without_default)] + +use sails_rs::prelude::*; +mod services; +use crate::services::game::utils::Config; +use services::game::GameService; +use services::session::SessionService; +pub struct Program(()); + +#[program] +impl Program { + pub async fn new(config: Config, dns_id_and_name: Option<(ActorId, String)>) -> Self { + GameService::init(config, dns_id_and_name).await; + SessionService::init(); + Self(()) + } + + pub fn tic_tac_toe(&self) -> GameService { + GameService::new() + } + + pub fn session(&self) -> SessionService { + SessionService::new() + } +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/game/funcs.rs b/contracts/tic-tac-toe/sails/app/src/services/game/funcs.rs new file mode 100644 index 000000000..e647bb341 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/game/funcs.rs @@ -0,0 +1,488 @@ +use crate::services::game::{ + Config, Event, GameError, GameInstance, GameResult, Mark, Storage, VICTORIES, +}; +use crate::services::session::utils::{ActionsForSession, SessionData}; +use gstd::{collections::HashMap, exec, msg}; +use sails_rs::prelude::*; + +pub fn start_game( + storage: &mut Storage, + sessions: &HashMap, + msg_source: ActorId, + session_for_account: Option, +) -> Result { + check_allow_messages(storage, msg_source)?; + let player = get_player( + sessions, + &msg_source, + &session_for_account, + ActionsForSession::StartGame, + ); + if let Some(current_game) = storage.current_games.get(&player) { + if !current_game.game_over { + return Err(GameError::GameIsAlreadyStarted); + } + } + + let turn = random_turn(player); + + let (player_mark, bot_mark) = if turn == 0 { + (Mark::O, Mark::X) + } else { + (Mark::X, Mark::O) + }; + let mut game_instance = GameInstance { + board: vec![None; 9], + player_mark, + bot_mark, + last_time: exec::block_timestamp(), + game_result: None, + game_over: false, + }; + + if bot_mark == Mark::X { + game_instance.board[4] = Some(Mark::X); + } + + storage.current_games.insert(player, game_instance.clone()); + + Ok(Event::GameStarted { + game: game_instance, + }) +} + +pub fn turn( + storage: &mut Storage, + sessions: &HashMap, + msg_source: ActorId, + step: u8, + session_for_account: Option, +) -> Result { + check_allow_messages(storage, msg_source)?; + let player = get_player( + sessions, + &msg_source, + &session_for_account, + ActionsForSession::StartGame, + ); + + let game_instance = storage + .current_games + .get_mut(&player) + .ok_or(GameError::GameIsNotStarted)?; + + if game_instance.board[step as usize].is_some() { + return Err(GameError::CellIsAlreadyOccupied); + } + if game_instance.game_over { + return Err(GameError::GameIsAlreadyOver); + } + let block_timestamp = exec::block_timestamp(); + if game_instance.last_time + storage.config.turn_deadline_ms < block_timestamp { + return Err(GameError::MissedYourTurn); + } + game_instance.board[step as usize] = Some(game_instance.player_mark); + game_instance.last_time = block_timestamp; + + if let Some(mark) = get_result(&game_instance.board.clone()) { + if mark == game_instance.player_mark { + game_over(game_instance, &player, &storage.config, GameResult::Player); + } else { + game_over(game_instance, &player, &storage.config, GameResult::Bot); + } + return Ok(Event::GameFinished { + game: game_instance.clone(), + player_address: player, + }); + } + + let bot_step = make_move(game_instance); + + if let Some(step_num) = bot_step { + game_instance.board[step_num] = Some(game_instance.bot_mark); + } + + if let Some(mark) = get_result(&game_instance.board.clone()) { + if mark == game_instance.player_mark { + game_over( + game_instance, + &msg_source, + &storage.config, + GameResult::Player, + ); + } else { + game_over(game_instance, &msg_source, &storage.config, GameResult::Bot); + } + return Ok(Event::GameFinished { + game: game_instance.clone(), + player_address: player, + }); + } else if !game_instance.board.contains(&None) || bot_step.is_none() { + game_over( + game_instance, + &msg_source, + &storage.config, + GameResult::Draw, + ); + return Ok(Event::GameFinished { + game: game_instance.clone(), + player_address: player, + }); + } + Ok(Event::MoveMade { + game: game_instance.clone(), + }) +} + +pub fn skip( + storage: &mut Storage, + sessions: &HashMap, + msg_source: ActorId, + session_for_account: Option, +) -> Result { + check_allow_messages(storage, msg_source)?; + let player = get_player( + sessions, + &msg_source, + &session_for_account, + ActionsForSession::StartGame, + ); + + let game_instance = storage + .current_games + .get_mut(&player) + .ok_or(GameError::GameIsNotStarted)?; + + if game_instance.game_over { + return Err(GameError::GameIsAlreadyOver); + } + let block_timestamp = exec::block_timestamp(); + if game_instance.last_time + storage.config.turn_deadline_ms >= block_timestamp { + return Err(GameError::NotMissedTurnMakeMove); + } + + let bot_step = make_move(game_instance); + game_instance.last_time = block_timestamp; + + match bot_step { + Some(step_num) => { + game_instance.board[step_num] = Some(game_instance.bot_mark); + let win = get_result(&game_instance.board.clone()); + if let Some(mark) = win { + if mark == game_instance.player_mark { + game_over(game_instance, &player, &storage.config, GameResult::Player); + } else { + game_over(game_instance, &player, &storage.config, GameResult::Bot); + } + return Ok(Event::GameFinished { + game: game_instance.clone(), + player_address: player, + }); + } else if !game_instance.board.contains(&None) { + game_over(game_instance, &player, &storage.config, GameResult::Draw); + return Ok(Event::GameFinished { + game: game_instance.clone(), + player_address: player, + }); + } + } + None => { + game_over(game_instance, &player, &storage.config, GameResult::Draw); + return Ok(Event::GameFinished { + game: game_instance.clone(), + player_address: player, + }); + } + } + Ok(Event::MoveMade { + game: game_instance.clone(), + }) +} + +fn game_over( + game_instance: &mut GameInstance, + player: &ActorId, + config: &Config, + result: GameResult, +) { + game_instance.game_over = true; + game_instance.game_result = Some(result); + send_delayed_message_to_remove_game(*player, config.gas_to_remove_game, config.time_interval); +} + +pub fn remove_game_instance( + storage: &mut Storage, + msg_source: ActorId, + account: ActorId, +) -> Result { + if msg_source != exec::program_id() { + return Err(GameError::MessageOnlyForProgram); + } + + let game_instance = storage + .current_games + .get(&account) + .expect("Unexpected: the game does not exist"); + + if game_instance.game_over { + storage.current_games.remove(&account); + } + Ok(Event::GameInstanceRemoved) +} + +pub fn remove_game_instances( + storage: &mut Storage, + msg_source: ActorId, + accounts: Option>, +) -> Result { + if !storage.admins.contains(&msg_source) { + return Err(GameError::NotAdmin); + } + match accounts { + Some(accounts) => { + for account in accounts { + storage.current_games.remove(&account); + } + } + None => { + storage.current_games.retain(|_, game_instance| { + exec::block_timestamp() - game_instance.last_time + < storage.config.time_interval as u64 * storage.config.s_per_block + }); + } + } + Ok(Event::GameInstanceRemoved) +} + +pub fn add_admin( + storage: &mut Storage, + msg_source: ActorId, + admin: ActorId, +) -> Result { + if !storage.admins.contains(&msg_source) { + return Err(GameError::NotAdmin); + } + storage.admins.push(admin); + Ok(Event::AdminAdded) +} +pub fn remove_admin( + storage: &mut Storage, + msg_source: ActorId, + admin: ActorId, +) -> Result { + if !storage.admins.contains(&msg_source) { + return Err(GameError::NotAdmin); + } + storage.admins.retain(|id| *id != admin); + Ok(Event::AdminRemoved) +} + +pub fn update_config( + storage: &mut Storage, + msg_source: ActorId, + s_per_block: Option, + gas_to_remove_game: Option, + time_interval: Option, + turn_deadline_ms: Option, + gas_to_delete_session: Option, +) -> Result { + if !storage.admins.contains(&msg_source) { + return Err(GameError::NotAdmin); + } + + if let Some(s_per_block) = s_per_block { + storage.config.s_per_block = s_per_block; + } + if let Some(gas_to_remove_game) = gas_to_remove_game { + storage.config.gas_to_remove_game = gas_to_remove_game; + } + if let Some(time_interval) = time_interval { + storage.config.time_interval = time_interval; + } + if let Some(turn_deadline_ms) = turn_deadline_ms { + storage.config.turn_deadline_ms = turn_deadline_ms; + } + if let Some(gas_to_delete_session) = gas_to_delete_session { + storage.config.gas_to_delete_session = gas_to_delete_session; + } + Ok(Event::ConfigUpdated) +} + +pub fn allow_messages( + storage: &mut Storage, + msg_source: ActorId, + messages_allowed: bool, +) -> Result { + if !storage.admins.contains(&msg_source) { + return Err(GameError::NotAdmin); + } + storage.messages_allowed = messages_allowed; + Ok(Event::StatusMessagesUpdated) +} + +fn check_allow_messages(storage: &Storage, msg_source: ActorId) -> Result<(), GameError> { + if !storage.messages_allowed && !storage.admins.contains(&msg_source) { + return Err(GameError::NotAllowedToSendMessages); + } + Ok(()) +} + +fn make_move(game: &GameInstance) -> Option { + match game.bot_mark { + Mark::O => { + // if on any of the winning lines there are 2 own pieces and 0 strangers + // make move + let step = check_line(&game.board, 2, 0); + if let Some(step_num) = step { + return Some(step_num); + } + + // if on any of the winning lines there are 2 stranger pieces and 0 own + // make move + let step = check_line(&game.board, 0, 2); + if let Some(step_num) = step { + return Some(step_num); + } + // if on any of the winning lines there are 1 own pieces and 0 strangers + // make move + let step = check_line(&game.board, 1, 0); + if let Some(step_num) = step { + return Some(step_num); + } + // if the center is empty, then we occupy the center + if game.board[4] != Some(Mark::O) && game.board[4] != Some(Mark::X) { + return Some(4); + } + // occupy the first cell + if game.board[0] != Some(Mark::O) && game.board[0] != Some(Mark::X) { + return Some(0); + } + } + Mark::X => { + // if on any of the winning lines there are 2 own pieces and 0 strangers + // make move + let step = check_line(&game.board, 0, 2); + + if let Some(step_num) = step { + return Some(step_num); + } + // if on any of the winning lines there are 2 stranger pieces and 0 own + // make move + let step = check_line(&game.board, 2, 0); + if let Some(step_num) = step { + return Some(step_num); + } + // if on any of the winning lines there are 1 own pieces and 0 strangers + // make move + let step = check_line(&game.board, 0, 1); + + if let Some(step_num) = step { + return Some(step_num); + } + // if the center is empty, then we occupy the center + if game.board[4] != Some(Mark::O) && game.board[4] != Some(Mark::X) { + return Some(4); + } + // occupy the first cell + if game.board[0] != Some(Mark::O) && game.board[0] != Some(Mark::X) { + return Some(0); + } + } + } + None +} + +fn check_line(map: &[Option], sum_o: u8, sum_x: u8) -> Option { + for line in VICTORIES.iter() { + let mut o = 0; + let mut x = 0; + for i in 0..3 { + if map[line[i]] == Some(Mark::O) { + o += 1; + } + if map[line[i]] == Some(Mark::X) { + x += 1; + } + } + + if sum_o == o && sum_x == x { + for i in 0..3 { + if map[line[i]] != Some(Mark::O) && map[line[i]] != Some(Mark::X) { + return Some(line[i]); + } + } + } + } + None +} + +fn get_result(map: &[Option]) -> Option { + for i in VICTORIES.iter() { + if map[i[0]] == Some(Mark::X) && map[i[1]] == Some(Mark::X) && map[i[2]] == Some(Mark::X) { + return Some(Mark::X); + } + + if map[i[0]] == Some(Mark::O) && map[i[1]] == Some(Mark::O) && map[i[2]] == Some(Mark::O) { + return Some(Mark::O); + } + } + None +} + +fn random_turn(account: ActorId) -> u8 { + let random_input: [u8; 32] = account.into(); + let (random, _) = exec::random(random_input).expect("Error in getting random number"); + random[0] % 2 +} + +fn send_delayed_message_to_remove_game( + account: ActorId, + gas_to_remove_game: u64, + time_interval: u32, +) { + let request = [ + "TicTacToe".encode(), + "RemoveGameInstance".to_string().encode(), + (account).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_delayed( + exec::program_id(), + request, + gas_to_remove_game, + 0, + time_interval, + ) + .expect("Error in sending message"); +} + +fn get_player( + session_map: &HashMap, + msg_source: &ActorId, + session_for_account: &Option, + actions_for_session: ActionsForSession, +) -> ActorId { + let player = match session_for_account { + Some(account) => { + let session = session_map + .get(account) + .expect("This account has no valid session"); + assert!( + session.expires > exec::block_timestamp(), + "The session has already expired" + ); + assert!( + session.allowed_actions.contains(&actions_for_session), + "This message is not allowed" + ); + assert_eq!( + session.key, *msg_source, + "The account is not approved for this session" + ); + *account + } + None => *msg_source, + }; + player +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/game/mod.rs b/contracts/tic-tac-toe/sails/app/src/services/game/mod.rs new file mode 100644 index 000000000..2887eaab7 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/game/mod.rs @@ -0,0 +1,208 @@ +use super::session::Storage as SessionStorage; +use crate::services; +use gstd::{exec, msg}; +use sails_rs::{collections::HashMap, gstd::service, prelude::*}; +mod funcs; +pub mod utils; +use utils::*; + +#[derive(Default)] +pub struct Storage { + admins: Vec, + current_games: HashMap, + config: Config, + messages_allowed: bool, + dns_info: Option<(ActorId, String)>, +} + +impl Storage { + pub fn get_config() -> &'static Config { + unsafe { &STORAGE.as_ref().expect("Storage is not initialized").config } + } +} + +static mut STORAGE: Option = None; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + GameFinished { + game: GameInstance, + player_address: ActorId, + }, + GameStarted { + game: GameInstance, + }, + MoveMade { + game: GameInstance, + }, + GameInstanceRemoved, + ConfigUpdated, + AdminRemoved, + AdminAdded, + StatusMessagesUpdated, + Killed { + inheritor: ActorId, + }, +} + +#[derive(Clone)] +pub struct GameService(()); + +impl GameService { + pub async fn init(config: Config, dns_id_and_name: Option<(ActorId, String)>) -> Self { + unsafe { + STORAGE = Some(Storage { + admins: vec![msg::source()], + current_games: HashMap::with_capacity(10_000), + config, + messages_allowed: true, + dns_info: dns_id_and_name.clone(), + }); + } + + if let Some((id, name)) = dns_id_and_name { + let request = [ + "Dns".encode(), + "AddNewProgram".to_string().encode(), + (name, exec::program_id()).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_for_reply(id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } + Self(()) + } + pub fn get_mut(&mut self) -> &'static mut Storage { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + pub fn get(&self) -> &'static Storage { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[service(events = Event)] +impl GameService { + pub fn new() -> Self { + Self(()) + } + pub fn start_game(&mut self, session_for_account: Option) { + let storage = self.get_mut(); + let sessions = SessionStorage::get_session_map(); + let event = services::utils::panicking(|| { + funcs::start_game(storage, sessions, msg::source(), session_for_account) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn turn(&mut self, step: u8, session_for_account: Option) { + let storage = self.get_mut(); + let sessions = SessionStorage::get_session_map(); + let event = services::utils::panicking(|| { + funcs::turn(storage, sessions, msg::source(), step, session_for_account) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn skip(&mut self, session_for_account: Option) { + let storage = self.get_mut(); + let sessions = SessionStorage::get_session_map(); + let event = services::utils::panicking(|| { + funcs::skip(storage, sessions, msg::source(), session_for_account) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn remove_game_instance(&mut self, account: ActorId) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::remove_game_instance(storage, msg::source(), account) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn remove_game_instances(&mut self, accounts: Option>) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::remove_game_instances(storage, msg::source(), accounts) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn add_admin(&mut self, admin: ActorId) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::add_admin(storage, msg::source(), admin)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn remove_admin(&mut self, admin: ActorId) { + let storage = self.get_mut(); + let event = + services::utils::panicking(|| funcs::remove_admin(storage, msg::source(), admin)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn update_config( + &mut self, + s_per_block: Option, + gas_to_remove_game: Option, + time_interval: Option, + turn_deadline_ms: Option, + gas_to_delete_session: Option, + ) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::update_config( + storage, + msg::source(), + s_per_block, + gas_to_remove_game, + time_interval, + turn_deadline_ms, + gas_to_delete_session, + ) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn allow_messages(&mut self, messages_allowed: bool) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::allow_messages(storage, msg::source(), messages_allowed) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub async fn kill(&mut self, inheritor: ActorId) { + let storage = self.get(); + if !storage.admins.contains(&msg::source()) { + services::utils::panic(GameError::NotAdmin); + } + if let Some((id, _name)) = &storage.dns_info { + let request = ["Dns".encode(), "DeleteMe".to_string().encode(), ().encode()].concat(); + + msg::send_bytes_with_gas_for_reply(*id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } + + self.notify_on(Event::Killed { inheritor }) + .expect("Notification Error"); + exec::exit(inheritor); + } + + pub fn admins(&self) -> &'static Vec { + &self.get().admins + } + pub fn game(&self, player_id: ActorId) -> Option { + self.get().current_games.get(&player_id).cloned() + } + pub fn all_games(&self) -> Vec<(ActorId, GameInstance)> { + self.get().current_games.clone().into_iter().collect() + } + pub fn config(&self) -> &'static Config { + &self.get().config + } + pub fn messages_allowed(&self) -> &'static bool { + &self.get().messages_allowed + } + pub fn dns_info(&self) -> Option<(ActorId, String)> { + self.get().dns_info.clone() + } +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/game/utils.rs b/contracts/tic-tac-toe/sails/app/src/services/game/utils.rs new file mode 100644 index 000000000..d4d818563 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/game/utils.rs @@ -0,0 +1,72 @@ +use sails_rs::prelude::*; + +pub const VICTORIES: [[usize; 3]; 8] = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], +]; + +#[derive(Debug, Default, Encode, Clone, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Config { + pub s_per_block: u64, + pub gas_to_remove_game: u64, + pub gas_to_delete_session: u64, + pub time_interval: u32, + pub turn_deadline_ms: u64, + pub minimum_session_duration_ms: u64, +} + +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum GameError { + GameIsAlreadyStarted, + CellIsAlreadyOccupied, + GameIsAlreadyOver, + MissedYourTurn, + NotMissedTurnMakeMove, + GameIsNotStarted, + MessageOnlyForProgram, + NotAdmin, + MessageProcessingSuspended, + NotAllowedToSendMessages, +} + +/// Represent game instance status. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum GameResult { + Player, + Bot, + Draw, +} + +/// Represent concrete game instance. +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct GameInstance { + pub board: Vec>, + pub player_mark: Mark, + pub bot_mark: Mark, + pub last_time: u64, + pub game_over: bool, + pub game_result: Option, +} + +/// Indicates tic-tac-toe board mark-state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Mark { + X, + O, +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/mod.rs b/contracts/tic-tac-toe/sails/app/src/services/mod.rs new file mode 100644 index 000000000..59881fa95 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/mod.rs @@ -0,0 +1,3 @@ +pub mod game; +pub mod session; +pub mod utils; diff --git a/contracts/tic-tac-toe/sails/app/src/services/session/funcs.rs b/contracts/tic-tac-toe/sails/app/src/services/session/funcs.rs new file mode 100644 index 000000000..3973fa8f5 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/session/funcs.rs @@ -0,0 +1,141 @@ +use crate::services::game::utils::Config; +use crate::services::session::{Event, SessionData, SessionError, SessionMap, SignatureData}; +use gstd::{exec, msg}; +use sails_rs::{collections::HashMap, prelude::*}; + +use schnorrkel::PublicKey; + +pub fn create_session( + sessions: &mut SessionMap, + config: &Config, + signature_data: SignatureData, + signature: Option>, +) -> Result { + if signature_data.duration < config.minimum_session_duration_ms { + return Err(SessionError::DurationIsSmall); + } + + let msg_source = msg::source(); + let block_timestamp = exec::block_timestamp(); + let block_height = exec::block_height(); + + let expires = block_timestamp + signature_data.duration; + + let number_of_blocks = + u32::try_from(signature_data.duration.div_ceil(config.s_per_block * 1_000)) + .expect("Duration is too large"); + + if signature_data.allowed_actions.is_empty() { + return Err(SessionError::ThereAreNoAllowedMessages); + } + + let account = match signature { + Some(sig_bytes) => { + check_if_session_exists(sessions, &signature_data.key)?; + let pub_key: [u8; 32] = (signature_data.key).into(); + let mut prefix = b"".to_vec(); + let mut message = SignatureData { + key: msg_source, + duration: signature_data.duration, + allowed_actions: signature_data.allowed_actions.clone(), + } + .encode(); + let mut postfix = b"".to_vec(); + prefix.append(&mut message); + prefix.append(&mut postfix); + + verify(&sig_bytes, prefix, pub_key)?; + sessions.entry(signature_data.key).insert(SessionData { + key: msg_source, + expires, + allowed_actions: signature_data.allowed_actions, + expires_at_block: block_height + number_of_blocks, + }); + signature_data.key + } + None => { + check_if_session_exists(sessions, &msg_source)?; + + sessions.entry(msg_source).insert(SessionData { + key: signature_data.key, + expires, + allowed_actions: signature_data.allowed_actions, + expires_at_block: block_height + number_of_blocks, + }); + msg_source + } + }; + + let request = [ + "Session".encode(), + "DeleteSessionFromProgram".to_string().encode(), + (account).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_delayed( + exec::program_id(), + request, + config.gas_to_delete_session, + 0, + number_of_blocks, + ) + .expect("Error in sending message"); + + Ok(Event::SessionCreated) +} + +pub fn delete_session_from_program( + sessions: &mut SessionMap, + session_for_account: ActorId, +) -> Result { + if msg::source() != exec::program_id() { + return Err(SessionError::MessageOnlyForProgram); + } + + if let Some(session) = sessions.remove(&session_for_account) { + if session.expires_at_block > exec::block_height() { + return Err(SessionError::TooEarlyToDeleteSession); + } + } + Ok(Event::SessionDeleted) +} + +pub fn delete_session_from_account(sessions: &mut SessionMap) -> Result { + if sessions.remove(&msg::source()).is_none() { + return Err(SessionError::NoSession); + } + Ok(Event::SessionDeleted) +} + +fn verify, M: AsRef<[u8]>>( + signature: &[u8], + message: M, + pubkey: P, +) -> Result<(), SessionError> { + let signature = + schnorrkel::Signature::from_bytes(signature).map_err(|_| SessionError::BadSignature)?; + let pub_key = PublicKey::from_bytes(pubkey.as_ref()).map_err(|_| SessionError::BadPublicKey)?; + pub_key + .verify_simple(b"substrate", message.as_ref(), &signature) + .map(|_| ()) + .map_err(|_| SessionError::VerificationFailed) +} + +fn check_if_session_exists( + session_map: &HashMap, + account: &ActorId, +) -> Result<(), SessionError> { + if let Some(SessionData { + key: _, + expires: _, + allowed_actions: _, + expires_at_block, + }) = session_map.get(account) + { + if *expires_at_block > exec::block_height() { + return Err(SessionError::AlreadyHaveActiveSession); + } + } + Ok(()) +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/session/mod.rs b/contracts/tic-tac-toe/sails/app/src/services/session/mod.rs new file mode 100644 index 000000000..1532b163c --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/session/mod.rs @@ -0,0 +1,80 @@ +use super::game::Storage as GameStorage; +use crate::services; +use sails_rs::{collections::HashMap, gstd::service, prelude::*}; +mod funcs; +pub mod utils; +use utils::*; + +#[derive(Default)] +pub struct Storage(()); + +impl Storage { + pub fn get_session_map() -> &'static SessionMap { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +static mut STORAGE: Option = None; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + SessionCreated, + SessionDeleted, +} + +#[derive(Clone)] +pub struct SessionService(()); + +impl SessionService { + pub fn init() -> Self { + unsafe { + STORAGE = Some(HashMap::new()); + } + Self(()) + } + pub fn as_mut(&mut self) -> &'static mut SessionMap { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + pub fn as_ref(&self) -> &'static SessionMap { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[service(events = Event)] +impl SessionService { + pub fn new() -> Self { + Self(()) + } + pub fn create_session(&mut self, signature_data: SignatureData, signature: Option>) { + let sessions = self.as_mut(); + let config = GameStorage::get_config(); + let event = services::utils::panicking(|| { + funcs::create_session(sessions, config, signature_data, signature) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn delete_session_from_program(&mut self, session_for_account: ActorId) { + let sessions = self.as_mut(); + let event = services::utils::panicking(|| { + funcs::delete_session_from_program(sessions, session_for_account) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn delete_session_from_account(&mut self) { + let sessions = self.as_mut(); + let event = services::utils::panicking(|| funcs::delete_session_from_account(sessions)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn sessions(&self) -> Vec<(ActorId, SessionData)> { + self.as_ref().clone().into_iter().collect() + } + + pub fn session_for_the_account(&self, account: ActorId) -> Option { + self.as_ref().get(&account).cloned() + } +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/session/utils.rs b/contracts/tic-tac-toe/sails/app/src/services/session/utils.rs new file mode 100644 index 000000000..dfb33cdc5 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/session/utils.rs @@ -0,0 +1,51 @@ +use sails_rs::{collections::HashMap, prelude::*}; +pub type SessionMap = HashMap; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum SessionError { + BadSignature, + BadPublicKey, + VerificationFailed, + DurationIsSmall, + ThereAreNoAllowedMessages, + MessageOnlyForProgram, + TooEarlyToDeleteSession, + NoSession, + AlreadyHaveActiveSession, +} + +// This structure is for creating a gaming session, which allows players to predefine certain actions for an account that will play the game on their behalf for a certain period of time. +// Sessions can be used to send transactions from a dApp on behalf of a user without requiring their confirmation with a wallet. +// The user is guaranteed that the dApp can only execute transactions that comply with the allowed_actions of the session until the session expires. +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct SessionData { + // the address of the player who will play on behalf of the user + pub key: ActorId, + // until what time the session is valid + pub expires: u64, + // what messages are allowed to be sent by the account (key) + pub allowed_actions: Vec, + pub expires_at_block: u32, +} + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum ActionsForSession { + StartGame, + Move, + Skip, +} + +#[derive(Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct SignatureData { + pub key: ActorId, + pub duration: u64, + pub allowed_actions: Vec, +} diff --git a/contracts/tic-tac-toe/sails/app/src/services/utils.rs b/contracts/tic-tac-toe/sails/app/src/services/utils.rs new file mode 100644 index 000000000..9dee576e1 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/src/services/utils.rs @@ -0,0 +1,13 @@ +use core::fmt::Debug; +use gstd::{ext, format}; + +pub fn panicking Result>(f: F) -> T { + match f() { + Ok(v) => v, + Err(e) => panic(e), + } +} + +pub fn panic(err: impl Debug) -> ! { + ext::panic(&format!("{err:?}")) +} diff --git a/contracts/tic-tac-toe/sails/app/tests/test.rs b/contracts/tic-tac-toe/sails/app/tests/test.rs new file mode 100644 index 000000000..1394b4053 --- /dev/null +++ b/contracts/tic-tac-toe/sails/app/tests/test.rs @@ -0,0 +1,165 @@ +use sails_rs::calls::*; +use sails_rs::gtest::calls::*; +use tic_tac_toe_wasm::{ + traits::{TicTacToe, TicTacToeFactory}, + Config, GameResult, TicTacToe as TicTacToeClient, TicTacToeFactory as Factory, +}; + +#[tokio::test] +async fn test_play_game() { + let program_space = GTestRemoting::new(100.into()); + program_space.system().init_logger(); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/tic_tac_toe_wasm.opt.wasm"); + + let tic_tac_toe_factory = Factory::new(program_space.clone()); + let config = Config { + s_per_block: 3, + gas_to_remove_game: 5_000_000_000, + time_interval: 20, + turn_deadline_ms: 30_000, + gas_to_delete_session: 5_000_000_000, + minimum_session_duration_ms: 180_000, + }; + let tic_tac_toe_id = tic_tac_toe_factory + .new(config, None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = TicTacToeClient::new(program_space); + // start_game + client + .start_game(None) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + // check game instance + let game_instance = client.game(100.into()).recv(tic_tac_toe_id).await.unwrap(); + assert!(game_instance.is_some()); + + client + .turn(0.into(), None) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + + client + .turn(1.into(), None) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + + // check game instance + let game_instance = client + .game(100.into()) + .recv(tic_tac_toe_id) + .await + .unwrap() + .unwrap(); + assert_eq!(game_instance.game_over, true); + assert_eq!(game_instance.game_result, Some(GameResult::Bot)); + // println!("GAME: {:?}", game_instance); +} + +#[tokio::test] +async fn add_and_remove_admin() { + let program_space = GTestRemoting::new(100.into()); + program_space.system().init_logger(); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/tic_tac_toe_wasm.opt.wasm"); + + let tic_tac_toe_factory = Factory::new(program_space.clone()); + let config = Config { + s_per_block: 3, + gas_to_remove_game: 5_000_000_000, + time_interval: 20, + turn_deadline_ms: 30_000, + gas_to_delete_session: 5_000_000_000, + minimum_session_duration_ms: 180_000, + }; + let tic_tac_toe_id = tic_tac_toe_factory + .new(config, None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = TicTacToeClient::new(program_space.clone()); + // add admin + client + .add_admin(101.into()) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + // check state + let admins = client.admins().recv(tic_tac_toe_id).await.unwrap(); + assert_eq!(admins, vec![100.into(), 101.into()]); + + // remove admin + client + .remove_admin(101.into()) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + // check state + let admins = client.admins().recv(tic_tac_toe_id).await.unwrap(); + assert_eq!(admins, vec![100.into()]); +} + +#[tokio::test] +async fn allow_messages() { + let program_space = GTestRemoting::new(100.into()); + program_space.system().init_logger(); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/tic_tac_toe_wasm.opt.wasm"); + + let tic_tac_toe_factory = Factory::new(program_space.clone()); + let config = Config { + s_per_block: 3, + gas_to_remove_game: 5_000_000_000, + time_interval: 20, + turn_deadline_ms: 30_000, + gas_to_delete_session: 5_000_000_000, + minimum_session_duration_ms: 180_000, + }; + let tic_tac_toe_id = tic_tac_toe_factory + .new(config, None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = TicTacToeClient::new(program_space.clone()); + // allow messages in false + client + .allow_messages(false) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + // check state + let messages_allowed = client + .messages_allowed() + .recv(tic_tac_toe_id) + .await + .unwrap(); + assert_eq!(messages_allowed, false); + + let res = client + .start_game(None) + .with_args(GTestArgs::new(101.into())) + .send_recv(tic_tac_toe_id) + .await; + assert!(res.is_err()); + + // start_game + client + .start_game(None) + .send_recv(tic_tac_toe_id) + .await + .unwrap(); + // check game instance + let game_instance = client.game(100.into()).recv(tic_tac_toe_id).await.unwrap(); + assert!(game_instance.is_some()); +} diff --git a/contracts/tic-tac-toe/sails/wasm/Cargo.toml b/contracts/tic-tac-toe/sails/wasm/Cargo.toml new file mode 100644 index 000000000..ecc8e3518 --- /dev/null +++ b/contracts/tic-tac-toe/sails/wasm/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tic-tac-toe-wasm" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tic-tac-toe-app = { path = "../app" } +sails-rs.workspace = true + +[build-dependencies] +gear-wasm-builder.workspace = true +sails-idl-gen.workspace = true +sails-client-gen.workspace = true +tic-tac-toe-app = { path = "../app" } + +[lib] +crate-type = ["rlib"] +name = "tic_tac_toe_wasm" diff --git a/contracts/tic-tac-toe/sails/wasm/build.rs b/contracts/tic-tac-toe/sails/wasm/build.rs new file mode 100644 index 000000000..f8922b91b --- /dev/null +++ b/contracts/tic-tac-toe/sails/wasm/build.rs @@ -0,0 +1,20 @@ +use sails_client_gen::ClientGenerator; +use sails_idl_gen::program; +use std::{env, fs::File, path::PathBuf}; +use tic_tac_toe_app::Program; + +fn main() { + gear_wasm_builder::build(); + + let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let idl_file_path = manifest_dir_path.join("tic-tac-toe.idl"); + + let idl_file = File::create(idl_file_path.clone()).unwrap(); + + program::generate_idl::(idl_file).unwrap(); + + ClientGenerator::from_idl_path(&idl_file_path) + .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("tic_tac_toe_client.rs")) + .unwrap(); +} diff --git a/contracts/tic-tac-toe/sails/wasm/src/lib.rs b/contracts/tic-tac-toe/sails/wasm/src/lib.rs new file mode 100644 index 000000000..a08449bbb --- /dev/null +++ b/contracts/tic-tac-toe/sails/wasm/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] +include!(concat!(env!("OUT_DIR"), "/tic_tac_toe_client.rs")); +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +#[cfg(target_arch = "wasm32")] +pub use tic_tac_toe_app::wasm::*; diff --git a/contracts/tic-tac-toe/sails/wasm/tic-tac-toe.idl b/contracts/tic-tac-toe/sails/wasm/tic-tac-toe.idl new file mode 100644 index 000000000..d37cd5634 --- /dev/null +++ b/contracts/tic-tac-toe/sails/wasm/tic-tac-toe.idl @@ -0,0 +1,96 @@ +type Config = struct { + s_per_block: u64, + gas_to_remove_game: u64, + gas_to_delete_session: u64, + time_interval: u32, + turn_deadline_ms: u64, + minimum_session_duration_ms: u64, +}; + +type SignatureData = struct { + key: actor_id, + duration: u64, + allowed_actions: vec ActionsForSession, +}; + +type ActionsForSession = enum { + StartGame, + Move, + Skip, +}; + +type SessionData = struct { + key: actor_id, + expires: u64, + allowed_actions: vec ActionsForSession, + expires_at_block: u32, +}; + +type GameInstance = struct { + board: vec opt Mark, + player_mark: Mark, + bot_mark: Mark, + last_time: u64, + game_over: bool, + game_result: opt GameResult, +}; + +type Mark = enum { + X, + O, +}; + +type GameResult = enum { + Player, + Bot, + Draw, +}; + +constructor { + New : (config: Config, dns_id_and_name: opt struct { actor_id, str }); +}; + +service Session { + CreateSession : (signature_data: SignatureData, signature: opt vec u8) -> null; + DeleteSessionFromAccount : () -> null; + DeleteSessionFromProgram : (session_for_account: actor_id) -> null; + query SessionForTheAccount : (account: actor_id) -> opt SessionData; + query Sessions : () -> vec struct { actor_id, SessionData }; + + events { + SessionCreated; + SessionDeleted; + } +}; + +service TicTacToe { + AddAdmin : (admin: actor_id) -> null; + AllowMessages : (messages_allowed: bool) -> null; + Kill : (inheritor: actor_id) -> null; + RemoveAdmin : (admin: actor_id) -> null; + RemoveGameInstance : (account: actor_id) -> null; + RemoveGameInstances : (accounts: opt vec actor_id) -> null; + Skip : (session_for_account: opt actor_id) -> null; + StartGame : (session_for_account: opt actor_id) -> null; + Turn : (step: u8, session_for_account: opt actor_id) -> null; + UpdateConfig : (s_per_block: opt u64, gas_to_remove_game: opt u64, time_interval: opt u32, turn_deadline_ms: opt u64, gas_to_delete_session: opt u64) -> null; + query Admins : () -> vec actor_id; + query AllGames : () -> vec struct { actor_id, GameInstance }; + query Config : () -> Config; + query DnsInfo : () -> opt struct { actor_id, str }; + query Game : (player_id: actor_id) -> opt GameInstance; + query MessagesAllowed : () -> bool; + + events { + GameFinished: struct { game: GameInstance, player_address: actor_id }; + GameStarted: struct { game: GameInstance }; + MoveMade: struct { game: GameInstance }; + GameInstanceRemoved; + ConfigUpdated; + AdminRemoved; + AdminAdded; + StatusMessagesUpdated; + Killed: struct { inheritor: actor_id }; + } +}; + From 9f414c0af9230831d6d52e25378d30dbc88aa99c Mon Sep 17 00:00:00 2001 From: Vadim Tokar <52737608+vraja-nayaka@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:41:39 +0400 Subject: [PATCH 09/94] Tic-tac-toe: add salis (#437) --- frontend/apps/tic-tac-toe/README.md | 2 + frontend/apps/tic-tac-toe/src/app.tsx | 13 +- .../apps/tic-tac-toe/src/app/hocs/index.tsx | 7 +- .../src/app/hocs/query-provider.tsx | 23 + .../src/app/hooks/use-is-app-ready.ts | 5 - .../apps/tic-tac-toe/src/app/utils/index.ts | 1 + .../tic-tac-toe/src/app/utils/sails/index.ts | 2 + .../tic-tac-toe/src/app/utils/sails/lib.ts | 583 ++++++++++++++++++ .../tic-tac-toe/src/app/utils/sails/sails.tsx | 12 + .../game-countdown/game-countdown.tsx | 25 +- .../components/game-field/game-field.tsx | 84 +-- .../game-info-player-mark.tsx | 2 +- .../components/game-mark/game-mark.tsx | 4 +- .../game-skip-button/game-skip-button.tsx | 81 +-- .../game-start-button/game-start-button.tsx | 76 +-- .../tic-tac-toe/components/game/game.tsx | 36 +- .../src/features/tic-tac-toe/hooks.ts | 139 +---- .../tic-tac-toe/sails/events/index.ts | 3 + .../use-event-game-finished-subscription.ts | 25 + .../use-event-game-start-subscription.ts | 25 + .../use-event-move-made-subscription.ts | 25 + .../src/features/tic-tac-toe/sails/index.ts | 3 + .../tic-tac-toe/sails/messages/index.ts | 3 + .../sails/messages/use-skip-message.ts | 25 + .../sails/messages/use-start-game-message.ts | 25 + .../sails/messages/use-turn-message.ts | 25 + .../tic-tac-toe/sails/queries/index.ts | 2 + .../sails/queries/use-config-query.ts | 15 + .../sails/queries/use-game-query.ts | 17 + .../src/features/tic-tac-toe/store.ts | 6 +- .../src/features/tic-tac-toe/types.ts | 42 +- .../src/features/tic-tac-toe/utils.ts | 5 - frontend/apps/tic-tac-toe/src/pages/home.tsx | 11 +- frontend/dev/gstd-tic-tac-toe/.env.example | 12 + frontend/dev/gstd-tic-tac-toe/.eslintignore | 6 + frontend/dev/gstd-tic-tac-toe/.eslintrc | 3 + frontend/dev/gstd-tic-tac-toe/Dockerfile | 38 ++ frontend/dev/gstd-tic-tac-toe/README.md | 40 ++ frontend/dev/gstd-tic-tac-toe/index.html | 24 + frontend/dev/gstd-tic-tac-toe/package.json | 77 +++ .../dev/gstd-tic-tac-toe/postcss.config.js | 6 + .../favicons/android-chrome-192x192.png | Bin 0 -> 2710 bytes .../favicons/android-chrome-512x512.png | Bin 0 -> 10925 bytes .../public/favicons/apple-touch-icon.png | Bin 0 -> 2514 bytes .../public/favicons/browserconfig.xml | 9 + .../public/favicons/favicon-16x16.png | Bin 0 -> 704 bytes .../public/favicons/favicon-32x32.png | Bin 0 -> 835 bytes .../public/favicons/favicon.ico | Bin 0 -> 7406 bytes .../public/favicons/mstile-144x144.png | Bin 0 -> 2148 bytes .../public/favicons/mstile-150x150.png | Bin 0 -> 2236 bytes .../public/favicons/mstile-310x150.png | Bin 0 -> 2629 bytes .../public/favicons/mstile-310x310.png | Bin 0 -> 4720 bytes .../public/favicons/mstile-70x70.png | Bin 0 -> 1687 bytes .../public/favicons/safari-pinned-tab.svg | 18 + .../public/favicons/site.webmanifest | 19 + .../public/fonts/Anuphan-Bold.woff2 | 0 .../public/fonts/Anuphan-ExtraLight.woff2 | 0 .../public/fonts/Anuphan-Light.woff2 | 0 .../public/fonts/Anuphan-Medium.woff2 | 0 .../public/fonts/Anuphan-Regular.woff2 | 0 .../public/fonts/Anuphan-SemiBold.woff2 | 0 .../public/fonts/Anuphan-Thin.woff2 | 0 .../public/fonts/anuphan-variable.woff2 | Bin 0 -> 83504 bytes .../gstd-tic-tac-toe/public/sprites/icons.svg | 145 +++++ frontend/dev/gstd-tic-tac-toe/src/app.scss | 2 + frontend/dev/gstd-tic-tac-toe/src/app.tsx | 31 + .../dev/gstd-tic-tac-toe/src/app/consts.ts | 20 + .../gstd-tic-tac-toe/src/app/hocs/index.tsx | 78 +++ .../gstd-tic-tac-toe}/src/app/hooks/api.ts | 0 .../gstd-tic-tac-toe}/src/app/hooks/index.ts | 0 .../src/app/hooks/use-is-app-ready.ts | 34 + .../src/app/hooks/use-login-by-params.tsx | 0 .../src/app/hooks/use-once-read-state.ts | 0 .../src/app/hooks/use-watch-messages.ts | 0 .../dev/gstd-tic-tac-toe/src/app/types.ts | 33 + .../gstd-tic-tac-toe}/src/app/utils.ts | 0 .../src/assets/images/icons/burger-menu.svg | 3 + .../src/assets/images/icons/caret-down.svg | 10 + .../src/assets/images/icons/chevron-down.svg | 12 + .../src/assets/images/icons/chevron-left.svg | 12 + .../src/assets/images/icons/chevron-right.svg | 12 + .../src/assets/images/icons/chevrons-left.svg | 14 + .../assets/images/icons/chevrons-right.svg | 14 + .../src/assets/images/icons/cross.svg | 4 + .../src/assets/images/icons/discord.svg | 4 + .../src/assets/images/icons/gear-logo.svg | 4 + .../src/assets/images/icons/gear.svg | 4 + .../src/assets/images/icons/github.svg | 4 + .../src/assets/images/icons/medium.svg | 4 + .../src/assets/images/icons/search.svg | 11 + .../src/assets/images/icons/star.svg | 4 + .../src/assets/images/icons/tvara-coin.svg | 19 + .../src/assets/images/icons/twitter.svg | 4 + .../src/assets/images/icons/vara-coin.svg | 11 + .../src/assets/images/icons/vara-logo.svg | 21 + .../src/assets/images/icons/vara-sign.svg | 13 + .../src/assets/images/index.ts | 19 + .../src/assets/styles/common.scss | 40 ++ .../src/assets/styles/fonts.scss | 57 ++ .../src/assets/styles/index.scss | 2 + .../gstd-tic-tac-toe/src/components/index.ts | 3 + .../layout/header/header.module.scss | 48 ++ .../src/components/layout/header/header.tsx | 28 + .../src/components/layout/header/index.ts | 1 + .../components/layout/header/logo/index.ts | 1 + .../layout/header/logo/logo.module.scss | 43 ++ .../components/layout/header/logo/logo.tsx | 20 + .../src/components/layout/index.ts | 4 + .../src/components/layout/main-layout.tsx | 29 + .../components/layout/not-authorized/index.ts | 1 + .../not-authorized/not-authorized.module.scss | 59 ++ .../layout/not-authorized/not-authorized.tsx | 41 ++ .../layout/not-found/assets/images/404.jpg | Bin 0 -> 88878 bytes .../layout/not-found/assets/images/404.webp | Bin 0 -> 40876 bytes .../src/components/layout/not-found/index.ts | 1 + .../layout/not-found/not-found.module.scss | 46 ++ .../components/layout/not-found/not-found.tsx | 29 + .../loaders/api-loader/ApiLoader.module.scss | 24 + .../loaders/api-loader/ApiLoader.tsx | 7 + .../components/loaders/api-loader/index.ts | 3 + .../src/components/loaders/index.ts | 3 + .../loaders/loader/Loader.module.scss | 32 + .../src/components/loaders/loader/Loader.tsx | 5 + .../src/components/loaders/loader/index.ts | 3 + .../components/loaders/loading-error/index.ts | 1 + .../loading-error/loading-error.module.scss | 18 + .../loaders/loading-error/loading-error.tsx | 9 + .../src/components/ui/alert/alert.module.scss | 99 +++ .../src/components/ui/alert/alert.tsx | 23 + .../src/components/ui/alert/alert.types.ts | 20 + .../src/components/ui/alert/index.ts | 2 + .../components/ui/balance/Balance.module.scss | 40 ++ .../src/components/ui/balance/Balance.tsx | 30 + .../src/components/ui/balance/index.ts | 3 + .../src/components/ui/button/button.tsx | 74 +++ .../components/ui/button/buttons.module.scss | 209 +++++++ .../src/components/ui/button/index.ts | 2 + .../ui/container/container.module.scss | 13 + .../src/components/ui/container/container.tsx | 9 + .../src/components/ui/container/index.ts | 1 + .../components/ui/heading/Heading.module.scss | 40 ++ .../src/components/ui/heading/Heading.tsx | 35 ++ .../src/components/ui/heading/index.ts | 2 + .../src/components/ui/modal/Modal.module.scss | 49 ++ .../src/components/ui/modal/Modal.tsx | 65 ++ .../components/ui/modal/dialog.module.scss | 62 ++ .../src/components/ui/modal/dialog.tsx | 120 ++++ .../src/components/ui/modal/index.ts | 3 + .../src/components/ui/modal/modal.variants.ts | 39 ++ .../src/components/ui/modal/modal2.tsx | 41 ++ .../src/components/ui/scroll-area/index.ts | 1 + .../ui/scroll-area/scroll-area.module.scss | 51 ++ .../components/ui/scroll-area/scroll-area.tsx | 37 ++ .../src/components/ui/sprite.tsx | 15 + .../src/components/ui/table/index.ts | 1 + .../src/components/ui/table/table.module.scss | 61 ++ .../src/components/ui/table/table.tsx | 46 ++ .../src/components/ui/text-gradient/index.ts | 3 + .../text-gradient/text-gradient.module.scss | 8 + .../ui/text-gradient/text-gradient.tsx | 6 + .../src/components/ui/text/index.ts | 2 + .../src/components/ui/text/text.module.scss | 35 ++ .../src/components/ui/text/text.tsx | 34 + frontend/dev/gstd-tic-tac-toe/src/env.d.ts | 13 + .../account-available-balance/consts.ts | 6 + .../account-available-balance/hooks.ts | 69 +++ .../account-available-balance/types.ts | 14 + .../src/features/auth/consts.ts | 8 + .../src/features/auth/hooks.ts | 45 ++ .../src/features/auth/index.ts | 1 + .../src/features/auth/types.ts | 35 ++ .../src/features/auth/utils.ts | 3 + .../assets/images/icons/game-reward.svg | 5 + .../assets/images/icons/player-circle.svg | 11 + .../assets/images/icons/player-cross.svg | 6 + .../tic-tac-toe/assets/images/welcome-bg.png | Bin 0 -> 501184 bytes .../tic-tac-toe/assets/images/welcome-bg.webp | Bin 0 -> 245606 bytes .../src/features/tic-tac-toe/assets/index.ts | 3 + .../assets/meta/tic_tac_toe.meta.txt | 0 .../game-cell/game-cell.module.scss | 48 ++ .../components/game-cell/game-cell.tsx | 36 ++ .../tic-tac-toe/components/game-cell/index.ts | 1 + .../game-countdown/game-countdown.module.scss | 55 ++ .../game-countdown/game-countdown.tsx | 65 ++ .../components/game-countdown/index.ts | 1 + .../game-field/game-field.module.scss | 119 ++++ .../components/game-field/game-field.tsx | 113 ++++ .../components/game-field/index.ts | 1 + .../game-info-player-mark.module.scss | 42 ++ .../game-info-player-mark.tsx | 41 ++ .../components/game-info-player-mark/index.ts | 1 + .../components/game-mark/game-mark.tsx | 11 + .../tic-tac-toe/components/game-mark/index.ts | 1 + .../game-reward/game-reward.module.scss | 0 .../components/game-reward/game-reward.tsx | 0 .../components/game-reward/index.ts | 0 .../game-skip-button/game-skip-button.tsx | 84 +++ .../components/game-skip-button/index.ts | 1 + .../game-start-button/game-start-button.tsx | 84 +++ .../components/game-start-button/index.ts | 1 + .../components/game/game.module.scss | 90 +++ .../tic-tac-toe/components/game/game.tsx | 88 +++ .../tic-tac-toe/components/game/index.ts | 1 + .../components/ui/columns/columns.module.scss | 40 ++ .../ui/columns/components/column-left.tsx | 9 + .../ui/columns/components/column-right.tsx | 9 + .../columns/components/columns-container.tsx | 7 + .../components/ui/columns/index.ts | 3 + .../ui/typography/help-description.tsx | 9 + .../components/ui/typography/index.ts | 1 + .../ui/typography/typography.module.scss | 13 + .../tic-tac-toe/components/welcome/index.ts | 1 + .../components/welcome/welcome.module.scss | 88 +++ .../components/welcome/welcome.tsx | 33 + .../src/features/tic-tac-toe/hooks.ts | 184 ++++++ .../src/features/tic-tac-toe/index.ts | 2 + .../src/features/tic-tac-toe/store.ts | 8 + .../src/features/tic-tac-toe/types.ts | 43 ++ .../src/features/tic-tac-toe/utils.ts | 25 + .../src/features/tic-tac-toe/variants.ts | 28 + .../wallet/assets/images/icons/copy.svg | 8 + .../wallet/assets/images/icons/edit.svg | 3 + .../wallet/assets/images/icons/enkrypt.svg | 10 + .../wallet/assets/images/icons/exit.svg | 5 + .../wallet/assets/images/icons/nova.svg | 16 + .../wallet/assets/images/icons/polkadot.svg | 12 + .../wallet/assets/images/icons/subwallet.svg | 88 +++ .../wallet/assets/images/icons/talisman.svg | 12 + .../src/features/wallet/assets/index.ts | 8 + .../src/features/wallet/consts.ts | 22 + .../src/features/wallet/hooks.ts | 24 + .../src/features/wallet/types.ts | 6 + frontend/dev/gstd-tic-tac-toe/src/global.css | 43 ++ frontend/dev/gstd-tic-tac-toe/src/global.d.ts | 11 + frontend/dev/gstd-tic-tac-toe/src/main.tsx | 14 + .../dev/gstd-tic-tac-toe/src/pages/home.tsx | 36 ++ .../dev/gstd-tic-tac-toe/src/pages/index.tsx | 20 + .../src/pages/not-authorized.tsx | 12 + .../gstd-tic-tac-toe/src/pages/not-found.tsx | 5 + .../src/utils/_breakpoints.scss | 30 + .../gstd-tic-tac-toe/src/utils/_index.scss | 3 + .../gstd-tic-tac-toe/src/utils/_mixins.scss | 76 +++ .../src/utils/_variables.scss | 7 + .../dev/gstd-tic-tac-toe/src/vite-env.d.ts | 1 + .../dev/gstd-tic-tac-toe/tailwind.config.js | 9 + frontend/dev/gstd-tic-tac-toe/tsconfig.json | 32 + .../dev/gstd-tic-tac-toe/tsconfig.node.json | 9 + frontend/dev/gstd-tic-tac-toe/vite.config.ts | 31 + .../gasless-transactions/src/context/utils.ts | 4 +- 249 files changed, 5740 insertions(+), 412 deletions(-) create mode 100644 frontend/apps/tic-tac-toe/src/app/hocs/query-provider.tsx create mode 100644 frontend/apps/tic-tac-toe/src/app/utils/index.ts create mode 100644 frontend/apps/tic-tac-toe/src/app/utils/sails/index.ts create mode 100644 frontend/apps/tic-tac-toe/src/app/utils/sails/lib.ts create mode 100644 frontend/apps/tic-tac-toe/src/app/utils/sails/sails.tsx create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/index.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-finished-subscription.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-start-subscription.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-move-made-subscription.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/index.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/index.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-skip-message.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-start-game-message.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-turn-message.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/index.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-config-query.ts create mode 100644 frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-game-query.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/.env.example create mode 100644 frontend/dev/gstd-tic-tac-toe/.eslintignore create mode 100644 frontend/dev/gstd-tic-tac-toe/.eslintrc create mode 100644 frontend/dev/gstd-tic-tac-toe/Dockerfile create mode 100644 frontend/dev/gstd-tic-tac-toe/README.md create mode 100644 frontend/dev/gstd-tic-tac-toe/index.html create mode 100644 frontend/dev/gstd-tic-tac-toe/package.json create mode 100644 frontend/dev/gstd-tic-tac-toe/postcss.config.js create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/android-chrome-192x192.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/android-chrome-512x512.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/apple-touch-icon.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/browserconfig.xml create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/favicon-16x16.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/favicon-32x32.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/favicon.ico create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-144x144.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-150x150.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-310x150.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-310x310.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-70x70.png create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/safari-pinned-tab.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/public/favicons/site.webmanifest create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Bold.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-ExtraLight.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Light.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Medium.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Regular.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-SemiBold.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Thin.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/fonts/anuphan-variable.woff2 create mode 100644 frontend/dev/gstd-tic-tac-toe/public/sprites/icons.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/app.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/app.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/app/consts.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/app/hocs/index.tsx rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/app/hooks/api.ts (100%) rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/app/hooks/index.ts (100%) create mode 100644 frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-is-app-ready.ts rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/app/hooks/use-login-by-params.tsx (100%) rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/app/hooks/use-once-read-state.ts (100%) rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/app/hooks/use-watch-messages.ts (100%) create mode 100644 frontend/dev/gstd-tic-tac-toe/src/app/types.ts rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/app/utils.ts (100%) create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/burger-menu.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/caret-down.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-down.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-left.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-right.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-left.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-right.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/cross.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/discord.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear-logo.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/github.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/medium.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/search.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/star.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/tvara-coin.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/twitter.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-coin.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-logo.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-sign.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/images/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/styles/common.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/styles/fonts.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/assets/styles/index.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/header/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/main-layout.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-authorized/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-authorized/not-authorized.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-authorized/not-authorized.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-found/assets/images/404.jpg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-found/assets/images/404.webp create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-found/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-found/not-found.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/layout/not-found/not-found.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/api-loader/ApiLoader.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/api-loader/ApiLoader.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/api-loader/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/loader/Loader.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/loader/Loader.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/loader/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/loading-error/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/loading-error/loading-error.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/loaders/loading-error/loading-error.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/alert/alert.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/alert/alert.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/alert/alert.types.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/alert/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/balance/Balance.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/balance/Balance.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/balance/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/button/button.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/button/buttons.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/button/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/container/container.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/container/container.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/container/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/heading/Heading.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/heading/Heading.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/heading/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/Modal.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/Modal.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/dialog.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/dialog.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/modal.variants.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/modal/modal2.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/scroll-area/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/scroll-area/scroll-area.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/scroll-area/scroll-area.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/sprite.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/table/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/table/table.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/table/table.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/text-gradient/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/text-gradient/text-gradient.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/text-gradient/text-gradient.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/text/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/text/text.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/components/ui/text/text.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/env.d.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/account-available-balance/consts.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/account-available-balance/hooks.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/account-available-balance/types.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/auth/consts.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/auth/hooks.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/auth/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/auth/types.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/auth/utils.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/assets/images/icons/game-reward.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/assets/images/icons/player-circle.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/assets/images/icons/player-cross.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/assets/images/welcome-bg.png create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/assets/images/welcome-bg.webp create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/assets/index.ts rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/features/tic-tac-toe/assets/meta/tic_tac_toe.meta.txt (100%) create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-cell/game-cell.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-cell/game-cell.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-cell/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/game-countdown.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/game-countdown.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-field/game-field.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-field/game-field.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-field/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/game-info-player-mark.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/game-info-player-mark.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-mark/game-mark.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-mark/index.ts rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/features/tic-tac-toe/components/game-reward/game-reward.module.scss (100%) rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/features/tic-tac-toe/components/game-reward/game-reward.tsx (100%) rename frontend/{apps/tic-tac-toe => dev/gstd-tic-tac-toe}/src/features/tic-tac-toe/components/game-reward/index.ts (100%) create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-skip-button/game-skip-button.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-skip-button/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-start-button/game-start-button.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game-start-button/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game/game.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game/game.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/game/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/columns/columns.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/columns/components/column-left.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/columns/components/column-right.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/columns/components/columns-container.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/columns/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/typography/help-description.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/typography/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/ui/typography/typography.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/welcome/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/welcome/welcome.module.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/components/welcome/welcome.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/hooks.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/store.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/types.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/utils.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/tic-tac-toe/variants.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/copy.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/edit.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/enkrypt.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/exit.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/nova.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/polkadot.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/subwallet.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/images/icons/talisman.svg create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/assets/index.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/consts.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/hooks.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/features/wallet/types.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/global.css create mode 100644 frontend/dev/gstd-tic-tac-toe/src/global.d.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/src/main.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/pages/home.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/pages/index.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/pages/not-authorized.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/pages/not-found.tsx create mode 100644 frontend/dev/gstd-tic-tac-toe/src/utils/_breakpoints.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/utils/_index.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/utils/_mixins.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/utils/_variables.scss create mode 100644 frontend/dev/gstd-tic-tac-toe/src/vite-env.d.ts create mode 100644 frontend/dev/gstd-tic-tac-toe/tailwind.config.js create mode 100644 frontend/dev/gstd-tic-tac-toe/tsconfig.json create mode 100644 frontend/dev/gstd-tic-tac-toe/tsconfig.node.json create mode 100644 frontend/dev/gstd-tic-tac-toe/vite.config.ts diff --git a/frontend/apps/tic-tac-toe/README.md b/frontend/apps/tic-tac-toe/README.md index c88a9d627..a0d7f93be 100644 --- a/frontend/apps/tic-tac-toe/README.md +++ b/frontend/apps/tic-tac-toe/README.md @@ -12,6 +12,8 @@ React application of [Tic-Tac-Toe](https://wiki.gear-tech.io/docs/examples/Gaming/tictactoe) based on [Rust smart-contract](https://github.com/gear-foundation/dapps/tree/master/contracts/tic-tac-toe). +This is a frontend implementation for programs created using the [Sails Library](https://wiki.vara.network/docs/build/sails/). For programs built with [Gear library](https://wiki.vara.network/docs/build/gstd/), refer to the implementation [here](https://github.com/gear-foundation/dapps/tree/master/frontend/dev/gstd-tic-tac-toe). + ## Getting started ### Install packages: diff --git a/frontend/apps/tic-tac-toe/src/app.tsx b/frontend/apps/tic-tac-toe/src/app.tsx index 097414b81..ec9d43994 100644 --- a/frontend/apps/tic-tac-toe/src/app.tsx +++ b/frontend/apps/tic-tac-toe/src/app.tsx @@ -1,29 +1,26 @@ import './app.scss'; import { withProviders } from '@/app/hocs'; import { useInitGame, useInitGameSync } from '@/features/tic-tac-toe/hooks'; -import meta from '@/features/tic-tac-toe/assets/meta/tic_tac_toe.meta.txt'; import { Loader, LoadingError, MainLayout } from '@/components'; import '@gear-js/vara-ui/dist/style.css'; import { Routing } from '@/pages'; -import { useProgramMetadata } from './app/hooks'; function Component() { - const metadata = useProgramMetadata(meta); const { isGameReady } = useInitGame(); - const { errorGame: hasError } = useInitGameSync(metadata); + const { errorGame } = useInitGameSync(); return ( - {!!hasError && ( + {!!errorGame && (

Error in the Game contract :(

-            Error message: {hasError}
+            Error message: {errorGame.message}
           
)} - {!hasError && isGameReady && } - {!hasError && !isGameReady && } + {!errorGame && isGameReady && } + {!errorGame && !isGameReady && }
); } diff --git a/frontend/apps/tic-tac-toe/src/app/hocs/index.tsx b/frontend/apps/tic-tac-toe/src/app/hocs/index.tsx index d9714dbf7..b67cdbc33 100644 --- a/frontend/apps/tic-tac-toe/src/app/hocs/index.tsx +++ b/frontend/apps/tic-tac-toe/src/app/hocs/index.tsx @@ -14,9 +14,10 @@ import { EzTransactionsProvider, } from '@dapps-frontend/ez-transactions'; -import metaTxt from '@/features/tic-tac-toe/assets/meta/tic_tac_toe.meta.txt'; import { ADDRESS } from '@/app/consts'; import { Alert, alertStyles } from '@/components/ui/alert'; +import { QueryProvider } from './query-provider'; +import { useProgram } from '../utils'; function ApiProvider({ children }: ProviderProps) { return {children}; @@ -53,8 +54,9 @@ function GaslessTransactionsProvider({ children }: ProviderProps) { function SignlessTransactionsProvider({ children }: ProviderProps) { const { programId } = useDnsProgramIds(); + const program = useProgram(); return ( - + {children} ); @@ -66,6 +68,7 @@ const providers = [ AccountProvider, AlertProvider, DnsProvider, + QueryProvider, GaslessTransactionsProvider, SignlessTransactionsProvider, EzTransactionsProvider, diff --git a/frontend/apps/tic-tac-toe/src/app/hocs/query-provider.tsx b/frontend/apps/tic-tac-toe/src/app/hocs/query-provider.tsx new file mode 100644 index 000000000..82ae550f8 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/app/hocs/query-provider.tsx @@ -0,0 +1,23 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactNode } from 'react'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 0, + staleTime: Infinity, + refetchOnWindowFocus: false, + retry: false, + }, + }, +}); + +type Props = { + children: ReactNode; +}; + +const QueryProvider = ({ children }: Props) => ( + {children} +); + +export { QueryProvider }; diff --git a/frontend/apps/tic-tac-toe/src/app/hooks/use-is-app-ready.ts b/frontend/apps/tic-tac-toe/src/app/hooks/use-is-app-ready.ts index f7c155d27..060094a6e 100644 --- a/frontend/apps/tic-tac-toe/src/app/hooks/use-is-app-ready.ts +++ b/frontend/apps/tic-tac-toe/src/app/hooks/use-is-app-ready.ts @@ -22,11 +22,6 @@ export function useIsAppReadySync() { const { setIsAppReady } = useIsAppReady(); useAccountAvailableBalanceSync(); - console.log('----------------'); - console.log(isApiReady); - console.log(isAccountReady); - console.log(isAvailableBalanceReady); - console.log(isAuthReady); useEffect(() => { setIsAppReady(isApiReady && isAccountReady && isAvailableBalanceReady && isAuthReady); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/apps/tic-tac-toe/src/app/utils/index.ts b/frontend/apps/tic-tac-toe/src/app/utils/index.ts new file mode 100644 index 000000000..15858e186 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/app/utils/index.ts @@ -0,0 +1 @@ +export * from './sails'; diff --git a/frontend/apps/tic-tac-toe/src/app/utils/sails/index.ts b/frontend/apps/tic-tac-toe/src/app/utils/sails/index.ts new file mode 100644 index 000000000..538de8615 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/app/utils/sails/index.ts @@ -0,0 +1,2 @@ +export * from './sails'; +export * from './lib'; diff --git a/frontend/apps/tic-tac-toe/src/app/utils/sails/lib.ts b/frontend/apps/tic-tac-toe/src/app/utils/sails/lib.ts new file mode 100644 index 000000000..f8ca780ac --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/app/utils/sails/lib.ts @@ -0,0 +1,583 @@ +import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js'; +import { GearApi, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; + +type ActorId = string; +export interface Config { + s_per_block: number | string | bigint; + gas_to_remove_game: number | string | bigint; + gas_to_delete_session: number | string | bigint; + time_interval: number; + turn_deadline_ms: number | string | bigint; + minimum_session_duration_ms: number | string | bigint; +} + +export interface SignatureData { + key: ActorId; + duration: number | string | bigint; + allowed_actions: Array; +} + +export type ActionsForSession = 'StartGame' | 'Move' | 'Skip'; + +export interface SessionData { + key: ActorId; + expires: number | string | bigint; + allowed_actions: Array; + expires_at_block: number; +} + +export interface GameInstance { + board: Array; + player_mark: Mark; + bot_mark: Mark; + last_time: number | string | bigint; + game_over: boolean; + game_result: GameResult | null; +} + +export type Mark = 'X' | 'O'; + +export type GameResult = 'Player' | 'Bot' | 'Draw'; + +export class Program { + public readonly registry: TypeRegistry; + public readonly session: Session; + public readonly ticTacToe: TicTacToe; + + constructor( + public api: GearApi, + public programId?: `0x${string}`, + ) { + const types: Record = { + Config: { + s_per_block: 'u64', + gas_to_remove_game: 'u64', + gas_to_delete_session: 'u64', + time_interval: 'u32', + turn_deadline_ms: 'u64', + minimum_session_duration_ms: 'u64', + }, + SignatureData: { key: '[u8;32]', duration: 'u64', allowed_actions: 'Vec' }, + ActionsForSession: { _enum: ['StartGame', 'Move', 'Skip'] }, + SessionData: { + key: '[u8;32]', + expires: 'u64', + allowed_actions: 'Vec', + expires_at_block: 'u32', + }, + GameInstance: { + board: 'Vec>', + player_mark: 'Mark', + bot_mark: 'Mark', + last_time: 'u64', + game_over: 'bool', + game_result: 'Option', + }, + Mark: { _enum: ['X', 'O'] }, + GameResult: { _enum: ['Player', 'Bot', 'Draw'] }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.session = new Session(this); + this.ticTacToe = new TicTacToe(this); + } + + newCtorFromCode(code: Uint8Array | Buffer, config: Config): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + ['New', config], + '(String, Config)', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, config: Config) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + ['New', config], + '(String, Config)', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class Session { + constructor(private _program: Program) {} + + public createSession(signature_data: SignatureData, signature: `0x${string}` | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Session', 'CreateSession', signature_data, signature], + '(String, String, SignatureData, Option>)', + 'Null', + this._program.programId, + ); + } + + public deleteSessionFromAccount(): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Session', 'DeleteSessionFromAccount'], + '(String, String)', + 'Null', + this._program.programId, + ); + } + + public deleteSessionFromProgram(session_for_account: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Session', 'DeleteSessionFromProgram', session_for_account], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public async sessionForTheAccount( + account: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['Session', 'SessionForTheAccount', account]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as SessionData | null; + } + + public async sessions( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Session', 'Sessions']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<([u8;32], SessionData)>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, SessionData]>; + } + + public subscribeToSessionCreatedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Session' && getFnNamePrefix(payload) === 'SessionCreated') { + callback(null); + } + }); + } + + public subscribeToSessionDeletedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Session' && getFnNamePrefix(payload) === 'SessionDeleted') { + callback(null); + } + }); + } +} + +export class TicTacToe { + constructor(private _program: Program) {} + + public addAdmin(admin: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'AddAdmin', admin], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public allowMessages(messages_allowed: boolean): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'AllowMessages', messages_allowed], + '(String, String, bool)', + 'Null', + this._program.programId, + ); + } + + public removeAdmin(admin: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'RemoveAdmin', admin], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public removeGameInstance(account: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'RemoveGameInstance', account], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public removeGameInstances(accounts: Array | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'RemoveGameInstances', accounts], + '(String, String, Option>)', + 'Null', + this._program.programId, + ); + } + + public skip(session_for_account: ActorId | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'Skip', session_for_account], + '(String, String, Option<[u8;32]>)', + 'Null', + this._program.programId, + ); + } + + public startGame(session_for_account: ActorId | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'StartGame', session_for_account], + '(String, String, Option<[u8;32]>)', + 'Null', + this._program.programId, + ); + } + + public turn(step: number, session_for_account: ActorId | null): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['TicTacToe', 'Turn', step, session_for_account], + '(String, String, u8, Option<[u8;32]>)', + 'Null', + this._program.programId, + ); + } + + public updateConfig( + s_per_block: number | string | bigint | null, + gas_to_remove_game: number | string | bigint | null, + time_interval: number | null, + turn_deadline_ms: number | string | bigint | null, + gas_to_delete_session: number | string | bigint | null, + ): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + [ + 'TicTacToe', + 'UpdateConfig', + s_per_block, + gas_to_remove_game, + time_interval, + turn_deadline_ms, + gas_to_delete_session, + ], + '(String, String, Option, Option, Option, Option, Option)', + 'Null', + this._program.programId, + ); + } + + public async admins( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['TicTacToe', 'Admins']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public async allGames( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['TicTacToe', 'AllGames']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<([u8;32], GameInstance)>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, GameInstance]>; + } + + public async config( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['TicTacToe', 'Config']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Config)', reply.payload); + return result[2].toJSON() as unknown as Config; + } + + public async game( + player_id: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['TicTacToe', 'Game', player_id]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as GameInstance | null; + } + + public async messagesAllowed( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['TicTacToe', 'MessagesAllowed']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, bool)', reply.payload); + return result[2].toJSON() as unknown as boolean; + } + + public subscribeToGameFinishedEvent( + callback: (data: { game: GameInstance; player_address: ActorId }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'GameFinished') { + callback( + this._program.registry + .createType('(String, String, {"game":"GameInstance","player_address":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { game: GameInstance; player_address: ActorId }, + ); + } + }); + } + + public subscribeToGameStartedEvent( + callback: (data: { game: GameInstance }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'GameStarted') { + callback( + this._program.registry + .createType('(String, String, {"game":"GameInstance"})', message.payload)[2] + .toJSON() as unknown as { game: GameInstance }, + ); + } + }); + } + + public subscribeToMoveMadeEvent( + callback: (data: { game: GameInstance }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'MoveMade') { + callback( + this._program.registry + .createType('(String, String, {"game":"GameInstance"})', message.payload)[2] + .toJSON() as unknown as { game: GameInstance }, + ); + } + }); + } + + public subscribeToGameInstanceRemovedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'GameInstanceRemoved') { + callback(null); + } + }); + } + + public subscribeToConfigUpdatedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'ConfigUpdated') { + callback(null); + } + }); + } + + public subscribeToAdminRemovedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'AdminRemoved') { + callback(null); + } + }); + } + + public subscribeToAdminAddedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'AdminAdded') { + callback(null); + } + }); + } + + public subscribeToStatusMessagesUpdatedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'TicTacToe' && getFnNamePrefix(payload) === 'StatusMessagesUpdated') { + callback(null); + } + }); + } +} diff --git a/frontend/apps/tic-tac-toe/src/app/utils/sails/sails.tsx b/frontend/apps/tic-tac-toe/src/app/utils/sails/sails.tsx new file mode 100644 index 000000000..2cffde777 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/app/utils/sails/sails.tsx @@ -0,0 +1,12 @@ +import { useProgram as useGearJsProgram } from '@gear-js/react-hooks'; +import { Program } from '@/app/utils'; +import { useDnsProgramIds } from '@dapps-frontend/hooks'; + +const useProgram = () => { + const { programId } = useDnsProgramIds(); + const { data: program } = useGearJsProgram({ library: Program, id: programId }); + + return program; +}; + +export { useProgram }; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/game-countdown.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/game-countdown.tsx index 42d7443b9..411dfe97e 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/game-countdown.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-countdown/game-countdown.tsx @@ -1,16 +1,16 @@ -import styles from './game-countdown.module.scss'; +import clsx from 'clsx'; +import { useAtomValue } from 'jotai'; import Countdown, { CountdownRenderProps } from 'react-countdown'; +import styles from './game-countdown.module.scss'; import { GameMark } from '../game-mark'; import { useGame } from '../../hooks'; -import type { IGameInstance } from '../../types'; -import { toNumber } from '@/app/utils'; -import clsx from 'clsx'; import { BaseComponentProps } from '@/app/types'; -import { useAtomValue } from 'jotai'; import { stateChangeLoadingAtom } from '../../store'; +import { GameInstance } from '@/app/utils'; +import { useConfigQuery } from '../../sails'; type GameCountdownProps = BaseComponentProps & { - game: IGameInstance; + game: GameInstance; }; function Clock({ minutes, seconds }: CountdownRenderProps) { @@ -21,24 +21,25 @@ function Clock({ minutes, seconds }: CountdownRenderProps) { ); } -export function GameCountdown({ game: { playerMark, lastTime }, className }: GameCountdownProps) { - const { setCountdown, countdown, configState } = useGame(); +export function GameCountdown({ game: { player_mark, last_time }, className }: GameCountdownProps) { + const { setCountdown, countdown } = useGame(); + const { config } = useConfigQuery(); const isLoading = useAtomValue(stateChangeLoadingAtom); return (
- +
Your turn
- {!isLoading && countdown?.isActive && configState && ( + {!isLoading && countdown?.isActive && config && (
setCountdown((prev) => ({ - value: prev ? prev.value : '', + value: prev ? prev.value : 0, isActive: false, })) } diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-field/game-field.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-field/game-field.tsx index b226b68a9..e6b9ea757 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-field/game-field.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-field/game-field.tsx @@ -1,89 +1,53 @@ import clsx from 'clsx'; import styles from './game-field.module.scss'; import { GameCell } from '../game-cell'; -import type { IGameInstance } from '../../types'; import { GameMark } from '../game-mark'; -import { useGame, useGameMessage, useSubscriptionOnGameMessage } from '../../hooks'; +import { useGame } from '../../hooks'; import { calculateWinner } from '../../utils'; import { motion } from 'framer-motion'; import { variantsGameMark } from '../../variants'; import { BaseComponentProps } from '@/app/types'; -import { useEffect } from 'react'; import { useAtom } from 'jotai'; import { stateChangeLoadingAtom } from '../../store'; -import { useAccount, useAlert, useHandleCalculateGas } from '@gear-js/react-hooks'; -import { useCheckBalance, useDnsProgramIds } from '@dapps-frontend/hooks'; +import { useAccount, useAlert } from '@gear-js/react-hooks'; import { useEzTransactions } from '@dapps-frontend/ez-transactions'; -import { withoutCommas } from '@/app/utils'; -import { ProgramMetadata } from '@gear-js/api'; +import { GameInstance } from '@/app/utils'; +import { useEventGameFinishedSubscription, useEventMoveMadeSubscription, useTurnMessage } from '../../sails'; type GameFieldProps = BaseComponentProps & { - game: IGameInstance; - meta: ProgramMetadata; + game: GameInstance; }; -export function GameField({ game, meta }: GameFieldProps) { - const { programId } = useDnsProgramIds(); - const { signless, gasless } = useEzTransactions(); +export function GameField({ game }: GameFieldProps) { + const { turnMessage } = useTurnMessage(); + const { gasless } = useEzTransactions(); const { countdown } = useGame(); - const [isLoading, setIsLoading] = useAtom(stateChangeLoadingAtom); const board = game.board; + const [isLoading, setIsLoading] = useAtom(stateChangeLoadingAtom); const { account } = useAccount(); const alert = useAlert(); - const calculateGas = useHandleCalculateGas(programId, meta); - const message = useGameMessage(meta); - const { checkBalance } = useCheckBalance({ - signlessPairVoucherId: signless.voucher?.id, - gaslessVoucherId: gasless.voucherId, - }); - const { subscribe, unsubscribe, isOpened } = useSubscriptionOnGameMessage(meta); - const winnerRow = calculateWinner(board); - const winnerColor = winnerRow ? game.playerMark === board[winnerRow[0][0]] : false; + useEventMoveMadeSubscription(); + useEventGameFinishedSubscription(); - const onError = () => { - unsubscribe(); - }; + const winnerRow = calculateWinner(board); + const winnerColor = winnerRow ? game.player_mark === board[winnerRow[0][0]] : false; - const onSelectCell = async (value: number) => { - if (!meta || !account || !programId) { + const onSelectCell = async (step: number) => { + if (!account) { return; } - const payload = { Turn: { step: value } }; - - let voucherId = gasless.voucherId; - if (account && gasless.isEnabled && !gasless.voucherId && !signless.isActive) { - voucherId = await gasless.requestVoucher(account.address); + setIsLoading(true); + try { + await turnMessage(step); + } catch (error) { + console.log(error); + alert.error((error instanceof Error && error.message) || 'Game turn error'); + setIsLoading(false); } - - if (!isLoading) { - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - - subscribe(); - const sendMessage = () => message({ payload, gasLimit, voucherId, onError }); - if (voucherId) { - sendMessage(); - } else { - checkBalance(gasLimit, sendMessage, onError); - } - }) - .catch((error) => { - console.log(error); - alert.error('Gas calculation error'); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps }; - useEffect(() => { - setIsLoading(isOpened); - }, [isOpened]); - return (
- {mark && } + {mark && } ))} {winnerRow && ( diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/game-info-player-mark.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/game-info-player-mark.tsx index 29625ec17..b2e72013e 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/game-info-player-mark.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-info-player-mark/game-info-player-mark.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import styles from './game-info-player-mark.module.scss'; import { GameMark } from '../game-mark'; -import type { Mark } from '../../types'; +import { Mark } from '@/app/utils'; import { variantsPlayerMark } from '../../variants'; import { BaseComponentProps } from '@/app/types'; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-mark/game-mark.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-mark/game-mark.tsx index aada098c5..4415a30a7 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-mark/game-mark.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-mark/game-mark.tsx @@ -1,4 +1,4 @@ -import { Mark } from '../../types'; +import { Mark } from '@/app/utils'; import { PlayerIconCircle, PlayerIconCross } from '../../assets'; import { BaseComponentProps } from '@/app/types'; @@ -7,5 +7,5 @@ type PlayerMarkProps = BaseComponentProps & { }; export function GameMark({ mark, className }: PlayerMarkProps) { - return mark === Mark.X ? : ; + return mark === 'X' ? : ; } diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-skip-button/game-skip-button.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-skip-button/game-skip-button.tsx index e19f9359f..78a0da7ce 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-skip-button/game-skip-button.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-skip-button/game-skip-button.tsx @@ -1,83 +1,34 @@ import { Button } from '@/components/ui/button'; -import { useGameMessage, useSubscriptionOnGameMessage } from '../../hooks'; -import { useEffect, useState } from 'react'; -import { useAccount, useAlert, useHandleCalculateGas } from '@gear-js/react-hooks'; -import { withoutCommas } from '@/app/utils'; -import { ProgramMetadata } from '@gear-js/api'; -import { useEzTransactions } from '@dapps-frontend/ez-transactions'; -import { useCheckBalance, useDnsProgramIds } from '@dapps-frontend/hooks'; +import { useState } from 'react'; +import { useAccount, useAlert } from '@gear-js/react-hooks'; +import { useEventMoveMadeSubscription, useEventGameFinishedSubscription, useSkipMessage } from '../../sails'; -type Props = { - meta: ProgramMetadata; -}; - -export function GameSkipButton({ meta }: Props) { - const { programId } = useDnsProgramIds(); - const calculateGas = useHandleCalculateGas(programId, meta); - const message = useGameMessage(meta); +export function GameSkipButton() { + const { skipMessage } = useSkipMessage(); const alert = useAlert(); const { account } = useAccount(); - - const { signless, gasless } = useEzTransactions(); - - const { checkBalance } = useCheckBalance({ - signlessPairVoucherId: signless.voucher?.id, - gaslessVoucherId: gasless.voucherId, - }); - const [isLoading, setIsLoading] = useState(false); - const { subscribe, unsubscribe, isOpened } = useSubscriptionOnGameMessage(meta); - - useEffect(() => { - setIsLoading(isOpened); - }, [isOpened]); - - const onError = () => { - setIsLoading(false); - unsubscribe(); - }; - const onSuccess = () => { - setIsLoading(false); - }; + useEventMoveMadeSubscription(); + useEventGameFinishedSubscription(); - const onNextTurn = async () => { - if (!meta || !account || !programId) { + const onSkip = async () => { + if (!account) { return; } - const payload = { Skip: {} }; setIsLoading(true); - - let voucherId = gasless.voucherId; - if (account && gasless.isEnabled && !gasless.voucherId && !signless.isActive) { - voucherId = await gasless.requestVoucher(account.address); + try { + await skipMessage(); + } catch (error) { + console.log(error); + alert.error((error instanceof Error && error.message) || 'Game skip error'); + setIsLoading(false); } - - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - - subscribe(); - - const sendMessage = () => message({ payload, gasLimit, voucherId, onError, onSuccess }); - if (voucherId) { - sendMessage(); - } else { - checkBalance(gasLimit, sendMessage, onError); - } - }) - .catch((error) => { - onError(); - console.log(error); - alert.error('Gas calculation error'); - }); }; return ( - ); diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-start-button/game-start-button.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-start-button/game-start-button.tsx index 6691377c2..ce65b8400 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-start-button/game-start-button.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game-start-button/game-start-button.tsx @@ -1,83 +1,39 @@ import { Button } from '@/components/ui/button'; -import { useGameMessage, useSubscriptionOnGameMessage } from '../../hooks'; -import { useEffect } from 'react'; import { BaseComponentProps } from '@/app/types'; -import { useCheckBalance, useDnsProgramIds } from '@dapps-frontend/hooks'; -import { useAccount, useAlert, useHandleCalculateGas } from '@gear-js/react-hooks'; -import { withoutCommas } from '@/app/utils'; -import { ProgramMetadata } from '@gear-js/api'; -import { useGaslessTransactions, useSignlessTransactions } from '@dapps-frontend/ez-transactions'; +import { useAccount, useAlert } from '@gear-js/react-hooks'; +import { useGaslessTransactions } from '@dapps-frontend/ez-transactions'; import { useAtom } from 'jotai'; import { stateChangeLoadingAtom } from '../../store'; +import { useStartGameMessage, useEventGameStartedSubscription } from '../../sails'; -type GameStartButtonProps = BaseComponentProps & { - meta: ProgramMetadata; -}; +type GameStartButtonProps = BaseComponentProps; -export function GameStartButton({ children, meta }: GameStartButtonProps) { - const { programId } = useDnsProgramIds(); - const message = useGameMessage(meta); +export function GameStartButton({ children }: GameStartButtonProps) { + const { startGameMessage } = useStartGameMessage(); const { account } = useAccount(); const alert = useAlert(); - - const signless = useSignlessTransactions(); const gasless = useGaslessTransactions(); - const calculateGas = useHandleCalculateGas(programId, meta); - - const { checkBalance } = useCheckBalance({ - signlessPairVoucherId: signless.voucher?.id, - gaslessVoucherId: gasless.voucherId, - }); - const [isLoading, setIsLoading] = useAtom(stateChangeLoadingAtom); - const { subscribe, unsubscribe, isOpened } = useSubscriptionOnGameMessage(meta); - useEffect(() => { - setIsLoading(isOpened); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpened]); - - const onError = () => { - setIsLoading(false); - unsubscribe(); - }; + useEventGameStartedSubscription(); const onGameStart = async () => { - if (!meta || !account || !programId) { + if (!account) { return; } - const payload = { StartGame: {} }; - setIsLoading(true); - let voucherId = gasless.voucherId; - if (account && gasless.isEnabled && !gasless.voucherId && !signless.isActive) { - voucherId = await gasless.requestVoucher(account.address); + setIsLoading(true); + try { + await startGameMessage(); + } catch (error) { + console.log(error); + alert.error((error instanceof Error && error.message) || 'Game start error'); + setIsLoading(false); } - - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - - subscribe(); - - const sendMessage = () => message({ payload, gasLimit, voucherId, onError }); - if (voucherId) { - sendMessage(); - } else { - checkBalance(gasLimit, sendMessage, onError); - } - }) - .catch((error) => { - onError(); - console.log(error); - alert.error('Gas calculation error'); - }); }; return ( - ); diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game/game.tsx b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game/game.tsx index adaf036ba..b0afe5131 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game/game.tsx +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/components/game/game.tsx @@ -2,7 +2,6 @@ import { HelpDescription } from '../ui/typography'; import styles from './game.module.scss'; import { GameField } from '../game-field'; import { GameInfoPlayerMark } from '../game-info-player-mark'; -import type { IGameInstance } from '../../types'; import { GameCountdown } from '../game-countdown'; import { GameSkipButton } from '../game-skip-button'; import { GameStartButton } from '../game-start-button'; @@ -10,28 +9,27 @@ import { Heading } from '@/components/ui/heading'; import { TextGradient } from '@/components/ui/text-gradient'; import { useGame } from '@/features/tic-tac-toe/hooks'; import { BaseComponentProps } from '@/app/types'; -import { ProgramMetadata } from '@gear-js/api'; import { EzTransactionsSwitch } from '@dapps-frontend/ez-transactions'; import { SIGNLESS_ALLOWED_ACTIONS } from '@/app/consts'; +import { GameInstance } from '@/app/utils'; type GameProps = BaseComponentProps & { - game: IGameInstance; - meta: ProgramMetadata; + game: GameInstance; }; -export function Game({ game, meta }: GameProps) { - const { gameResult, playerMark } = game; +export function Game({ game }: GameProps) { + const { game_result, player_mark } = game; const { countdown } = useGame(); return (
<> - {!!gameResult ? ( + {!!game_result ? ( <> - {gameResult === 'Player' && You win} - {gameResult === 'Bot' && You lose} - {gameResult === 'Draw' && "It's a draw"} + {game_result === 'Player' && You win} + {game_result === 'Bot' && You lose} + {game_result === 'Draw' && "It's a draw"} ) : ( Tic Tac Toe game @@ -39,15 +37,15 @@ export function Game({ game, meta }: GameProps) { - {!!gameResult ? ( + {!!game_result ? ( <> - {gameResult === 'Player' && ( + {game_result === 'Player' && (

Congratulations, the game is over, you won!
Good job.

)} - {gameResult === 'Bot' &&

Try again to win.

} - {gameResult === 'Draw' && ( + {game_result === 'Bot' &&

Try again to win.

} + {game_result === 'Draw' && (

The game is over, it's a draw!
Try again to win. @@ -62,26 +60,26 @@ export function Game({ game, meta }: GameProps) {

- {Boolean(!gameResult) ? ( + {Boolean(!game_result) ? ( <> {countdown?.isActive ? ( ) : ( - + )} ) : (
- Play again + Play again
)}
- + - +
); diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/hooks.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/hooks.ts index 85c065089..226290085 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/hooks.ts +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/hooks.ts @@ -1,35 +1,29 @@ -import { useAccount, useApi } from '@gear-js/react-hooks'; -import { useEffect, useMemo } from 'react'; +import { useAccount } from '@gear-js/react-hooks'; +import { useEffect } from 'react'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; -import isEqual from 'lodash.isequal'; -import { useDnsProgramIds } from '@dapps-frontend/hooks'; -import { useSignlessSendMessage } from '@dapps-frontend/ez-transactions'; -import { IDecodedReplyGame, IGameInstance, IQueryResponseConfig, IQueryResponseGame } from './types'; -import { configAtom, countdownAtom, gameAtom, pendingAtom, stateChangeLoadingAtom } from './store'; -import { useOnceReadState } from '@/app/hooks/use-once-read-state'; -import { useWatchMessages } from '@/app/hooks/use-watch-messages'; -import { toNumber } from '@/app/utils'; -import { ProgramMetadata } from '@gear-js/api'; +import { countdownAtom, gameAtom, pendingAtom } from './store'; +import { useConfigQuery, useGameQuery } from '@/features/tic-tac-toe/sails'; +import { GameInstance } from '@/app/utils'; export function useGame() { const setGameState = useSetAtom(gameAtom); const gameState = useAtomValue(gameAtom); - const setConfigState = useSetAtom(configAtom); - const configState = useAtomValue(configAtom); + const { config } = useConfigQuery(); const setCountdown = useSetAtom(countdownAtom); const countdown = useAtomValue(countdownAtom); - const updateCountdown = (game: IGameInstance) => { + const updateCountdown = (game: GameInstance) => { setCountdown((prev) => { - const timeLeft = toNumber(game.lastTime) + toNumber(configState?.turnDeadlineMs || '0'); + const lastTime = Number(game.last_time); + const timeLeft = lastTime + Number(config?.turn_deadline_ms || '0'); const isPassed = Date.now() - timeLeft > 0; - const isNew = prev?.value !== game.lastTime; + const isNew = prev?.value !== lastTime; - return isNew ? { value: game.lastTime, isActive: isNew && !isPassed } : prev; + return isNew ? { value: lastTime, isActive: isNew && !isPassed } : prev; }); }; - const updateGame = (game: IGameInstance) => { + const updateGame = (game: GameInstance) => { setGameState(game); updateCountdown(game); }; @@ -50,53 +44,12 @@ export function useGame() { gameState, setCountdown, countdown, - setConfigState, - configState, updateCountdown, updateGame, clearGame, }; } -export function useOnceGameState(metadata?: ProgramMetadata) { - const { account } = useAccount(); - const { programId } = useDnsProgramIds(); - - const payloadGame = useMemo( - () => (account?.decodedAddress ? { Game: { player_id: account.decodedAddress } } : undefined), - [account?.decodedAddress], - ); - const payloadConfig = useMemo(() => ({ Config: null }), []); - - const { - state: stateConfig, - error: configError, - handleReadState: triggerConfig, - } = useOnceReadState({ - programId, - payload: payloadConfig, - meta: metadata, - }); - - const { - state: stateGame, - error: gameError, - handleReadState: triggerGame, - } = useOnceReadState({ - programId, - payload: payloadGame, - meta: metadata, - }); - - return { - stateGame, - stateConfig, - error: gameError || configError, - triggerGame, - triggerConfig, - }; -} - export const useInitGame = () => { const { account } = useAccount(); const { gameState } = useGame(); @@ -105,80 +58,22 @@ export const useInitGame = () => { isGameReady: account?.decodedAddress ? gameState !== undefined : true, }; }; -export const useInitGameSync = (metadata?: ProgramMetadata) => { - const { isApiReady, api } = useApi(); - const { account } = useAccount(); - const { stateGame, stateConfig, error, triggerGame, triggerConfig } = useOnceGameState(metadata); - const { updateGame, resetGame, setConfigState } = useGame(); - - useEffect(() => { - if (!isApiReady || !api || !metadata || stateConfig?.Config) return; - - triggerConfig(metadata); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isApiReady, api, metadata, account?.decodedAddress]); - - useEffect(() => { - if (!isApiReady || !api || !metadata || !stateConfig?.Config) return; - - triggerGame(metadata); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isApiReady, api, metadata, stateConfig?.Config, account?.decodedAddress]); +export const useInitGameSync = () => { + const { updateGame, resetGame } = useGame(); + const { game, error } = useGameQuery(); useEffect(() => { - if (!stateConfig?.Config) return; - - setConfigState(stateConfig.Config); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stateConfig?.Config]); - - useEffect(() => { - if (stateGame === undefined) return; - - const game = stateGame?.Game; - + if (game === undefined) return; game ? updateGame(game) : resetGame(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stateGame]); + }, [game]); return { errorGame: error, }; }; -export function useGameMessage(meta: ProgramMetadata) { - const { programId } = useDnsProgramIds(); - return useSignlessSendMessage(programId, meta, { disableAlerts: true }); -} - export function usePending() { const [pending, setPending] = useAtom(pendingAtom); return { pending, setPending }; } - -export function useSubscriptionOnGameMessage(meta: ProgramMetadata) { - const { gameState, updateGame } = useGame(); - const { subscribe, unsubscribe, reply, isOpened } = useWatchMessages(meta); - const setIsLoading = useSetAtom(stateChangeLoadingAtom); - - useEffect(() => { - if (!isOpened) return; - const game = reply?.MoveMade?.game || reply?.GameStarted?.game || reply?.GameFinished?.game; - - if (game && !isEqual(game.board, gameState?.board)) { - updateGame(game); - unsubscribe(); - setIsLoading(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reply, isOpened]); - - return { - subscribe, - unsubscribe, - reply, - isOpened, - }; -} diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/index.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/index.ts new file mode 100644 index 000000000..8e207cb6f --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/index.ts @@ -0,0 +1,3 @@ +export { useEventGameStartedSubscription } from './use-event-game-start-subscription'; +export { useEventMoveMadeSubscription } from './use-event-move-made-subscription'; +export { useEventGameFinishedSubscription } from './use-event-game-finished-subscription'; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-finished-subscription.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-finished-subscription.ts new file mode 100644 index 000000000..b2209d922 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-finished-subscription.ts @@ -0,0 +1,25 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { useSetAtom } from 'jotai'; +import { GameInstance, useProgram } from '@/app/utils'; +import { stateChangeLoadingAtom } from '../../store'; +import { useGame } from '../../hooks'; + +export type GameFinishedEvent = { game: GameInstance }; + +export function useEventGameFinishedSubscription() { + const program = useProgram(); + const { updateGame } = useGame(); + const setIsLoading = useSetAtom(stateChangeLoadingAtom); + + const onData = ({ game }: GameFinishedEvent) => { + updateGame(game); + setIsLoading(false); + }; + + useProgramEvent({ + program, + serviceName: 'ticTacToe', + functionName: 'subscribeToGameFinishedEvent', + onData, + }); +} diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-start-subscription.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-start-subscription.ts new file mode 100644 index 000000000..a29234b22 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-game-start-subscription.ts @@ -0,0 +1,25 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { useSetAtom } from 'jotai'; +import { GameInstance, useProgram } from '@/app/utils'; +import { stateChangeLoadingAtom } from '../../store'; +import { useGame } from '../../hooks'; + +export type GameStartedEvent = { game: GameInstance }; + +export function useEventGameStartedSubscription() { + const program = useProgram(); + const { updateGame } = useGame(); + const setIsLoading = useSetAtom(stateChangeLoadingAtom); + + const onData = ({ game }: GameStartedEvent) => { + updateGame(game); + setIsLoading(false); + }; + + useProgramEvent({ + program, + serviceName: 'ticTacToe', + functionName: 'subscribeToGameStartedEvent', + onData, + }); +} diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-move-made-subscription.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-move-made-subscription.ts new file mode 100644 index 000000000..9f71306d0 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/events/use-event-move-made-subscription.ts @@ -0,0 +1,25 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { useSetAtom } from 'jotai'; +import { GameInstance, useProgram } from '@/app/utils'; +import { stateChangeLoadingAtom } from '../../store'; +import { useGame } from '../../hooks'; + +export type MoveMadeEvent = { game: GameInstance }; + +export function useEventMoveMadeSubscription() { + const program = useProgram(); + const { updateGame } = useGame(); + const setIsLoading = useSetAtom(stateChangeLoadingAtom); + + const onData = ({ game }: MoveMadeEvent) => { + updateGame(game); + setIsLoading(false); + }; + + useProgramEvent({ + program, + serviceName: 'ticTacToe', + functionName: 'subscribeToMoveMadeEvent', + onData, + }); +} diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/index.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/index.ts new file mode 100644 index 000000000..35fc718a8 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/index.ts @@ -0,0 +1,3 @@ +export * from './events'; +export * from './messages'; +export * from './queries'; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/index.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/index.ts new file mode 100644 index 000000000..23e7ac36b --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/index.ts @@ -0,0 +1,3 @@ +export { useStartGameMessage } from './use-start-game-message'; +export { useSkipMessage } from './use-skip-message'; +export { useTurnMessage } from './use-turn-message'; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-skip-message.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-skip-message.ts new file mode 100644 index 000000000..d040be07a --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-skip-message.ts @@ -0,0 +1,25 @@ +import { useSendProgramTransaction } from '@gear-js/react-hooks'; +import { usePrepareEzTransactionParams } from '@dapps-frontend/ez-transactions'; +import { useProgram } from '@/app/utils'; + +export const useSkipMessage = () => { + const program = useProgram(); + const { sendTransactionAsync } = useSendProgramTransaction({ + program, + serviceName: 'ticTacToe', + functionName: 'skip', + }); + const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); + + const skipMessage = async () => { + const { sessionForAccount, ...params } = await prepareEzTransactionParams(); + const { result } = await sendTransactionAsync({ + args: [sessionForAccount], + ...params, + }); + await result.response(); + return; + }; + + return { skipMessage }; +}; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-start-game-message.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-start-game-message.ts new file mode 100644 index 000000000..d562112ff --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-start-game-message.ts @@ -0,0 +1,25 @@ +import { useSendProgramTransaction } from '@gear-js/react-hooks'; +import { usePrepareEzTransactionParams } from '@dapps-frontend/ez-transactions'; +import { useProgram } from '@/app/utils'; + +export const useStartGameMessage = () => { + const program = useProgram(); + const { sendTransactionAsync } = useSendProgramTransaction({ + program, + serviceName: 'ticTacToe', + functionName: 'startGame', + }); + const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); + + const startGameMessage = async () => { + const { sessionForAccount, ...params } = await prepareEzTransactionParams(); + const { result } = await sendTransactionAsync({ + args: [sessionForAccount], + ...params, + }); + await result.response(); + return; + }; + + return { startGameMessage }; +}; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-turn-message.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-turn-message.ts new file mode 100644 index 000000000..ba1c43065 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/messages/use-turn-message.ts @@ -0,0 +1,25 @@ +import { useSendProgramTransaction } from '@gear-js/react-hooks'; +import { usePrepareEzTransactionParams } from '@dapps-frontend/ez-transactions'; +import { useProgram } from '@/app/utils'; + +export const useTurnMessage = () => { + const program = useProgram(); + const { sendTransactionAsync } = useSendProgramTransaction({ + program, + serviceName: 'ticTacToe', + functionName: 'turn', + }); + const { prepareEzTransactionParams } = usePrepareEzTransactionParams(); + + const turnMessage = async (step: number) => { + const { sessionForAccount, ...params } = await prepareEzTransactionParams(); + const { result } = await sendTransactionAsync({ + args: [step, sessionForAccount], + ...params, + }); + await result.response(); + return; + }; + + return { turnMessage }; +}; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/index.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/index.ts new file mode 100644 index 000000000..b5f668485 --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/index.ts @@ -0,0 +1,2 @@ +export { useConfigQuery } from './use-config-query'; +export { useGameQuery } from './use-game-query'; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-config-query.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-config-query.ts new file mode 100644 index 000000000..9cab1d77f --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-config-query.ts @@ -0,0 +1,15 @@ +import { useProgramQuery } from '@gear-js/react-hooks'; +import { useProgram } from '@/app/utils'; + +export const useConfigQuery = () => { + const program = useProgram(); + + const { data, refetch, isFetching, error } = useProgramQuery({ + program, + serviceName: 'ticTacToe', + functionName: 'config', + args: [], + }); + + return { config: data, isFetching, refetch, error }; +}; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-game-query.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-game-query.ts new file mode 100644 index 000000000..02a0ce03a --- /dev/null +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/sails/queries/use-game-query.ts @@ -0,0 +1,17 @@ +import { useProgram } from '@/app/utils'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; + +export const useGameQuery = () => { + const program = useProgram(); + const { account } = useAccount(); + + const { data, refetch, isFetching, error } = useProgramQuery({ + program, + serviceName: 'ticTacToe', + functionName: 'game', + args: [account?.decodedAddress!], + query: { enabled: account ? undefined : false }, + }); + + return { game: data, isFetching, refetch, error }; +}; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/store.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/store.ts index 8572527d6..61a28db7d 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/store.ts +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/store.ts @@ -1,8 +1,8 @@ import { atom } from 'jotai'; -import { IGameConfig, IGameCountdown, IGameInstance } from './types'; +import { IGameCountdown } from './types'; +import { GameInstance } from '@/app/utils'; -export const gameAtom = atom(undefined); -export const configAtom = atom(null); +export const gameAtom = atom(undefined); export const pendingAtom = atom(false); export const countdownAtom = atom(undefined); export const stateChangeLoadingAtom = atom(false); diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/types.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/types.ts index f819075ef..0ee996fd4 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/types.ts +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/types.ts @@ -1,43 +1,5 @@ -export type IGameInstance = { - board: Cell[]; - botMark: Mark; - playerMark: Mark; - lastTime: string; - gameOver: boolean; - gameResult: null | IGameResultStatus; -}; - -export type IGameConfig = { - addAttributeGas: string; // "40,000,000,000", - msPerBlock: string; // "3", - tokensForOwnerGas: string; // "40,000,000,000", - gasToRemoveGame: string; // "5,000,000,000", - timeInterval: string; // "20", - turnDeadlineMs: string; // 120,000 in ms -}; - -export type IGameResultStatus = 'Player' | 'Bot' | 'Draw'; - -export enum Mark { - X = 'X', - O = 'O', -} +import { Mark } from '@/app/utils'; export type Cell = Mark | null; -export type IQueryResponseGame = { Game: IGameInstance | null }; -export type IQueryResponseConfig = { Config: IGameConfig | null }; - -export type IDecodedReplyGame = { - GameStarted?: { - game?: IGameInstance; - }; - MoveMade?: { - game?: IGameInstance; - }; - GameFinished?: { - game?: IGameInstance; - }; -}; - -export type IGameCountdown = { isActive: boolean; value: string } | undefined; +export type IGameCountdown = { isActive: boolean; value: number } | undefined; diff --git a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/utils.ts b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/utils.ts index 894c925eb..097ef5139 100644 --- a/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/utils.ts +++ b/frontend/apps/tic-tac-toe/src/features/tic-tac-toe/utils.ts @@ -1,5 +1,4 @@ import { Cell } from './types'; -import { toNumber } from '@/app/utils'; export function calculateWinner(squares: Cell[]) { const lines: [number[], string][] = [ @@ -19,7 +18,3 @@ export function calculateWinner(squares: Cell[]) { } } } - -export function calculateWinRate(wins: string, games: string) { - return Math.floor((toNumber(wins) / toNumber(games) || 0) * 10000) / 100; -} diff --git a/frontend/apps/tic-tac-toe/src/pages/home.tsx b/frontend/apps/tic-tac-toe/src/pages/home.tsx index 673bc4cc0..3295c4936 100644 --- a/frontend/apps/tic-tac-toe/src/pages/home.tsx +++ b/frontend/apps/tic-tac-toe/src/pages/home.tsx @@ -4,26 +4,25 @@ import { useGame } from '@/features/tic-tac-toe/hooks'; import { Game, Welcome } from '@/features/tic-tac-toe'; import { WalletNew as Wallet } from '@dapps-frontend/ui'; import { GameStartButton } from '@/features/tic-tac-toe/components/game-start-button'; -import metaTxt from '@/features/tic-tac-toe/assets/meta/tic_tac_toe.meta.txt'; -import { useProgramMetadata } from '@/app/hooks'; import { Loader } from '@/components'; import { SIGNLESS_ALLOWED_ACTIONS } from '@/app/consts'; +import { useProgram } from '@/app/utils'; export default function Home() { const { account } = useAccount(); const { gameState } = useGame(); - const meta = useProgramMetadata(metaTxt); + const program = useProgram(); - return meta ? ( + return program ? ( <> {gameState ? ( - + ) : ( {!account && } {!!account && ( <> - Start the game + Start the game )} diff --git a/frontend/dev/gstd-tic-tac-toe/.env.example b/frontend/dev/gstd-tic-tac-toe/.env.example new file mode 100644 index 000000000..057d8caee --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/.env.example @@ -0,0 +1,12 @@ +VITE_NODE_ADDRESS= +VITE_GASLESS_BACKEND_ADDRESS= +VITE_DNS_API_URL= +VITE_DNS_NAME= + +# optional, specify sentry dsn and targetted domain for error tracking +# if domain is not specified, localhost is used by default +VITE_SENTRY_DSN= +VITE_SENTRY_TARGET= + +#GTM +VITE_GTM_ID_TTT= diff --git a/frontend/dev/gstd-tic-tac-toe/.eslintignore b/frontend/dev/gstd-tic-tac-toe/.eslintignore new file mode 100644 index 000000000..f4f9f437c --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/.eslintignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.idea diff --git a/frontend/dev/gstd-tic-tac-toe/.eslintrc b/frontend/dev/gstd-tic-tac-toe/.eslintrc new file mode 100644 index 000000000..fcf5ef32e --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["react-app"] +} diff --git a/frontend/dev/gstd-tic-tac-toe/Dockerfile b/frontend/dev/gstd-tic-tac-toe/Dockerfile new file mode 100644 index 000000000..6f873fccf --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/Dockerfile @@ -0,0 +1,38 @@ +FROM node:18-alpine +MAINTAINER gear + +WORKDIR /frontend + +COPY /frontend/package.json . +COPY /frontend/yarn.lock . +COPY /frontend/.yarnrc.yml . +COPY /frontend/.yarn/releases .yarn/releases + +COPY ./frontend/apps/tic-tac-toe ./apps/tic-tac-toe +COPY ./frontend/packages ./packages + +RUN apk update + +RUN apk add xsel + +ARG VITE_DNS_API_URL \ + VITE_DNS_NAME \ + VITE_NODE_ADDRESS \ + VITE_GASLESS_BACKEND_ADDRESS \ + VITE_SENTRY_DSN +ENV VITE_DNS_API_URL=${VITE_DNS_API_URL} \ + VITE_DNS_NAME=${VITE_DNS_NAME} \ + VITE_NODE_ADDRESS=${VITE_NODE_ADDRESS} \ + VITE_GASLESS_BACKEND_ADDRESS=${VITE_GASLESS_BACKEND_ADDRESS} \ + VITE_SENTRY_DSN=${VITE_SENTRY_DSN} \ + DISABLE_ESLINT_PLUGIN=true + +WORKDIR /frontend/apps/tic-tac-toe + +RUN yarn install + +RUN yarn build + +RUN npm install --global serve + +CMD ["serve", "-s", "/frontend/apps/tic-tac-toe/build"] diff --git a/frontend/dev/gstd-tic-tac-toe/README.md b/frontend/dev/gstd-tic-tac-toe/README.md new file mode 100644 index 000000000..1691a0bbc --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/README.md @@ -0,0 +1,40 @@ +

+ + GEAR + +

+

+ +

+
+ +## Description + +React application of [Tic-Tac-Toe](https://wiki.gear-tech.io/docs/examples/Gaming/tictactoe) based on [Rust smart-contract](https://github.com/gear-foundation/dapps/tree/master/contracts/tic-tac-toe). + +This is a frontend implementation for programs created using the [Gear library](https://wiki.vara.network/docs/build/gstd/) library. For programs built with [Sails Library](https://wiki.vara.network/docs/build/sails/), refer to the implementation [here](https://github.com/gear-foundation/dapps/tree/master/frontend/apps/tic-tac-toe). + +## Getting started + +### Install packages: + +```sh +yarn install +``` + +### Declare environment variables: + +Create `.env` file, `.env.example` will let you know what variables are expected. + +In order for all features to work as expected, the node and it's runtime version should be chosen based on the +current `@gear-js/api` version. + +In case of issues with the application, try to switch to another network or run your own local node and specify its +address in the `.env` file. When applicable, make sure the smart contract(s) wasm files are uploaded and running in this +network accordingly. + +### Run the app: + +```sh +yarn start +``` diff --git a/frontend/dev/gstd-tic-tac-toe/index.html b/frontend/dev/gstd-tic-tac-toe/index.html new file mode 100644 index 000000000..a6e6e4cb5 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/index.html @@ -0,0 +1,24 @@ + + + + + Vara: Tic-Tac-Toe + + + + + + + + + + + + + + + +
+ + + diff --git a/frontend/dev/gstd-tic-tac-toe/package.json b/frontend/dev/gstd-tic-tac-toe/package.json new file mode 100644 index 000000000..2ca14ec7a --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/package.json @@ -0,0 +1,77 @@ +{ + "name": "tic-tac-toe", + "private": true, + "version": "1.0.2", + "type": "module", + "scripts": { + "start": "yarn build:packages && yarn build:ez-transactions && vite --open", + "build": "yarn build:packages && yarn build:ez-transactions && tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@dapps-frontend/error-tracking": "workspace:*", + "@dapps-frontend/ez-transactions": "workspace:*", + "@dapps-frontend/hooks": "workspace:*", + "@dapps-frontend/ui": "workspace:*", + "@gear-js/api": "0.38.1", + "@gear-js/react-hooks": "0.12.1", + "@headlessui/react": "1.7.17", + "@polkadot/api": "11.0.2", + "@polkadot/types": "11.0.2", + "@polkadot/util": "12.3.2", + "@polkadot/util-crypto": "12.6.2", + "@radix-ui/react-dialog": "1.0.4", + "@radix-ui/react-scroll-area": "1.0.4", + "@tanstack/react-query": "5.29.0", + "@types/node": "18.16.19", + "@types/react": "18.2.33", + "@types/react-dom": "18.2.14", + "assert": "2.0.0", + "buffer": "6.0.3", + "class-variance-authority": "0.6.1", + "clsx": "1.2.1", + "framer-motion": "10.16.2", + "jotai": "2.2.1", + "lodash.isequal": "^4.5.0", + "react": "18.2.0", + "react-countdown": "2.3.5", + "react-dom": "18.2.0", + "react-router-dom": "6.10.0", + "react-transition-group": "4.4.5", + "sails-js": "0.1.8", + "sass": "1.62.0" + }, + "devDependencies": { + "@types/lodash.isequal": "4.5.6", + "@vitejs/plugin-react-swc": "3.3.2", + "autoprefixer": "10.4.15", + "eslint": "8.48.0", + "eslint-config-react-app": "7.0.1", + "postcss": "8.4.29", + "prettier": "3.0.3", + "rollup-plugin-visualizer": "5.9.2", + "tailwindcss": "3.3.3", + "typescript": "4.9.5", + "vite": "4.4.9", + "vite-plugin-eslint": "1.8.1", + "vite-plugin-node-polyfills": "0.17.0", + "vite-plugin-svgr": "3.2.0" + }, + "browserslist": { + "production": [ + "chrome >= 67", + "edge >= 79", + "firefox >= 68", + "opera >= 54", + "safari >= 14" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "lint-staged": { + "./**/*.{js,css,ts,tsx}": "eslint --fix" + } +} diff --git a/frontend/dev/gstd-tic-tac-toe/postcss.config.js b/frontend/dev/gstd-tic-tac-toe/postcss.config.js new file mode 100644 index 000000000..2aa7205d4 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/android-chrome-192x192.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..35aae1e5a5c09831e62fce6ca451e8a4076ce9a3 GIT binary patch literal 2710 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 L>FaTlyE6YD=gSQ) literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/android-chrome-512x512.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..51dc2932ccf932ef91a37838fb595df81a44eaf0 GIT binary patch literal 10925 zcmeHNc|6o@yT4~-XlkSI3e2T>GlT+V2ws^`#=xmT){(kq0Gp|a*tW$Mj zgZKB0t>|GbF*19zJy(^mD>wR)^UFJn$2Lfx|C=gkQ)n0-FS+d=-xb&W!@pQNq2c4? zt$>8hdEIe$`rQ5QBzT30wQ}?jDvLPM4MI?~Fg3{Rg&%G~pa$+PIrvpR34XN8Vi&(m zg&&$aI9Rd#|6l#zHDK)7jlPsvDNj6kL)Ts8xheF%c@}%FAVHt+S^~f5u{=|MUJ@%C zQ`^o)j*X}6t0wVC&K3fujG?+ zOkcGs>~yP`K9l$%s8=}4)lzV;s3e{Ly(P``$#ZjbStG(X-Y$N{^^0@#uPNlhvHu+5 ze30dcZls4P^%X{#bbs)l9FR;rD2G_&Cf!EY&E0Z`Jz|xL$F6u@)zmmnsqS=!)e9q8 zyE-fd$TkR1`CKy3npRgaoMe*`T%!=(+c!JR!_gqM<)m2oS(IDY;Hw{EWwhun3gfWen@hzFE{>FF&5)(=m@7Fn{s8{m0%{fd#guuw09r;_(cI z%w)ua3%g?Vi)+CSFHacobmtrZ1)qGBZJU|Ev5lQ;D1qBeMZd21D)sc)lizus8x&s_ zEB{lM8R*e7AMR^QmVUAm_e#Jk@c8(5bGLhz%ZoYrqPF|>V%9$%FF(L?dl|{Z6*rdX zhs7I8B%HA(XO+=)i=qhlSBr>cjUHMD=p9VZ`1OJvzGqWGS+#*wRnbIqlNK>3*QlsF**B;8yse?c$(J8Y#y+N-UQiF=P=a;k zxI;M~#lpHu_~d(YcfJF9Q$dquT-}S-e0(;K?|$nss3=NOXs3#)C_SbmC2(oQ0Rb{* z$Z_Aq)bDQW^%5(Gnqxw*^!s(=k6Ef-)A}E-=6Cl#V&f7SR21=v&`7t;bgy zxo;qyog{Id55ei4Myk6-Wp9P2Kmg0Vi1_*3Jl>BuLy3%oR284MbltdP_2CQVL~f!! zVuu#piZqA^t8sL3R9-Y_wMTCwsmduK-p@!P!+|W#n=6?ZyeUb!?a#0v~ z#sXduzBZKj!Fk6hc$ff=eaLbZsX$DZI`o*&?KK8$UiThaI8?m%VR{LhB~IQ-|Jy2QTtU z9{f5B)M0koIWdw$$s^!X;f%(rR+D6vbW^2_pN>rt}zR2!rNs+1&!*OOP(vY_MXELZ*VN!$1phs?J|6CNH=wD#r?c1FVX z<3veHc5cOJG~uRA6-`ZHkEm=jQ{=%OkvB#+ot+zbbgs@QcQ=7SI{z#z{@lAaGAMA5}cc1qPA<5yyfocrr7RW zE)}V3KH%4VSp=IH7O{LUZ2egI`2_jh;dLTLnXDvbLDrc5R*h^(e?pVgiA5=azrJN< z(f(NhuWR+wz;>6Y=+V96!@Yy%CM^&Q+ehQJeq4J>*BSJC#~dQzx-mK;JZsJg4zrTz z^9@~C{;3>-9LaIt&CX}g&$?p57sYUFw_CZcw{EC~Y?)D{LOMd0zMzK-GQtvL&@E2; z@LG3)c1qx9XU}^cZ&_I0e@2s)zJZrbK0zMwQLi$n)STF7Z#f4}m9YaO;`X8=o{p8x z)q%?lOLRv!ipoya-&a;y)@w%|d2bHBk6k?WeMK#;e>@~^a)2Y&A-{(ZTr@{ZrMje& zF4>Nkq&Px0XkKjdsHB6nn-1O;K|~@GRs+;$?g=;d~=Y zCH825NIqIxOyU0o&LIyVc##)UNFFI^lnY@^x2j+3YlR52ah2W*pyKv0d$D)MG910vabdpB;w#|A}YR+Z+MI9V7 z+0QHFk|$&;g6gH$wJkKaCxhhy>0u~(2afpwcVfa&!bjcEA8b+GgE^de+hMY_9QZ+c z6d|dKqrzhgAXnHmo&Xi%L5%1;?3D48)5991@y&3?K9IQ(WS-6rn9OZ@tto+a-kNgv zRuCBr4Es7eYP{J2zcidz+~|djdtG~*Vs()SG<8glyUuO+$Rc)t&(L;r`Y{{KGx9HS zD8`a<+_ZO2PMnd8VEFdINSpVx%Sxmv^d`rgs@MK;1G{Co*n_ z8^J|pvz?cFI+CR++C+x%Rq1itg3-cpq+&ycm8u_ys!f8O97P4Nxvixq@R1)(3 z()mJdV_$f$51`VxV=lq51aWiO?_)=%sHoM)SKYAUq;R#3#p)}fWm}oafM66pwMsLR zP?_l3VTkawhAsI{c&+wU2t)MCSG{Uit-i!6}n-%pP^Z`?WR&j-M;G=N+8*$f=b&2@%+GHRdl-a0XKm>ks* z-s?ZTKtM$!EB6RW4|wT&X>+C%Z(tR?m{$}x$!n(dfBX!EEW1L+f0@kZR%vKW%3OWu z6Dm`5uhF$CIqbg#cc5ay!A?{}O%RCM%4=Dst~uZ=5Z)WE;kn~l@sos_kVBS^us5me zm#^mF)o8IYmjZ&;Hg#rHGa=*3gOo!wkg{q-QN(z{z%gFHvO@vpU4AzJ;~JGzRg~W~ zl=v`15;=Q~OO_Tj}*!msM&HCcvPk75zH}1W+UCT-u8=dr!Bu)^_%N z=Ld18xo3D~Hk&Dr*PaN0(^s_Wh{=^><*9=MCj z=4*qA=K(ydry`30QWv=3z{%0Ej>{%}ouE)CKqIdgido!qcTwW0n@u)dVyuyAXjN<( zz4sflnt<}&#CoR!Xl|G(980hi1ceDW?cfhkbaXZuBV!-PJ3V?VZFSPRNCC$ds>dq= z5#4tj?%|)b#vtd22}fuIr#+|1GMB=W-p3pHVR3Pj1e?{{%4wMgVZEP|g#g{Zfal&{ zyC(Dt3A|k^fH&AP3}E|HXKPTjm;SJpLp=T-jfjYLKa6-wRcm*uBK@KS;-(!xJBTi*? zT9}gS(4Ksef~ai3r(p9r5DqT>$vcKH*{zsb0si-Wz{7P$9%02w*ifuK#%ig}2Es}OG^q}Auq*blGzI|<2kc|BlMzDKtc#mx1bftrf6R~&5 zEB)C4h7xPVEpLnGN;rb)>ZnNCE>92Ah)@5gHvac(;9#lWJ{YS`W7fH+REgAk%$Uu=9E$8BNtC{PtmL(9MA&IE$P9CRybK3#PkB`PW9)|pcl(&L`;}(y4LR1}*QmDw&zFn{f5=_R- z$dP=^h}XA_+_+|@S&&~RdQEeilmL*Qx_5;N`Q1T+!3=98O!fC%kn6eu;yw zV}~)4y#40b z$wBQ@6tpMOl%Hh@ZHL{bAvo^)Cek-ZR7A~PI7Zx;gUe5dV8OutDW#mnPk#gC_g)Yu z#&YO+>Y=5a560kuUfV;~$AeR0)o|%ceTB1*@p!i8NYB+edRl5-w>%hs-C1MH@yy@G z_a92HJFO&Q%-DfrG`~C@^0TQLZq%|0LeUB0{lR+RAzpqo;46dE1IhHyeiLg;n%VuV z5YNz0>ZG)H|B4P=7Qf?A9y{=QGNSlnO*91b?l!>rZ15=s!blY1p=CTY6))z9S;2?p z5O0^jl$Ui2>-@g{6580nIjg~y0{p2^MuI*Wqu^x#pqpHvFYuy#OSKwmh9BJ zF)g@zbFPl%U@+Xho*J@X4pe0`oFwYpSP+(78aAB}trQ?3id=4-3Ygj7{wGgU>>zFv zLc4@F*eQW){a+QY@vcACt#q>BlLx@&KzE*#O%^+F_FY7YCvOg4WdkMYddH#2mu}9s z`EODJty~;||9P;#!-WKP2JlBIrr%i?@6Lm)1|-V1pP`s7VhKO*xWwZZgCPohAQ|1( z@6iJ*bVNVYHcU)JiM$vLhmdNuo5uzArCFxsu7+tgjrFPp|6;M^@K?)IIRruLSAdL_{gPZ(bQcx#^(;Sf?%hlep}v zhx5w#Q$eu#8qhe{ zW#eY-hNqrOjU=>3<2bgu55K*k$@XfFR7;t)w{PPI_F#TeQNi~a#z-a5vbu;dvJ;0c zoeq|16k$n2paw~!K~JcY64-wpj{x#SU$bHfax-D?9XO=K8B^geF2=S2VKcQsriSK8 zKwp47sqT}HOk(XY@A7{%O-ue`wu!XvOM~L$8+$bS-8cGvo+Jl*f|HD?Lt=#1N{Wig zYU|TgRODmMtfzEuA3fOR5rO$f&vaIn3|>S;{m%ch|JTsx>F$aqtjMAwhR>x#l%?W2 zko^zOxPo1{DRh-F_$EqbHg>Q)nIh&r@1+JUQD}dbUH<(|?iG%xCzu$MPNURPuCqWz z*(m40GxJDO_c=j$Wj!1cS8O8joS|p1`ORIybqN zYFGX1;&oR6?I7FqiKviS`;mB9k{lb%>us#!F8*a1|q`eo-reloCoIxK?1?;^CX{_nT6{)oJ2 z!u|`>Xbj0cTYTA|=N1;t;Z~<=XSC-<5mxAqgyS!6NZu@BfUh1iM%rGof>)rSWh}=?Ym6NY6!71Bf%iiw@gNOTvvDW*ia)^&dKsM<1^AmpCsF`v86O3L zV|+(RN}%?51DzH3hczc+>^)^BX$iAo2;;GVIg{wX0^wVUFxLtovp)om(&xc)p3G_t z1~i@!`ULfbeXP3WUR!0Y@;dx?Db$PZ%oLLhV5d+i z1BXH(dYTiTW3*xj_~LE17!cZP#q=*uhV|8F}4pH%J;A6GbvZ_ z=)&8K8_*jhIP(J3NbAhOgOprII`Xhk+98kNvL9|Uuz!*YY$i=D0qC^3QbZ*CMjSg9 zO(+??Nwdubrv*?l0~RpTWg^zoUxRtg+tyFa!KkDMiYoSGX{)oN+`m(X+-M~EO4#KO zt&gjKHAi7ia_#b16v2N|p%b4H=yL-I{aTvrCMx1ybTs0HEXA^2`$nOL7wFBD<=Ls1 z5||0;C2erq9=h-~EwgWSXWtayYr#tr|8+Ats2^5F0DD^!#%T{Q2hhT-2;;lo%w}V~ zC3huwXG3emXf<%=K+;QJ$0G}8&!fGc)s`J}zwFUxgY|l0jc`x71R8~4T4+#s9IIE* zq%5>n_8Y*48cQ3^x`3NI3?8)T_@}R81~8OMTB13Wa}d^#j*6A?W(~LBb#iwlK6l-h zaeM7~08?t9wXoI>{oOrS74V;C41m)Q?}aLS5`1KpgQ#rOP@<*qs}ck-^npRu`-k}5 zqgYDM@p4z4xf65G9q^`}ASaj(Txv~`X+eiFShJCLx_94GI0J3p$4E=@h&+7)_##YV ztgWgNyf|diCWd3M#ruI5aapQdKTRh9AAejfkB|f!V^sZoxBR!*{M0;vMBwvEP==mA zY}g{4DTVoF*>O&%)$fp$gd}pr+)Sx@3FXA2R7hVFUJ-S^7X~Nvf5#*qCXt*>AVh{F z+#M+0SOZ=TtwTy6RhEc?7gQ)vJh#+?n`HF@eIT4JhDnT(892@ceqnaUO{`VWWRH61 zLw*3)!hxP?)9rQ?5S#-U0N{ltzYki29h6leBYd|W7GWI|5xj0+#wr#0HTCy7@iVZL zCcIbiUU)?cYxVz@@8mnc9kNY#aU!9@QTT9mGGMXX#jk0yR4`KTejpcOfWic9_DT-^ zb_8jy&&FUKX|qa8?sFjE7K0*tR#~r_Xa)ECLlB^72t(n+F9hK~AB2}QpoH{2aOLMJ z0t+b#PafM2_W)(6V6(=k&$6QnCeNc{d=tsZnN^I#2+zKuomP zW4$)iwT_n6E#pqW@j?FMI+=Th#Y{7(*H-KIO*Fx~&{?)0_&0C_?q7C95$cBv-fj(Q$kL^-&_5NL;CE2H z#zO!a{UwRYTzmd|&x@Xd;==p>*)L30!2r|QJA--VV6YwV@u$ssrP=%Zu);85+WP>d z1iFDOD5cQl<`EWb9ohH&XNAddDwxAoe%wsrUML3sFa>E-RQ%t8CII%7HU7aISJeuP zLb1+PGFX#{xH)T6??9bs{Tm8E32?&-Ag$O4>cevbEG0)q@*m9tF|NR!EWE7Q5jDqv z_PZX^0A(iyL`zH9V$S^C&p}d#HW6894|OqTU1GxAchtLMUZ?Oi$OZk{OoFl7^e`OD zj2Gu@AnB}v^=36uHA4SFh}z^PUbBq^7y2!9FAlo0q`M~ZH?RQfo^R3=lHY;HCaUOO z5#)c?O`y=f05w6pghz?T=<)wKyRMC#9DV#7RiXPXr)?lW+%L0i{G5LSDenCT=DRI1 zqTwkFK)-`rYux)qL`o2pD!-clGJ(c8E|ln|=XLw1*9Ouz`VQajUEpVY5Lk{4jT8`2 z|Fvag@QvYt_@1-8bU%zuZjpG2Bw}nRGL$ed;Ga&0 zZ0`y0g~D99^W|Ot>E3UJwfwj}UQKjFHPzJ_;=Z{A8!89?1>gR7O|ECrZN1<_k63_H zsZb$<)4J+U*+{~yD+yvdxM*qK?G$T_;DKIMuf-NH*;+8O8a<90ewBaGkIjWi1EDo>7M1>d)e;{y1d{! z1ZutBhA~rI&@zA?^x?*nSAeZTS;EJ2z)ea)AlXCj(*u_@Jcgld-+nMNF!1R3x6BBb zHefB+!#p-_9GU}e{492G4a|d!PQf8M7xAb}oVNY<%<~q|IDiywAFPL|+qv@zn&|D; zPs2iPE+mX6AJscY$KWhDJ!H}8yB_V!uuqR)9sDvTR&JMvTgkt1Aq~<<<0m6L|AOua zxEmP8rVL|Nz~+=42LxO@SbdV?6<0TU6w9nROy5CCpmMH4SZ&{UW-Nx_|2+p511ObE zD9CZImGB1aGPp&=h;r)}Nm3$YT20Aytx((o&K`_`#Bvy1oTFWcSQSs90+y;7WjS zoodPEy&1Rz*gRhWV8|5+E#~Ed9DeL>=E{2PyCcc9e75-k0plec&8zS)WJxqZ7=5f+ z&C4IQ6fl68M!d?uXYtBoFo`N?oi~P|6efc;gnYo+nr8RSDy1g_vA?oe>+kbdHnReN$9#spm;v2=JlRsfJj6+@^UuzX<4}kdPwx;I zj`gj^mNY~9$Po?@DYVy6+OvOM0Z6iZ9E?(+(YBBL?UR_1$i+l(bvx79Ukv2{t4P7v z;PBjXAvP7nnLy>DEXn0efAa;;ge=berkTaB>(zu1Tmu@julzIYLvKMTuWmo)Bwn_X z2wVf{;nf{%o_890Qyoan6sY(SmYcHRFDS2+Sgcn%52e}%+r+$_Ak1OI+(A4Kq|1#z z`NZStOst||58E_~p@x=XqWMijSA!+t7OQbd;NI=!+qW8&!&3*i#`xEO5|8KGVHp*c zjqE+Ru37>3a|kW&=0EKKE((O~@Aw1$ks+96gjOub{sGR6HiC|~*WZsW)Bv_K4z%0v zoDk2Si`67UY{S#K&XDW@znA4bwvyQV01}xr_$ab+g(*`nyuk>yT)M{EBNW&Ta4{4f zEr!8XGiZXb*oAZfhUI7^{|~-Km=Vl|a42c)03ur0{27o*8n2xGQ)6@Rf;p8F z3z^E;VK1@S*H%dPYcQE3>bvDTFGzsrM1Xm?Z`NRwh+u4BHxnBWAmjN~$JbbEtC~LOf!N|QLg?`Kh4AzLzdQGTse!LkIN_;wnHJZ_)u7vz9=aAD_J=(j=ys zZNml?bv5|UOkJH$)1<3wu2oZ`tEn}&k!St41t(nXIgUR6`-1dyNqMkfF?NEftD}e4 z;ZqLC%gbv$$Hm#*?y#G~de>7xC6z*$IXGlar3Z4 z$UE-iz-;N0`>E77dn&6~pG}a2;;i*{v*szzGYFHDm`Bm!(#q{66tm1ImGA0aJ+FE$ h|6Hn)%S_Z?kGdAl^%&Tx5dlXamM&Z8;ZECg{{hHbfE@q; literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/apple-touch-icon.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0dc8e417fd8f0a441af7004b0de42d8a7c081896 GIT binary patch literal 2514 zcmZ`(dpHzm7ysqnNVbsDh>|Ys*m0?t%`L-Va+`D;saz&AD-nZYD_wRg*V>pOBSsS? z>@p%lO4r?~7?f5IhF zK+ngU%21)`tLdn#Ji*|D%0nZ{-OnBPEl2lDn5HT=kML&r0V&IXv}3?$RVr-+NF)QJ zI|0f$z&Nh3F=#EIRu{NF*b~!t@ULnlhoCVBodP`MW1tFW_JZlD;^LcG=)a18TH$7k z#S8HEIwXZywGwX{V7>rrZSbWPFRL&|51};NISX|)9Aly4D1?V_C>$ED`U>I7?AlMy(1K8;cElnKQiP8i3 zISMy^!8&^o=V9y`4D_+V4V6EkI2AwbLO30|I{32;%NN061yXn5I2&Pfc-VvCimIPc zv>%avcyU}(@*_@i(0m#TjqrCJe%yiT<0#yRUH+J8g55GaE<#2$ znoc2NJ*W;Cse$oa1i7Ft12bmed@@dOkp2VMfzZ){@&-s2NMs^wH^iqfbsN4;FfqjI zI=reypfj#zB0mY7AP{uWErjhdaJM2m26PJM&xMa8>a%c`i)aQqN?>DwZZ~fM&Gqr~ z)E*)bbjbu0wF1?b(LCTobr0UxGuHd*d|tx*`)|dZFvoIsP-OgkLLN)t;AJ22!qViP zo#j#qJ2&@|oxyBRVNGu3?gb+A-)|`B*QiNT`YqDJK4sxJalALdwR@uZ&=l9X+RUYU z`1?ot7ISU3HN^eZ`jf8le=Z4o`RVRy!S*b42Q_)VROBIhU(&IVF$I<~2_qE^XoUX}8{;EGT7x{J~nS zXT%Z9jq2W;dz+hs z?-|qaHsfRu31&a(Xh@~ymkKk}nF68q2EJB)>4w{-hIQ}RS2pyRn7%zM_fNF4r_sJ0 zwGA!{8)6?Ut`l7H3go90lIfPE6M~tt>x1YG_uFk#dBWU6X1n5V{&o zP)VjG{(A+bMyuBGUs4l-0y;WADYjo_+l>CsG0pNwnne;D^)bWeK-zz337AnhVm8()dM2iC1|p z7e;d@GEIsH)#5TMf?BtfU6s|kZ4TAR&R8G)n&N{MMnUCr)mmWa4jQC&Ku#Ne*COp^=J>7+IS$>64~Jl?W!V8JSn z&3+p9jW%0xu6DMFqkiU9l$W0oImr8-46*aqdp3K~JMEc45!v@-;tv!{*;D%;PVC8% ziTjA8iQuk{pTu$!-4#vg3`fFR#zHREzD(+IeUtKYIv$)PdUgXS_m&iiOg~K1+ zBuhTNf0>#`o>G>mw>PBH2{LC32Sp!)6gsR#3lQ7tv=>>5HyvQ9JE@=O2pcm@&eO za(~-9?wj(6X{?$}BD1kioB33|^sOez`K~r)e92fJ-M1jATD`sIaFT)*qwMd#YkGJ{ z_PktJ_Q_3RJsqp0Xl1!79(5kL zn`VyI$0@%Q561K~Pdt{^%x+gSy(Y@uEh>L;{DN|A39B+xsK$s;_=>&b1VOc$Xn;Wb;m6PinJhjCUw^ zeX&QmRv;`)ip@_DeJhkrwmLO_rUxE+(!Y>&-rBACffI9{TI0v)VfH7j_ULX$+UA3F z@!5A-y!g-KKfG|FNtB}04gAuI`n1o-886s>HW_ufQfQLhJKUtp~xt83vk~t+m>n7@2HivIx&8U9kdfn`QH(|!>sTtb251O1__pVp% zW=pQ;7H(u1mqm$)XQ=>oWIH>OEm?JfZEYzIjucz6HJMBylNIFP-Ty_1iH+p!+W-Fv zMKOQ3s|X9ecGwWRi<=x4&q8u?vJGcXbV5W}9Lpv)KI*)Dt%>T<=9J(qTt-;3d0c$# zF3xV2Id^{?%bXL(jR5u)4!oPCvv<>q6@LUs9{LP2pl+dS6QOHhVL(4)sBU0RO>?*% uscumlY%Y1$_2ACKJH$KMk~HsPUmJ`X&*8pXw<=H70zRJp)T(tmkNg|YHY&#e literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/browserconfig.xml b/frontend/dev/gstd-tic-tac-toe/public/favicons/browserconfig.xml new file mode 100644 index 000000000..5aecc916b --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/public/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #00aba9 + + + diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/favicon-16x16.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..5189aa2793c9b06394a75067c056e79af4841cdb GIT binary patch literal 704 zcmZ`$Yen zO3E-L1p~=UGR!iehl`@wkFunqA4z0kAo+-%x;}sQBe?e-&OP^VFI=C#I6s0T-~fow zs7V8jQEM5-q^meHvXll^l|rikZnSa#m_unUw5SbQpfL^bwgA8AmiIGIM*wpsKzdkDdKiy2++AS`~Yi!{HLVCG&P#L!E|H82M9Yp1p}{((_wq zbgiqLUJaRWde(3|m@_qJ8vWih(inNUJ)ZxZ?5y0nAF}j3@zq;<2fsxyt3B(A0*Cn~ zon$?gct5B6D61ob`EK64T>k>6(iNRG-f?lsf_`9V=WOqh(UkYv{<_L#fkFdCYDbJRXn4ap*v;#q6?4obIYi^SJ_gsJziwK^e>*q08;GJF0C$ zs=;LwI$V?msPFo)$P23}6N{hf0+X7L1~5fjiG>>@iYaW5WyT0eZ`OpBDY|JC2425> i;-B;<`q=@mx~D*bx%g;mQMu(jy#g9lF?n5SYW@p;<=mYB literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/favicon-32x32.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..883e02ba8e7c79d044e49e1231f5004be45b235f GIT binary patch literal 835 zcmZ`%SxgfF5PiKk3biUCk$_e}zyq=z3RXVaTBHRjh(*8Ec7fY#;H50jaBnK$z?Gs)CsYtlr*U?BjJ zTBXckQ8MX1JT@v@YVWYXH_0>Qz=b-$37ufFs8{7=0^1`2#tw|J6f+Ez5x|EcKza-a zvNm5`mBJQoWv|qx!s9?uI-5c4k5_HjX9kyxuA>MLW9S0DT|;Cjmc?MA8^61;JOQCI z@u?GiC*UQ(9s}NXAXknm6ZkybZbW+(S}LGOz}#6FxdKBb{C)AF6@_Vto`c2nG1i51 zm3VLxyNXeqfx$M^m=QG_<2S(+!_f#12c!nyuVLvTROnE<0XL37nF#lJ%n(B>M@Kcz z?1U)`k59wL%j5Ap{9gHz-JesOk*1XWd0sfcCcaixK`9VgJ?We}_Xx?7-jq5s)q6-N z^dm%eqd1qPTxz9Uo80@r`OMZ$OcglY$Jd1@eC@&M>#Lj!i^r39)?P9@e}7Wd&VqwO z2Ud(uzkc72g?-DoN_}Tra{cAK2BFHd@1s~UJ|8{q-Q3S%uXd@KblI;aS|H2imWx`G zemeOTYq`$4&Zo3gHkyYgrE@-irM;$~nrjLjDW}$OBJMVqy(R;h{;_vt1GC^_O4+6U z2v^Y3>f>pypew_C+)y>-@+%H&re+s3E@%tHw>O@MCGY!d_ZK!X*B=$^<&@A}gZ$wx z-$RkIsP-L3ZfBT?>2Hj3-7XcVTBg*isX!Nd>pUtopEBquQmVI+EFhkUkB^BXSkuPE zNfVQ$aS00vLP`*W1DcV41SJ-O*|_chf{w}`?AKw|50BSq_MBxstBHE-Tns@GY` zSc}cn^eH8nZOYrO&8Kp7w8Uz&7|okV3AN2iO3YSD4{U9I^Tpq1%bMuuo~+(JwL1sA zaKBi+UqE<3#*rDk0Ev=Gyk+2pU(!l?2cFz>^f^Kt-o1?KRC+8v1Wlv9C>A%cBcM*z KDBBf9d;S6-z8$py literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/favicon.ico b/frontend/dev/gstd-tic-tac-toe/public/favicons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..663c05fc60d7668ecd73f4fe6ca368b1da174ab8 GIT binary patch literal 7406 zcmeHMX>3$g6#ibP)AqG9-CMeMx=yG2I@5(}TMC6PRA^bs04=o(r6tDr$H*EL6cP8J zKM(;?iHc|q;0g%nAD0+4CYp#bagA|{YuwMdccz_m<_!-e!Nj@AdvotS-#z!7@4cCx z?-o#CKzTXnGYp3!0XqPT#WfBC&e2?Sv>dNB0^4T+1%#m`7@&*SiKfaAaaUyj!FS~6 zuH!)4Dj?Ycyzm1MWT3U1fD=ChRdWDi2yp0qU~nsN>Q`XHJ|L9*J^BH~GY>fZ2gPIq z7LEX_3D|uGSTqJ~eHf_k1WZxD*bd;nSAd=g;Hgi6r@x@M698Kc5Ss+tbOhKj2|V)+ z(7GHbX#jQ}rT8h<{tXn@j}%`PUg4^aI!K0Umh|xb6WUgs2|69=Q7?kVy~W^zXo?0~FJT zz}dfmEe`<^^t|djD3+IjoKj%%I555wnEV`wiU-!;M$fOBIHBjb_YL6q*Az=S@a%WM z+!gY91?~X`E(Qb6K>IfgT&Bntq0dQ>?Sk-`*)F)E%fCI@4cCWnW_bO4zU%RiC`Lvu z(uofnU|^F|FMbWf+ve!S3o1J+oI3H6$}W0Ly5g~~=4eLDdhx>yPgu-vfgb-0dKurx za6R~L2gCCP+?mgP|Icd{L)MK6d>{Ay|83~HLK2x&m3h)seo6`6>xw4?F|gc zVk(`^Pzm>kznmcnGQrs)=fz7Z>xB1t!7jMA|DrgCObEE7vXdt?`K)EDi^^*5mx-^u z7QTR~Ea?~SqXm`C{@#Zx1(hZNmsG~s{orHEIRa~?z;|)qlHu?D8DvOGjesXIm9|`e z@7vmN#;+7`No6|qHRCB-#&CmxJH@YvquU=op5evg0w2l!vR3YE`OjfUNt%GWs1$t- zeB%(~mk5ZPN8**c9 z*W4?066-Y9ZLCk-T5ELsv`%Eb<<_mN$5{8V)*Rd-H6iON)}*W(w?9EX<|TdEaVL>Qa3 z?HP(vO9h?QdOG$}>PXRP*c+(UHJCN+4!feHCTQEKip`ETYuZDoK(kR|8vd3A~pE8NA+wvn8UC%6l$z>Jjc_yU`IK4}O%j2WnrJijmy?&` zeyM-?xK4?t%R<*wo#LuZXo@GNT39$Re>E_CgFHv)Gws740Y|@-=hs}9@L9Tv9P)Xt zXFU?1B5RV9zsR%U$y|y`R}J|k{chV^YJ?YN1HrvpGq>&2`~^~ zz>5J@Z>yw#?o@byEr|0R=jRm1c#N|P6QVrFS;3{Mb=qDHvs#oqH4cd=PYIEUUiLwsxsA7n^AR-J!1CJ^&;c~ GC;kTRgr>0o literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-144x144.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..cc0de5f49b66734372446cf483f68121bb917e6d GIT binary patch literal 2148 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 Tn3=(a@2T2YI#`sM?LYn}KWd?{ literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-310x150.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-310x150.png new file mode 100644 index 0000000000000000000000000000000000000000..82a7676b52e47b65682ada3941276b5e71226e4a GIT binary patch literal 2629 zcma)6c~H~W7XACKZ-APmZ z3v~dRF3yf#z#IyovUq{2;^fpHk>4q{xuJFpJK%bz#)rUp3alUO?8N{$Yk}0mz>MN3 z^)KKDDj*L4tWE-?=z^N<-zqK>cy43T6#@9vfSF%Wn}y*re7uYL9P}6CO&!__5$L82 zFw#d-07%R5R|RaTn5={0Duivv``g%T0+&sgKM&W^F<6Sl8sNv_XehL`Fmes&ldxbu zI?rMD4(vC8PQz|GOvsq7hpZgSwQwvFFRP)i1NKgo9fjIF+$%x@AF6YpqJl~ug16yo zUA(SEav)YLMW_d?OwnEh(IK3O#ZO^as)-9paM*x7-+>o_)jEjr#k{#_7GSO_Ze)Pv zj#o8E3Bo!(l%zpx5%Ob^5ssm&xPK0gmaw7X!yT+&jVUR94#OH=v$2aH$aPCgO{Y+C>6)2CtD z5yj*}CCyTGin>eM&a$pHIkk zN`;x?>4hAIo(E;jyY*5+ntZ4s_a8M5<4mHuB*E8|q+_$~O|(cFUnm^AoXd;%j?LaM zDX{4p?2wn$Desg^njRWOa8CGjjb`zpj3kztUe?FN(?O**9h6g{A_XB(%#7MF*yW7g65cjk%GH8%VgkaQlM=TtWhuD`1ES(79QkztFwD$10| zfx`ZI=3TYrQ>8K$vd)-O@*y_Y?3S_!6J=vZr2M_gghE2*DixMjOtX~|w487olIPK^ zH%gR&k&>ow6~5~V#Yb~_9y+ntBW8AUH%81@v15!PC~T=B!_aqJ|A1ag$)>!`{NtyY zS{wVh6D!0@cAKQ>YE{Xq{Z$G&DTn9qFgAP3q@ccQaNKM9eta&^Rz;${@A8%Bborn? z<>Oo#`SsX#Ym=Lb=;p>VS%l0_DjXNbOO@y>A^Vs|n}y``eqFqO2Cjs$C)S#f zYK8p1_lLByd7%d-O?QnC=_p^aU}5{Emp{p$+|Q#iDD{V*k=@=1`~t@Jwl+1EKOto1 zQlVbev|&wiNdtrY zES_!UC^@Rw&!?Z(o}wGG&WRE+DOlw}`8d)ukJ$cWUXwNs3$_sx3C5{>ROK`KeV;tD$Vz_b?&bk>48nw?eg*lBQ7+ z->m*?Qu%pVMfe5z$a?mKu`8vszvRJWuWXoEK*&zZl#@pu>AYtD=9Q`Bt+o*q@!T*hWw8ccj$Q+v&-Mq{qY0$SGO8i0s&Ru?CWa6`M)#N^=QaULo%TgL0hSap{E~ zdtyC@!cvarQd`ogF57dbh{U&g?1g{yrdZF()|iv^S@u~!@#h7la;D81Z|v6T6}1*B z7;EC_JL5tE<5*U~Y?fk!1=YgB)SRj?rn$M5P-St`x{gOXFVI)AE&V NyU^Vo%j^Tv{srpwHH`oO literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-310x310.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-310x310.png new file mode 100644 index 0000000000000000000000000000000000000000..20b2084c14dab937ba27b1e7538eab4cdfefb461 GIT binary patch literal 4720 zcmds4do^;xk&;ES&-p})V z<7})gepf1OgVDg3Lr!s zAo2>pJgSQP3=lv75V`>v+yYSWyUnpR1P}w8-|fuN39$4E-rfiMgOC*rJ%zx{1M(3} zFDU_SS>SdQYHmP7I!rwWbtU*s8AO#ZT?x&%K|&nP+Ca-4SS10MJ>lVHh&cnLv9MSJ z2en}BPw=T2O!vaCvM^W*1H~ZsGnf#-TnCD-Ks*(gF_3T`nliwh2(L3?yCUq^0%xqj z>li$UhO1uikqP_~ke3Bx0(`H5IB)PELSqI@a^SQT#GZx1DEMU!6ki2#G1$2kR5wFw z7Tk}7Lwn&v5g6g2Bo?ln1Iq89vI!>G;Ass7k?^NCm}$f3a!`;1LmaT;VZ{o#;tBP) zVD<%Qs6t0BNUehF=OI4=e$>KIeUM!P{f|KK81`$xlLUx94c@k}Z6ml?K%x)ig#*z5 z`X0dsIav4$o?eGY51<~0l`G+88sJnxX+4ZRh0c2*EeXlK@Vf!5kp^J}===urFX7pB z2zLh`JE%^DZ`FX`4t58D?gzPHV6`7Exq`1fyr6^PdKi5IU3qZg0Hh(16AIRbAR`4E znYo0Nf6ObGBe(1cmO2DF1MMVKCd0m6@U9TjFG9vea4-d4K5S8dztTW#B?O#; zi_Y*o8FY8SreEQw)sPtoNq>OjA!xq~E~ zLFxrKWe#7Tf$~Op#ehHTA`>5MN6}Tc0oO(y|auv_Uq~|{47#G;70#D_JFDE3OQ3>=@q7y+N+Y!D(ktu zbQ6zWQ}w4ushi`To$E)gt=K$+yH>R=r!3H{F1l*Z+L(&;#j()-*~&M@&EvzedM59O zpL1Ktsp&EGcM#2 zbjqihRoR13`e<(7E)a>U+4`=F{6BB@V15se+)!Rjxh^_aO{fsXh9X8tQ^92I`y9#! z&34U(t1QD~PqnM6!{zg75R;UslD z;y|y0;jA9JaL&YjVZCwb%<}A4oNrFD<@H-a{YDiyl?6@aIq^xXPwS2IW@LI@I`}Go zw+^4{uZiV`yRT-O@wy~KZ|Pk0${2`QRpO1T{VcqU#vvFbV-tIKY!+mwD@5T;dTLwo z>aqIdvm^hOX@xp73G=*);rs9SDjhz!dr75S!j{?Zv`ms2=S}|7S?Y+OL62Oxh|XCnFlJnH{??5;9FRc%`MixmyFFKT z`XOeKZH}`Q(TN%7uN&3w<(>Z`(XV5LTWK`d?nUlM3OX&45k+vUaI1|5>4!6(e!ZHg zFcNy{XZEX3XPmbqb)@Z{BECh()sKYXq2Lx9oGH(Xy{EPW{#zlqBF|)Pby>(L{vp`M zmYU8W8hB^1W+;mIZXRv0dfg6%s4RcW6GdfShTMthxsK7-5xcE%E31aJmQGWBcsf3P z1NJ;^)G&)^;2Fi5S(T`e_;AX%4rTo?PGE-yFo3!;>3y=q)MHC^HhF%~rjB%MbzL;A zJNC2{qf5xAug~C3Zw$uhFT6oC=%_5n{t)!n55WuxiOVPByi;_Ze96e7RJmc!D?0%VwDw@1K`*JkQRXYN5_IB`Sn)u$qY?wa;<8FDvMg2_*ve z@F_IXj7HBNG29*n7t*4st88^J>fDM?$FVz4g(@}3%;*?plA_OUXVB7hE*4ORfVVm#M053 zB5T|`5xtnnqb?`Q2}dj`s3~kaQpX#=N<#UgYRX~U%GN=8miP7E;F|+6F0Bd< z3;WNYe%)|lZ=Tfu@gajO(k($D{YBdJtYEailDtfny8nY`1tkHMZc9U=T@eilB=JqH zYDNv4Lcg-n8bW8W#gVmV_n~Zu_f0)=EnQ-Md%X-IlNb^eY*qa?8k%q2j+A)2T6Uho zNlGAjB*;B?uIAb@^fu2AYTRxfb2R9$@2y|fEgRk;awhY+J$ULEma2MxOT4b{M@K!f zUPl0iX^Nfqg;^%)DhP7l`z0o}7%hj!_L+3uPIkhl~(n1Y6gnGG2 z zn?Ft<#x2#oR64(6p(Hw$_#7EVCSr>`AEV_#KQ=~ck2UeITCi=9=|y)h`mq^Odqa`8 zq{d%EtgNRvhc&M~Z}XC_tGq1tW9-VgD%{XuGHW|!Ajy+v~w*wUD9q`{{f`xJ+! z4#d1`RJ*zlvCJ;xx_o^rl;ogIp_c7{uzM>DOmwT~B^H*tRTxhy=)|5zR5Kp*(Qpav zwz-N_;(Rn~yfRf0e|wx6L()3;8$)eKC+w4S7ycWXf~NcVq;`c?>O4`b3;%3c!6daa zTH~re1%1>)Qm7h?X?v;%PbW;DUC3fIkGekI_s7=^b;hNh=KN0?FZw2;$1&+ti&|Lk zrrp+8IL4Fp>_@wt<294LtZLvc44EnK0>^$G}{SogbY7oTs zoGxQpi8?IN&kTQg0nLW7#;}KLNB=^&NXF4&nYm_m-qu^ZX;~$_-Lm|SokwNn4=*-O zn076L%xb1@bw1HQmnu|vh}LP-eG#?`Wpk@Bl4krZP4+hYU=PF$@>NtC)fQ_ug_>by zFloz2-r$)msw)~(k_v-X5^S~fdLmk6;($g$C$*O_NkfbE6E$7@+?=Mm7X{-}qz+zk ze55*q*4FEQuV$isLNv}LwT~B(44Rejd(;^-4-m`hr^v>MSfS)MlNI8p+3)>tb~P_{ zVI9|qSkAVUhOjS_RN4pWKh6#wN2@PBlEC^vsw z`DqGKubtGM%_JTE9z)9gXGlv};((u>O(7C`NbOrFO!7^1_=(is-{R}~!x~ps=r34` zz0n+mHQkB;+&bQLTP=!FC8t;|KO)05(OQjhq_fP>O>8$~t!k6nF9Ew{H?ZQmnhV1C zanriFMyNk5aNv1ZAIZ0ik&$GLl zf9%E%s+=~;PqCSk}mX0)f5r!GKe0nAO&~uh`in)Vu1JhB{SO*cPz) zP&yTLno6Q|dapsR2fG=9hUEAJgX`Pg@}|qsFt2eE?kahqVIIZp5)!UzHoW$%uji}e zj(Kbdvv8_13|fvM{v*1xZAbpG@zN|hiNfC7WNzwGS2~}B7K!dqu{XcnIAC3ECgDZL zW~9kNDdwXdB5l!o#S^^?-LtMiPjy84@SQ7yGzq?3JV)xC84_o%bj9a`HX?1VWELZD zy&X-Zk)6K(7Pfe9+ue2*MB2>dQ2rR!$rdh$SjEc&rOUM*i1eY}=-QWW`EoHFiKH0p z>P1g7kYJ2q4P)UF{YDFkJE#=>=DSRjns|VeHG^~%x3+NqS55*+#m4J{j-NhTJg4oY z&qp>t_kQuqal>(}^$Jx#*Gx7U4<{4%30M8UxU>H^FBboa*P#F98dAc3n~JJcDK75Y zPzyDhxfAWQJI&L8f_S12&?0DQX=oDAubrl*fwrE3=Dytof&qb$@WI{TzZm%Vp7uTy z{68BkR=pLX296aSw*%4U(;wWq~YAKNK6cXu|vYI&vjO0qffNX}6l2o=`SCQWs2 PV6ZLCt<8!}-J<^m^8py8 literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-70x70.png b/frontend/dev/gstd-tic-tac-toe/public/favicons/mstile-70x70.png new file mode 100644 index 0000000000000000000000000000000000000000..8fc7f143559862371a912211c45429302ea64750 GIT binary patch literal 1687 zcmZ`&c~sL^8ofyfWML~%gHVMEB8!9sQP~ZaR8tlKQx=5?2_WkbAPu`j99$+v&|(o$ zo3bd?wu(!Yh#*TQC@y$J8_;5fX+=5_rCX?^WL{7B7B_* z!-fICM6fgRoL2{PCk6cz-uRWsiuQ3-7?SsxV!6gv^Ve*!b}{t4hS z4)8P)@I45yq=(g!{s7rLB2>sH44CV~pS}3$1b+S&|2c&}y7AiuJimx!;w0dk3LKK7 zC;B+lW}<;ZXAUn6@eVQ>%n3cY~P3eJpPq|-RbDA1%-^L zHJI%|uony{=syJcW*jNPFQ;K;1_xVAsxfl~t6jmQL3a$Nc0m$~`>ilxV8t@L?Lule zTDPO20EO{*)d6#3#C(LCM-kuwWhNp6A@GK_9tE3mxDXR(z_P#(pMyj~QV1S=15YQU zMc}vsZ#v=bfQ}m6X#~v>RavM^#V0;^au!Q$@Uk60wqT0@8Id@q0NVj(#&~!VLrtI? z!Jm!le_%%ndJZ5W03p8k_d!@&;6fEd>ya6SmU6tljO9!4X%Ki^__@Q*8Z|lCEg98DbE0p%ZV``8l0}7Y1n*Z}>bww# ztP4JMGpmV92qT#wjBoTSgTdsOF3J%-2Pjs8;2>e)$g@K+F&iVzE9;dOpZA$Y3$qgU zw9*)I>Lsyuv_!kN)x6DK{9OEecD1^+zrIW$PF$LQb3m^R$V>@zdK;URyTa4Urt0IS z+i4?}$xUFW=5d7l!ZS=gEoyNyJ2RF|4?aHZB2sN0Z1#KY^I+E)X=-H}AAuu#mHaT*Lwdu+{C&DN9(U~^5oc<1a3@||jPs{pKgc$Gg*cnfK&9d^$F1wW8`We-&ILK zki9&2OS7W;;BE}YiKM0_(#Qo@{BE&DSAzO2aJNm#Dr4WH$5Qm&{5Yt4{A^C= zoTHkzXUM%0db>XhAv&$%Og)RQrKlP@S{9acDJwAQ{-@^0iXAmeapF?;y>o`DH|@@J zUV%=tNq?Ik3H5;Tx1@(@OcjNFjXXHp%w|vqDS@v)(l=z23afP*?Vxg-!z4AnbaIFa z_4IW*0v=a)GnZjsonG9)MDeQ;^Nv00wD%RGlMOF!(s6k!YmGN)+IqY(1v9mov_Leq>C)3ZCOq#ub;7S4EOwKf74vX38ch5954RoZ z8aC+lF*~s8VBkKajDczn2Ka6boZzQAq{%(!8A1^sE$nS&=lwK3yu40ELIgCyi;j(az4- zHVj-B{1OF<1CuRbS~^&Ud|^egv=3Hz^(Ips&I;`ZbVL1p*ZV%`Ga68=SAD|9Q`UR3 T$+asF5LZCJ4-fu+ZDPe=P-1I0 literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/safari-pinned-tab.svg b/frontend/dev/gstd-tic-tac-toe/public/favicons/safari-pinned-tab.svg new file mode 100644 index 000000000..ae140daef --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/public/favicons/safari-pinned-tab.svg @@ -0,0 +1,18 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/public/favicons/site.webmanifest b/frontend/dev/gstd-tic-tac-toe/public/favicons/site.webmanifest new file mode 100644 index 000000000..26b0806c7 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/public/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Vara: Tic-Tac-Toe", + "short_name": "Tic-Tac-Toe", + "icons": [ + { + "src": "/favicons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Bold.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Bold.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-ExtraLight.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-ExtraLight.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Light.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Light.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Medium.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Medium.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Regular.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Regular.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-SemiBold.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-SemiBold.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Thin.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/Anuphan-Thin.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/dev/gstd-tic-tac-toe/public/fonts/anuphan-variable.woff2 b/frontend/dev/gstd-tic-tac-toe/public/fonts/anuphan-variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9a02514d4b05093eb70a8f8124f9f14b1500c83a GIT binary patch literal 83504 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 literal 0 HcmV?d00001 diff --git a/frontend/dev/gstd-tic-tac-toe/public/sprites/icons.svg b/frontend/dev/gstd-tic-tac-toe/public/sprites/icons.svg new file mode 100644 index 000000000..52935f368 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/public/sprites/icons.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/app.scss b/frontend/dev/gstd-tic-tac-toe/src/app.scss new file mode 100644 index 000000000..56d29507e --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/app.scss @@ -0,0 +1,2 @@ +@import '@/assets/styles'; +@import 'global.css'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/app.tsx b/frontend/dev/gstd-tic-tac-toe/src/app.tsx new file mode 100644 index 000000000..097414b81 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/app.tsx @@ -0,0 +1,31 @@ +import './app.scss'; +import { withProviders } from '@/app/hocs'; +import { useInitGame, useInitGameSync } from '@/features/tic-tac-toe/hooks'; +import meta from '@/features/tic-tac-toe/assets/meta/tic_tac_toe.meta.txt'; +import { Loader, LoadingError, MainLayout } from '@/components'; +import '@gear-js/vara-ui/dist/style.css'; +import { Routing } from '@/pages'; +import { useProgramMetadata } from './app/hooks'; + +function Component() { + const metadata = useProgramMetadata(meta); + const { isGameReady } = useInitGame(); + const { errorGame: hasError } = useInitGameSync(metadata); + + return ( + + {!!hasError && ( + +

Error in the Game contract :(

+
+            Error message: {hasError}
+          
+
+ )} + {!hasError && isGameReady && } + {!hasError && !isGameReady && } +
+ ); +} + +export const App = withProviders(Component); diff --git a/frontend/dev/gstd-tic-tac-toe/src/app/consts.ts b/frontend/dev/gstd-tic-tac-toe/src/app/consts.ts new file mode 100644 index 000000000..36284a109 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/app/consts.ts @@ -0,0 +1,20 @@ +export const ACCOUNT_ID_LOCAL_STORAGE_KEY = 'account'; + +export const ADDRESS = { + NODE: import.meta.env.VITE_NODE_ADDRESS, + GASLESS_BACKEND: import.meta.env.VITE_GASLESS_BACKEND_ADDRESS, + BASE_NODES: import.meta.env.VITE_DEFAULT_NODES_URL, + DNS_API_URL: import.meta.env.VITE_DNS_API_URL, + DNS_NAME: import.meta.env.VITE_DNS_NAME, + STAGING_NODES: import.meta.env.VITE_STAGING_NODES_URL, + SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN_TTT, +}; + +export const ROUTES = { + HOME: '/', + LOGIN: '/login', + // UNAUTHORIZED: '/not-authorized', + NOTFOUND: '*', +}; + +export const SIGNLESS_ALLOWED_ACTIONS = ['StartGame', 'Move', 'Skip']; diff --git a/frontend/dev/gstd-tic-tac-toe/src/app/hocs/index.tsx b/frontend/dev/gstd-tic-tac-toe/src/app/hocs/index.tsx new file mode 100644 index 000000000..d9714dbf7 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/app/hocs/index.tsx @@ -0,0 +1,78 @@ +import { + ApiProvider as GearApiProvider, + AlertProvider as GearAlertProvider, + AccountProvider as GearAccountProvider, + ProviderProps, +} from '@gear-js/react-hooks'; +import { ComponentType } from 'react'; +import { BrowserRouter } from 'react-router-dom'; + +import { DnsProvider as SharedDnsProvider, useDnsProgramIds } from '@dapps-frontend/hooks'; +import { + SignlessTransactionsProvider as SharedSignlessTransactionsProvider, + GaslessTransactionsProvider as SharedGaslessTransactionsProvider, + EzTransactionsProvider, +} from '@dapps-frontend/ez-transactions'; + +import metaTxt from '@/features/tic-tac-toe/assets/meta/tic_tac_toe.meta.txt'; +import { ADDRESS } from '@/app/consts'; +import { Alert, alertStyles } from '@/components/ui/alert'; + +function ApiProvider({ children }: ProviderProps) { + return {children}; +} + +function AccountProvider({ children }: ProviderProps) { + return {children}; +} + +function AlertProvider({ children }: ProviderProps) { + return ( + + {children} + + ); +} + +function DnsProvider({ children }: ProviderProps) { + return ( + + {children} + + ); +} + +function GaslessTransactionsProvider({ children }: ProviderProps) { + const { programId } = useDnsProgramIds(); + return ( + + {children} + + ); +} + +function SignlessTransactionsProvider({ children }: ProviderProps) { + const { programId } = useDnsProgramIds(); + return ( + + {children} + + ); +} + +const providers = [ + BrowserRouter, + ApiProvider, + AccountProvider, + AlertProvider, + DnsProvider, + GaslessTransactionsProvider, + SignlessTransactionsProvider, + EzTransactionsProvider, +]; + +function withProviders(Component: ComponentType) { + return () => providers.reduceRight((children, Provider) => {children}, ); +} + +export { withProviders }; diff --git a/frontend/apps/tic-tac-toe/src/app/hooks/api.ts b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/api.ts similarity index 100% rename from frontend/apps/tic-tac-toe/src/app/hooks/api.ts rename to frontend/dev/gstd-tic-tac-toe/src/app/hooks/api.ts diff --git a/frontend/apps/tic-tac-toe/src/app/hooks/index.ts b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/index.ts similarity index 100% rename from frontend/apps/tic-tac-toe/src/app/hooks/index.ts rename to frontend/dev/gstd-tic-tac-toe/src/app/hooks/index.ts diff --git a/frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-is-app-ready.ts b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-is-app-ready.ts new file mode 100644 index 000000000..f7c155d27 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-is-app-ready.ts @@ -0,0 +1,34 @@ +import { atom, useAtomValue, useSetAtom } from 'jotai'; +import { useAccount, useApi } from '@gear-js/react-hooks'; +import { useAccountAvailableBalance, useAccountAvailableBalanceSync } from '@/features/account-available-balance/hooks'; +import { useEffect } from 'react'; +import { useAuth } from '@/features/auth'; + +const isAppReadyAtom = atom(false); + +export function useIsAppReady() { + const isAppReady = useAtomValue(isAppReadyAtom); + const setIsAppReady = useSetAtom(isAppReadyAtom); + + return { isAppReady, setIsAppReady }; +} + +export function useIsAppReadySync() { + const { isApiReady } = useApi(); + const { isAccountReady } = useAccount(); + const { isAvailableBalanceReady } = useAccountAvailableBalance(); + const { isAuthReady } = useAuth(); + + const { setIsAppReady } = useIsAppReady(); + + useAccountAvailableBalanceSync(); + console.log('----------------'); + console.log(isApiReady); + console.log(isAccountReady); + console.log(isAvailableBalanceReady); + console.log(isAuthReady); + useEffect(() => { + setIsAppReady(isApiReady && isAccountReady && isAvailableBalanceReady && isAuthReady); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAccountReady, isApiReady, isAvailableBalanceReady, isAuthReady]); +} diff --git a/frontend/apps/tic-tac-toe/src/app/hooks/use-login-by-params.tsx b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-login-by-params.tsx similarity index 100% rename from frontend/apps/tic-tac-toe/src/app/hooks/use-login-by-params.tsx rename to frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-login-by-params.tsx diff --git a/frontend/apps/tic-tac-toe/src/app/hooks/use-once-read-state.ts b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-once-read-state.ts similarity index 100% rename from frontend/apps/tic-tac-toe/src/app/hooks/use-once-read-state.ts rename to frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-once-read-state.ts diff --git a/frontend/apps/tic-tac-toe/src/app/hooks/use-watch-messages.ts b/frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-watch-messages.ts similarity index 100% rename from frontend/apps/tic-tac-toe/src/app/hooks/use-watch-messages.ts rename to frontend/dev/gstd-tic-tac-toe/src/app/hooks/use-watch-messages.ts diff --git a/frontend/dev/gstd-tic-tac-toe/src/app/types.ts b/frontend/dev/gstd-tic-tac-toe/src/app/types.ts new file mode 100644 index 000000000..0c3956f2f --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/app/types.ts @@ -0,0 +1,33 @@ +import { FC, PropsWithChildren, SVGProps } from 'react'; + +export type BaseComponentProps = PropsWithChildren & { + className?: string; +}; + +export type PickPartial = T | Pick; + +// in case Object.entries return value is immutable +// ref: https://stackoverflow.com/a/60142095 +export type Entries = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; + +export type SVGComponent = FC< + SVGProps & { + title?: string | undefined; + } +>; + +export type ArrayElement = ArrayType extends readonly (infer ElementType)[] + ? ElementType + : never; + +export type ContractError = { + message?: string; +}; + +declare global { + interface Window { + walletExtension?: { isNovaWallet: boolean }; + } +} diff --git a/frontend/apps/tic-tac-toe/src/app/utils.ts b/frontend/dev/gstd-tic-tac-toe/src/app/utils.ts similarity index 100% rename from frontend/apps/tic-tac-toe/src/app/utils.ts rename to frontend/dev/gstd-tic-tac-toe/src/app/utils.ts diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/burger-menu.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/burger-menu.svg new file mode 100644 index 000000000..e0aeb3426 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/burger-menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/caret-down.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/caret-down.svg new file mode 100644 index 000000000..7cc1f0b2b --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/caret-down.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-down.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-down.svg new file mode 100644 index 000000000..b6f35713f --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-down.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-left.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-left.svg new file mode 100644 index 000000000..f35573311 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-left.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-right.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-right.svg new file mode 100644 index 000000000..9404e32bb --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevron-right.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-left.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-left.svg new file mode 100644 index 000000000..a8fea8d72 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-left.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-right.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-right.svg new file mode 100644 index 000000000..b01a5a7c1 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/chevrons-right.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/cross.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/cross.svg new file mode 100644 index 000000000..dc8f8e1fc --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/cross.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/discord.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/discord.svg new file mode 100644 index 000000000..edfc883f8 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/discord.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear-logo.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear-logo.svg new file mode 100644 index 000000000..49aa60865 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear.svg new file mode 100644 index 000000000..f9113ea5c --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/gear.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/github.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/github.svg new file mode 100644 index 000000000..68b456739 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/github.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/medium.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/medium.svg new file mode 100644 index 000000000..af8375726 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/medium.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/search.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/search.svg new file mode 100644 index 000000000..f5820d16b --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/search.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/star.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/star.svg new file mode 100644 index 000000000..0515c08f1 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/star.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/tvara-coin.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/tvara-coin.svg new file mode 100644 index 000000000..ec6036faa --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/tvara-coin.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/twitter.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/twitter.svg new file mode 100644 index 000000000..9187632cc --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/twitter.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-coin.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-coin.svg new file mode 100644 index 000000000..0d0467d2f --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-coin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-logo.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-logo.svg new file mode 100644 index 000000000..67452d7d9 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-sign.svg b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-sign.svg new file mode 100644 index 000000000..29c4466cc --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/icons/vara-sign.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/images/index.ts b/frontend/dev/gstd-tic-tac-toe/src/assets/images/index.ts new file mode 100644 index 000000000..ddf5a53fd --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/images/index.ts @@ -0,0 +1,19 @@ +export { ReactComponent as CrossIcon } from './icons/cross.svg'; +export { ReactComponent as BurgerMenuIcon } from './icons/burger-menu.svg'; +export { ReactComponent as GearIcon } from './icons/gear.svg'; +export { ReactComponent as TwitterIcon } from './icons/twitter.svg'; +export { ReactComponent as GithubIcon } from './icons/github.svg'; +export { ReactComponent as DiscordIcon } from './icons/discord.svg'; +export { ReactComponent as MediumIcon } from './icons/medium.svg'; +export { ReactComponent as VaraLogoIcon } from './icons/vara-logo.svg'; +export { ReactComponent as VaraSignIcon } from './icons/vara-sign.svg'; +export { ReactComponent as GearLogoIcon } from './icons/gear-logo.svg'; +export { ReactComponent as ChevronDown } from './icons/chevron-down.svg'; +export { ReactComponent as ChevronLeft } from './icons/chevron-left.svg'; +export { ReactComponent as ChevronsLeft } from './icons/chevrons-left.svg'; +export { ReactComponent as ChevronRight } from './icons/chevron-right.svg'; +export { ReactComponent as ChevronsRight } from './icons/chevrons-right.svg'; +export { ReactComponent as CaretDown } from './icons/caret-down.svg'; +export { ReactComponent as VaraCoinIcon } from './icons/vara-coin.svg'; +export { ReactComponent as TVaraCoinIcon } from './icons/tvara-coin.svg'; +export { ReactComponent as BonusPointsIcon } from './icons/star.svg'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/styles/common.scss b/frontend/dev/gstd-tic-tac-toe/src/assets/styles/common.scss new file mode 100644 index 000000000..eafcb83e0 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/styles/common.scss @@ -0,0 +1,40 @@ +body { + font-family: 'Anuphan', sans-serif; + color: #000; + background-color: #fff; + font-variant-numeric: lining-nums proportional-nums; + + @supports not (font-variation-settings: 'wdth' 115) { + & { + font-family: 'Anuphan-Fallback', sans-serif; + } + } + + &.modal-open { + overflow-y: hidden; + } +} + +#root { + display: flex; + flex-direction: column; + min-height: 100vh; + max-width: 100vw; + + @supports (height: 1svh) { + min-height: 100svh; + } +} + +main { + position: relative; // for loaders + display: flex; + flex-direction: column; + flex: 1; + padding: 32px 0; + overflow-x: hidden; + + @media screen and (max-width: 767px) { + padding: 0; + } +} diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/styles/fonts.scss b/frontend/dev/gstd-tic-tac-toe/src/assets/styles/fonts.scss new file mode 100644 index 000000000..62e199de7 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/styles/fonts.scss @@ -0,0 +1,57 @@ +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 100; + src: url('/fonts/Anuphan-Thin.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 200; + src: url('/fonts/Anuphan-ExtraLight.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 300; + src: url('/fonts/Anuphan-Light.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 400; + src: url('/fonts/Anuphan-Regular.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 500; + src: url('/fonts/Anuphan-Medium.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 600; + src: url('/fonts/Anuphan-SemiBold.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan-Fallback'; + font-weight: 700; + src: url('/fonts/Anuphan-Bold.woff2') format('woff2'); + font-display: swap; +} + +@font-face { + font-family: 'Anuphan'; + src: url('/fonts/anuphan-variable.woff2') format('woff2-variations'); + font-weight: 100 700; + font-stretch: 75% 125%; + font-style: normal; + font-display: swap; +} diff --git a/frontend/dev/gstd-tic-tac-toe/src/assets/styles/index.scss b/frontend/dev/gstd-tic-tac-toe/src/assets/styles/index.scss new file mode 100644 index 000000000..0b0cb0733 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/assets/styles/index.scss @@ -0,0 +1,2 @@ +@import 'fonts'; +@import 'common'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/index.ts b/frontend/dev/gstd-tic-tac-toe/src/components/index.ts new file mode 100644 index 000000000..04ef08bae --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/index.ts @@ -0,0 +1,3 @@ +export { Header, NotAuthorized, NotFound, MainLayout } from './layout'; +export { ApiLoader, Loader, LoadingError } from './loaders'; +export { Modal } from './ui/modal'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.module.scss b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.module.scss new file mode 100644 index 000000000..2baa1f508 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.module.scss @@ -0,0 +1,48 @@ +.header { + position: relative; + z-index: 9; + padding: 20px; + + &__container { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__logo { + flex-shrink: 0; + + &--center { + @media screen and (max-width: 767px) { + margin: 0 auto; + } + } + } + + &__wallet { + @media screen and (max-width: 767px) { + display: none; + } + } +} + +.wallet > button { + font-size: 16px; + padding-right: 22px; + padding-left: 22px; +} + +.mobile_balance { + display: none; + + @media screen and (max-width: 767px) { + display: block; + display: flex; + margin-right: 14px; + } +} + +.menu_wrapper { + display: flex; + flex-direction: row-reverse; +} diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.tsx b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.tsx new file mode 100644 index 000000000..89514784d --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/header.tsx @@ -0,0 +1,28 @@ +import { EzGaslessTransactions, EzSignlessTransactions } from '@dapps-frontend/ez-transactions'; +import { Logo } from './logo'; +import styles from './header.module.scss'; +import { Header as CommonHeader, MenuHandler } from '@dapps-frontend/ui'; +import clsx from 'clsx'; +import { useAccount } from '@gear-js/react-hooks'; +import { SIGNLESS_ALLOWED_ACTIONS } from '@/app/consts'; + +export function Header() { + const { account } = useAccount(); + + return ( + + } + className={{ header: styles.header, content: styles.header__container }} + menu={ + }, + { key: 'gasless', option: }, + ]} + /> + } + /> + ); +} diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/index.ts b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/index.ts new file mode 100644 index 000000000..ddd972315 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/index.ts @@ -0,0 +1 @@ +export { Header } from './header'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/index.ts b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/index.ts new file mode 100644 index 000000000..cfdf7a76b --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/index.ts @@ -0,0 +1 @@ +export { Logo } from './logo'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.module.scss b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.module.scss new file mode 100644 index 000000000..b9def072c --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.module.scss @@ -0,0 +1,43 @@ +.link { + display: inline-flex; + transition: opacity 300ms ease; + + @media screen and (max-width: 767px) { + position: relative; + } + + &:not(.active):hover { + opacity: 0.7; + } + + .title { + --gradient-to: #0ed3a3; + + align-self: flex-start; + margin-top: -4px; + font-size: 20px; + line-height: 24px; + white-space: nowrap; + user-select: none; + + @media screen and (max-width: 767px) { + display: none; + position: absolute; + left: 100%; + font-size: 10px; + line-height: 18px; + } + } +} + +.logo { + width: 100%; + height: 100%; + max-height: 60px; + max-width: 92px; + + @media screen and (max-width: 767px) { + max-height: 40px; + max-width: 62px; + } +} diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.tsx b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.tsx new file mode 100644 index 000000000..44e54e9dc --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/header/logo/logo.tsx @@ -0,0 +1,20 @@ +import { NavLink } from 'react-router-dom'; +import clsx from 'clsx'; +import styles from './logo.module.scss'; +import { ROUTES } from '@/app/consts'; +import { TextGradient } from '@/components/ui/text-gradient'; +import { Sprite } from '@/components/ui/sprite'; +import type { BaseComponentProps } from '@/app/types'; + +type LogoProps = BaseComponentProps & { + label?: string; +}; + +export function Logo({ className, label }: LogoProps) { + return ( + clsx(styles.link, isActive && styles.active, className)}> + + {label && {label}} + + ); +} diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/index.ts b/frontend/dev/gstd-tic-tac-toe/src/components/layout/index.ts new file mode 100644 index 000000000..7202dcb89 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/index.ts @@ -0,0 +1,4 @@ +export { Header } from './header'; +export { MainLayout } from './main-layout'; +export { NotFound } from './not-found'; +export { NotAuthorized } from './not-authorized'; diff --git a/frontend/dev/gstd-tic-tac-toe/src/components/layout/main-layout.tsx b/frontend/dev/gstd-tic-tac-toe/src/components/layout/main-layout.tsx new file mode 100644 index 000000000..4fa43d4a7 --- /dev/null +++ b/frontend/dev/gstd-tic-tac-toe/src/components/layout/main-layout.tsx @@ -0,0 +1,29 @@ +import { PropsWithChildren } from 'react'; +import { Footer } from '@dapps-frontend/ui'; +import { ApiLoader, Header } from '@/components'; +import { useIsAppReady, useIsAppReadySync } from '@/app/hooks/use-is-app-ready'; +import { useAuthSync } from '@/features/auth/hooks'; +import { Container } from '../ui/container'; + +type MainLayoutProps = PropsWithChildren; + +export function MainLayout({ children }: MainLayoutProps) { + const { isAppReady } = useIsAppReady(); + + useIsAppReadySync(); + useAuthSync(); + + return ( + <> +
+
+ {!isAppReady && } + {isAppReady && children} +
+ + +