dao-game / commit
Mouse position transmission works, but ATM we're always starting anew.
.prettierrc | 7 ++ public/index.html | 111 ++++++++++++------------ public/online.html | 10 +-- public/styles.css | 250 +++++++++++++++++++++++++++++------------------------ src/remote-game.ts | 136 +++++++++++++++++++++-------- src/storage.ts | 41 +++++---- 6 files changed, 329 insertions(+), 226 deletions(-) diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..91ef31e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "trailingComma": "es5", + "singleQuote": true +} diff --git a/public/index.html b/public/index.html index 766b639..2bcc389 100644 --- a/public/index.html +++ b/public/index.html @@ -1,62 +1,67 @@ <!DOCTYPE html> <html lang="en"> -<head> - <meta charset="utf-8" /> - <title>Dao Game</title> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="yin-yang.svg" type="image/svg+xml" /> - <link rel="stylesheet" href="styles.css" /> - <script defer src="bundle.js"></script> -</head> - -<body> - <div class="invisible"> - <p class="piece p1-piece" id="default-p1-piece"></p> - <p class="piece p2-piece" id="default-p2-piece"></p> - </div> - <main> - <div class="controls"> - <span class="p1-turn-text"> Red's turn </span> - <span class="divider">|</span> - <span class="p2-turn-text"> Blue's turn </span> + <head> + <meta charset="utf-8" /> + <title>Dao Game</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="yin-yang.svg" type="image/svg+xml" /> + <link rel="stylesheet" href="styles.css" /> + <script defer src="bundle.js"></script> + </head> + + <body> + <div class="invisible"> + <p class="piece p1-piece" id="default-p1-piece"></p> + <p class="piece p2-piece" id="default-p2-piece"></p> + </div> + <main> + <div class="controls"> + <span class="p1-turn-text"> Red's turn </span> + <span class="divider">|</span> + <span class="p2-turn-text"> Blue's turn </span> + </div> + + <table id="table"></table> + + <div class="controls"> + <button type="button" class="button" id="undo-button"> + Undo + </button> + <span class="divider">|</span> + <button type="button" class="button" id="reset-button"> + Reset + </button> + </div> + </main> + <hr /> + + <div class="title"> + <p>Board game + <a href="https://en.wikipedia.org/wiki/Dao_(game)" + target="_blank" rel="noopener">Dao</a>. + </p> </div> - <table id="table"></table> + <div class="info"> + The game consists of only one simple rule and three ways to win. + Pieces move in a straight line, including diagonally, as far as + possible with no jumps. The three ways to win are arranging your + pieces in a straight line on the board (not diagonally), occupying + the four corners, or forming a 2x2 square anywhere on the board. + </div> + <hr /> - <div class="controls"> - <button type="button" class="button" id="undo-button"> - Undo - </button> - <span class="divider">|</span> - <button type="button" class="button" id="reset-button"> - Reset - </button> + <div class="info"> + <p> + Here is the + <a href="https://gitlab.com/fuzzy-rp/dao-game" target="_blank" + rel="noopener">source code</a> + </p> </div> - </main> - <hr /> - - <div class="title"> - <p>Board game - <a href="https://en.wikipedia.org/wiki/Dao_(game)" target="_blank" rel="noopener">Dao</a>. - </p> - </div> - - <div class="info"> - The game consists of only one simple rule and three ways to win. - Pieces move in a straight line, including diagonally, as far as - possible with no jumps. The three ways to win are arranging your - pieces in a straight line on the board (not diagonally), occupying - the four corners, or forming a 2x2 square anywhere on the board. - </div> - <hr /> - - <div class="info"> - <p> - Here is the - <a href="https://gitlab.com/fuzzy-rp/dao-game" target="_blank" rel="noopener">source code</a> - </p> - </div> -</body> + + <canvas id="back-canvas"></canvas> + + </body> </html> \ No newline at end of file diff --git a/public/online.html b/public/online.html index 1b5fc5d..ad554d7 100644 --- a/public/online.html +++ b/public/online.html @@ -36,9 +36,7 @@ <a href="https://en.wikipedia.org/wiki/Dao_(game)" target="_blank" - rel="noopener" - >Dao</a - >. + rel="noopener">Dao</a>. </p> </div> @@ -57,10 +55,10 @@ <a href="https://gitlab.com/fuzzy-rp/dao-game" target="_blank" - rel="noopener" - >source code</a - > + rel="noopener">source code</a> </p> </div> + + <canvas id="back-canvas"></canvas> </body> </html> diff --git a/public/styles.css b/public/styles.css index 021b0df..6f25d87 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,205 +1,225 @@ :root { - --default-size: 5px; - --active-size: 40px; - --cell-size: 80px; - /* --p1-color: crimson; + --default-size: 5px; + --active-size: 40px; + --cell-size: 80px; + /* --p1-color: crimson; --p2-color: mediumblue; */ } * { - margin: auto; + margin: auto; } main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - /* height: 75vh; */ + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + /* height: 75vh; */ } body { - /* overflow: hidden; */ - margin: 0px; - background-color: white; + /* overflow: hidden; */ + margin: 0px; + background-color: white; } div { - display: flex; - justify-content: space-between; - /* display: inline; + display: flex; + justify-content: space-between; + /* display: inline; flex: 1; */ } +#back-canvas { + position: absolute; + top: 0px; + left: 0px; + z-index: -1000; + pointer-events: none; +} + .invisible { - display: none; + display: none; } .title { - text-align: center; - font-family: "open sans"; - font-size: 40px; + text-align: center; + font-family: 'open sans'; + font-size: 40px; } /* tile */ td { - width: var(--cell-size); - height: var(--cell-size); - text-align: center; - border: 2px transparent solid; + width: var(--cell-size); + height: var(--cell-size); + text-align: center; + border: 2px transparent solid; } .controls { - font-size: 90px; - margin-left: 15px; - margin-right: 15px; - font-family: "open sans"; - font-size: 55px; - border-radius: 10px; - padding: 10px; + font-size: 90px; + margin-left: 15px; + margin-right: 15px; + font-family: 'open sans'; + font-size: 55px; + border-radius: 10px; + padding: 10px; } .divider { - font-size: 90px; - margin-left: 15px; - margin-right: 15px; + font-size: 90px; + margin-left: 15px; + margin-right: 15px; } -.p1-turn-text, .p2-turn-text { - text-align: center; - color: lightgray; - background-color: white; - /* border: 1px solid black; */ - border-radius: 10px; - padding: 10px; - -ms-user-select: none; - -webkit-user-select: none; - user-select: none; +.p1-turn-text, +.p2-turn-text { + text-align: center; + color: lightgray; + background-color: white; + /* border: 1px solid black; */ + border-radius: 10px; + padding: 10px; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; } .p1-turn-text.selected-player-text { - background-color: red; + background-color: red; } .p2-turn-text.selected-player-text { - background-color: blue; + background-color: blue; } .selected-player-text { - color: white; + color: white; } .button { - border-radius: 10px; - padding: 10px; - background-color: orange; - font-size: 60px; - border-color: transparent; - transition: .2s all; + border-radius: 10px; + padding: 10px; + background-color: orange; + font-size: 60px; + border-color: transparent; + transition: 0.2s all; } .button:active { - transform: scale(0.98); - /* Scaling button to 0.98 to its original size */ - box-shadow: 3px 2px 22px 1px rgba(0, 0, 0, 0.24); - /* Lowering the shadow */ + transform: scale(0.98); + /* Scaling button to 0.98 to its original size */ + box-shadow: 3px 2px 22px 1px rgba(0, 0, 0, 0.24); + /* Lowering the shadow */ } .white { - background-color: orange; + background-color: orange; } .black { - background-color: wheat; + background-color: wheat; } .piece { - width: var(--default-size); - height: var(--default-size); - padding: 10px; - border-radius: 50px; - animation: activeToDisabled 1s 1; - /* background-image: url(yin-yang.svg); */ + width: var(--default-size); + height: var(--default-size); + padding: 10px; + border-radius: 50px; + animation: activeToDisabled 1s 1; + /* background-image: url(yin-yang.svg); */ } .p1-piece { - background-color: crimson; - /* background-image: url(yin-yang.svg); */ + background-color: crimson; + /* background-image: url(yin-yang.svg); */ } .p2-piece { - background-color: mediumblue; - /* background-image: url(yin-yang.svg); */ + background-color: mediumblue; + /* background-image: url(yin-yang.svg); */ } .active-player-piece { - width: var(--active-size); - height: var(--active-size); - animation: disabledToActive 1s 1; + width: var(--active-size); + height: var(--active-size); + animation: disabledToActive 1s 1; } .selected-piece { - animation: activePulsing 1s infinite; + animation: activePulsing 1s infinite; } .info { - text-align: center; - font-family: "open sans"; - font-size: 20px; + text-align: center; + font-family: 'open sans'; + font-size: 20px; } .possible-move { - animation: blinkingText 1s infinite; - border-color: black; - filter: drop-shadow(0px 0px 5px black); + animation: blinkingText 1s infinite; + border-color: black; + filter: drop-shadow(0px 0px 5px black); } @keyframes blinkingText { - 0%{opacity: 1;} - 50%{opacity: .6;} - 100%{opacity: 1;} + 0% { + opacity: 1; + } + 50% { + opacity: 0.6; + } + 100% { + opacity: 1; + } } @keyframes fade-in { - 0%{opacity: 0;} - 50%{opacity: .5;} - 100%{opacity: 1;} + 0% { + opacity: 0; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } } @keyframes disabledToActive { - from { - width: var(--default-size); - height: var(--default-size); - } - to { - width: var(--active-size); - height: var(--active-size); - } + from { + width: var(--default-size); + height: var(--default-size); + } + to { + width: var(--active-size); + height: var(--active-size); + } } @keyframes activeToDisabled { - from { - width: var(--active-size); - height: var(--active-size); - } - to { - width: var(--default-size); - height: var(--default-size); - } + from { + width: var(--active-size); + height: var(--active-size); + } + to { + width: var(--default-size); + height: var(--default-size); + } } - /* .pulsing { animation: pulsingSize 1.5s infinite; } */ @keyframes activePulsing { - from { - width: var(--active-size); - height: var(--active-size); - } - 50% { - width: 25px; - height: 25px; - } - to { - width: var(--active-size); - height: var(--active-size); - } + from { + width: var(--active-size); + height: var(--active-size); + } + 50% { + width: 25px; + height: 25px; + } + to { + width: var(--active-size); + height: var(--active-size); + } } /* breaks */ @@ -291,4 +311,4 @@ td { .black-turn-text { font-size: 35px; } -} */ \ No newline at end of file +} */ diff --git a/src/remote-game.ts b/src/remote-game.ts index 90f579e..fdb631d 100644 --- a/src/remote-game.ts +++ b/src/remote-game.ts @@ -1,56 +1,120 @@ -import Peer from 'peerjs' +import Peer, { PeerJSOption } from 'peerjs'; import { LocalGame } from './local-game'; import { useLocalStorage } from './storage'; interface LocalData { - selfPeerId: string; - otherPeerIds: string[] + id: string; + peerId: string; } interface Pos { - x: number; - y: number; + x: number; + y: number; } interface PeerData { - mousePos: Pos; + mousePos: Pos; } // { // selfPeerId: `dao-game-${prompt('Name?', 'Red') ?? 'Red'}`, // otherPeerIds: [] // } -const { getter: getData, setter: setData } = useLocalStorage<LocalData>('dao-game'); +const { getter: getData, setter: setData } = useLocalStorage<LocalData>( + 'dao-game-v2022-03-18-1412', + { id: undefined, peerId: undefined } +); export class PeerGame extends LocalGame { - peerMousePos: Pos; - - public constructor() { - super('Local', 'Remote'); - - const { selfPeerId, otherPeerIds } = getData(); - const peer = new Peer(selfPeerId); - peer.on('connection',) - - if (otherPeerIds.length > 0) { - const otherPeer = otherPeerIds[0]; - const conn = peer.connect(otherPeer); - conn.on('data', (data: string) => { - const peerData = JSON.parse(data) as PeerData; - if (peerData && peerData.mousePos) { - this.peerMousePos = peerData.mousePos; - } - }); - const main = document.getElementsByTagName('main')[0]; - main.addEventListener('mousemove', (ev: MouseEvent) => { - const peerData: PeerData = { mousePos: { x: ev.x, y: ev.y } }; - const text = JSON.stringify(peerData); - conn.send(text); - }); + peerMousePos: Pos; + + public constructor() { + super('Local', 'Remote'); + + let { id, peerId } = getData(); + const main = document.getElementsByTagName('body')[0]; + const backCanvas = document.getElementById( + 'back-canvas' + ) as HTMLCanvasElement; + + const handleConn = (conn: Peer.DataConnection) => { + id = peer.id; + peerId = conn.peer; + setData({ id, peerId }); + + console.log('Adding event listener to main'); + main.addEventListener('mousemove', (ev: MouseEvent) => { + const peerData: PeerData = { mousePos: { x: ev.x, y: ev.y } }; + const text = JSON.stringify(peerData); + // console.log(`Sending data to ${conn.peer}: ${text}`); + conn.send(text); + }); + + conn.on('open', () => { + console.log(`Connection to ${conn.peer} opened`); + conn.send('{}'); + }); + conn.on('data', (data: string) => { + // console.log(`Receiving data from ${conn.peer}: ${data}`); + const peerData = JSON.parse(data) as PeerData; + if (peerData && peerData.mousePos) { + this.peerMousePos = peerData.mousePos; } - } + }); + conn.send('hi!'); + }; + + const { host, protocol, pathname, search } = location; + const queryParams = new URLSearchParams(search); + + console.log(`Creating peer connection for ${id}`); + const peerOptions: PeerJSOption = { debug: 0 }; + const peer = + id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions); + id = peer.id; + window.onunload = () => peer.destroy(); + // document.addEventListener('close', () => peer.destroy()); + peer.on('connection', (conn) => { + console.log(`Connection established to ${conn.peer}`); + handleConn(conn); + }); + peer.on('open', (trueId) => { + id = trueId; + peerId = queryParams.get('peerId') ?? peerId; + setData({ id, peerId }); + if (peerId) { + console.log(`${id} connecting to peer "${peerId}"`); + const conn = peer.connect(peerId); + handleConn(conn); + } else { + queryParams.set('peerId', id); + const newSearch = queryParams.toString(); + const newUrl = `microsoft-edge:${protocol}//${host}${pathname}?${newSearch}`; + const nw = window.open(newUrl, '_blank'); + } + }); + peer.on('error', (err) => console.error({ err })); + + const animate = () => { + const { peerMousePos } = this; + if (peerMousePos !== undefined) { + // console.log({ peerMousePos: this.peerMousePos }); + // this.peerMousePos = undefined; + backCanvas.width = window.innerWidth; + backCanvas.height = window.innerHeight; + const ctx = backCanvas.getContext('2d'); + ctx.fillStyle = 'green'; + ctx.beginPath(); + ctx.ellipse(peerMousePos.x, peerMousePos.y, 50, 50, 0, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + } + requestAnimationFrame(() => animate()); + }; + requestAnimationFrame(() => animate()); + } } export const playGame = () => { - const pg = new PeerGame(); - pg.init(); - pg.nextMove(); -} \ No newline at end of file + const pg = new PeerGame(); + pg.init(); + pg.nextMove(); +}; diff --git a/src/storage.ts b/src/storage.ts index c45bea2..7e48952 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,18 +1,27 @@ export interface StorageInterface<T> { - getter: () => T, - setter: (data: T) => void + getter: () => T; + setter: (data: T) => void; } -export const useLocalStorage = <T>(key: string, defaultData?: T): StorageInterface<T> => { - return { - getter: () => { - let item = localStorage.getItem(key); - if (item === null && defaultData !== undefined) { - item = JSON.stringify(defaultData); - localStorage.setItem(key, item); - } - const data = JSON.parse(item) as T; - return data; - }, - setter: (data: T) => localStorage.setItem(key, JSON.stringify(data as T)) - }; -} \ No newline at end of file +export const useLocalStorage = <T>( + key: string, + defaultData?: T +): StorageInterface<T> => { + return { + getter: () => { + let item = sessionStorage.getItem(key); + if (item === null && defaultData !== undefined) { + item = JSON.stringify(defaultData); + sessionStorage.setItem(key, item); + } + const data = JSON.parse(item) as T; + return data; + }, + setter: (data: T) => { + const text = JSON.stringify(data as T); + console.log(`Saving data to local storage`); + console.log(`Key=${key}`); + console.log(`Text=${text}`); + sessionStorage.setItem(key, text); + }, + }; +};