fuzzyworld

dao-game / peerjs-browser-bug / files / src / remote-game.ts

Branch peerjs-browser-bug · default branch · all branches

remote-game.ts

9.8 KB · history · blame

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