fuzzyworld

dao-game / files / src / game / remote-game.ts

remote-game.ts

9.1 KB · history · blame

1import Peer, { PeerJSOption } from 'peerjs';
2import { LocalGame } from './local-game';
3import { showModal } from '../utils/modal';
4import { useSessionStorage } from '../utils/storage';
5import { showSnackbar } from "../utils/snackbar";
6import { Move, MoveResult } from './game';
7import { Coord } from './coord';
8import { generateHash } from '../utils/hashing';
9import { delay } from '../utils/delay';
10import { copyToClipboard } from '../utils/clipboard';
11import { detect } from 'detect-browser';
12
13interface LocalData {
14 id: string;
15 peerId: string;
16}
17interface Pos {
18 x: number;
19 y: number;
20}
21interface PeerData {
22 mousePos?: Pos;
23 move?: Move;
24}
25const peerOptions: PeerJSOption = { debug: 0 };
26const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game', { id: undefined, peerId: undefined });
27const otherMouseElement = document.getElementById('other-mouse');
28const shareContainer = document.getElementById('share-container');
29const shareLinkButton = document.getElementById('share-link');
30const minString = (a: string, b: string): string => (a < b ? a : b);
31const maxString = (a: string, b: string): string => (a > b ? a : b);
32const getFirstPlayerId = (playerId1: string, playerId2: string) => {
33 const hash1 = generateHash(playerId1);
34 const hash2 = generateHash(playerId2);
35 const prod = hash1 * hash2;
36 const isEven = prod % 2 == 0;
37 const id = isEven ? minString(playerId1, playerId2) : maxString(playerId1, playerId2);
38 return id;
39};
40const dropFileExtension = (path: string) => {
41 const lastSlashIndex = path.lastIndexOf('/');
42 const dirName = path.substring(0, lastSlashIndex);
43 return dirName;
44}
45const showMessageAndLeave = async (title, text) => {
46 await showModal({ title, text, willBackgroundCancel: false });
47 sessionStorage.clear();
48 const { protocol, host, pathname } = location;
49 const dir = dropFileExtension(pathname);
50 const newUrl = `${protocol}//${host}${dir}`
51 document.location = newUrl;
52}
53
54export class PeerGame extends LocalGame {
55 public conn: Peer.DataConnection;
56 public selfId: string;
57 public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string, selfId: string, conn: Peer.DataConnection) {
58 super(player1Id, player1Name, player2Id, player2Name);
59 this.selfId = selfId;
60 this.conn = conn;
61 }
62 public override setActiveUi(): void {
63 const { currentPlayerId, selfId } = this;
64
65 const isLocalTurn = currentPlayerId === selfId;
66 if (isLocalTurn) {
67 super.setActiveUi();
68 }
69 }
70 public override makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
71 const result = super.makeMove(source, dest);
72 const peerData: PeerData = { move: { source, dest, playerId: this.currentPlayerId } };
73 this.conn.send(JSON.stringify(peerData));
74 return result;
75 }
76 public override async handleMoveResult(result: MoveResult): Promise<void> {
77 const { isTerminal, message } = result;
78 if (isTerminal) {
79 await showMessageAndLeave('Game Over', message);
80 } else {
81 this.resetUi();
82 this.setActiveUi();
83 }
84 }
85
86 public static async createPeerGame(): Promise<PeerGame> {
87 let { id, peerId } = getData();
88 const main = document.getElementsByTagName('main')[0];
89 let game: PeerGame = undefined;
90
91 const handleConn = async (conn: Peer.DataConnection) => {
92 shareContainer.classList.add('invisible');
93
94 await new Promise<void>((resolve) => {
95 conn.on('open', () => resolve());
96 });
97
98 id = peer.id;
99 peerId = conn.peer;
100 setData({ id, peerId });
101
102 const isLocalP1 = getFirstPlayerId(id, peerId) === id;
103 game = isLocalP1 ?
104 new PeerGame(id, "Local", peerId, "Remote", id, conn) :
105 new PeerGame(peerId, "Remote", id, "Local", id, conn);
106 game.resetUi();
107 game.setActiveUi();
108 main.classList.remove('invisible');
109
110 const table = document.getElementById('table');
111 table.addEventListener('mousemove', (ev: MouseEvent) => {
112 const xTable = ev.clientX - table.offsetLeft;
113 const yTable = ev.clientY - table.offsetTop;
114 const xRatio = xTable / table.clientWidth;
115 const yRatio = yTable / table.clientHeight;
116 const peerData: PeerData = { mousePos: { x: xRatio, y: yRatio } };
117 conn.send(JSON.stringify(peerData));
118 });
119 table.addEventListener('mouseleave', () => {
120 const peerData: PeerData = { mousePos: { x: -1, y: -1 } };
121 conn.send(JSON.stringify(peerData));
122 });
123
124 let lastTimerId = undefined;
125 conn.on('data', (data: string) => {
126 const peerData = JSON.parse(data) as PeerData;
127 const { mousePos, move } = peerData;
128
129 if (mousePos) {
130 const isValidPos = mousePos.x !== -1 && mousePos.y !== -1;
131 if (isValidPos) {
132 const xRatio = mousePos.x;
133 const yRatio = mousePos.y;
134 const xTable = xRatio * table.clientWidth;
135 const yTable = yRatio * table.clientHeight;
136 const x = table.offsetLeft + xTable;
137 const y = table.offsetTop + yTable;
138 const style = `left:${x - otherMouseElement.clientWidth / 2}px;top:${y}px;display:block;`;
139 otherMouseElement.setAttribute('style', style);
140 if (lastTimerId !== undefined) {
141 clearTimeout(lastTimerId);
142 }
143 lastTimerId = setTimeout(() => {
144 otherMouseElement.setAttribute('style', '');
145 lastTimerId = undefined;
146 }, 3000);
147 } else if (lastTimerId !== undefined) {
148 otherMouseElement.setAttribute('style', '');
149 }
150 }
151
152 const isWaitingForRemoteMove = peerId === game.currentPlayerId;
153 if (isWaitingForRemoteMove && move) {
154 const isMoveLegal = game.getPiece(move.source) === game.currentPlayerId && game.getPiece(move.dest) === null;
155 if (isMoveLegal) {
156 game.makeMove(move.source, move.dest);
157 } else {
158 console.log({ isMoveLegal, move });
159 }
160 }
161 });
162 conn.on('error', (err) => {
163 showMessageAndLeave(`Connection error`, err);
164 });
165 conn.on('close', () => {
166 showMessageAndLeave(`Connection closed`, 'Connection closed for some reason')
167 });
168 };
169
170 const { host, protocol, pathname, search } = location;
171 const queryParams = new URLSearchParams(search);
172
173 const peer = id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
174 window.onunload = () => peer.destroy();
175 peer.on('connection', (conn) => handleConn(conn));
176 peer.on('error', async (err) => {
177 await showMessageAndLeave('Peer error encountered', err);
178 });
179 peer.on('open', (trueId) => {
180 id = trueId;
181 peerId = queryParams.get('peerId') ?? peerId;
182 setData({ id, peerId });
183
184 if (peerId) {
185 console.log(`${id} connecting to peer "${peerId}"`);
186 const conn = peer.connect(peerId);
187 handleConn(conn);
188 } else {
189 queryParams.set('peerId', id);
190 const newSearch = queryParams.toString();
191 const newUrl = `${protocol}//${host}${pathname}?${newSearch}`;
192 shareLinkButton.onclick = () => {
193 showSnackbar('Url copied to clipboard');
194 copyToClipboard(newUrl);
195 };
196 shareContainer.classList.remove('invisible');
197 }
198 });
199 peer.on('disconnected', async () => {
200 await showMessageAndLeave('Peer disconnected', 'For some reason')
201 });
202 while (game === undefined) {
203 console.log('Waiting to connect to peer');
204 await delay(500);
205 }
206 return game;
207 }
208}
209
210const isBrowserSupported = () => {
211 const browser = detect();
212 switch (browser && browser.name) {
213 case 'chrome':
214 case 'firefox':
215 return true;
216 default:
217 return false;
218 }
219}
220export const playGame = async () => {
221 if (isBrowserSupported()) {
222 await PeerGame.createPeerGame();
223 } else {
224 showMessageAndLeave(
225 'Unsupported browser',
226 '<p>Your current browser is not yet supported.</p>' +
227 '<p>This PeerJs issue is tracking support in Microsoft Edge and Safari: </p>' +
228 '<p><a href="https://github.com/peers/peerjs/issues/899" target="_blank">https://github.com/peers/peerjs/issues/899</a></p>')
229 }
230};