fuzzyworld

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

Branch react · default branch · all branches

remote-game-2.ts

Blame: the commit that last changed each line · file · history

41db8b58 2022-03-24 Pavel Safronov1import Peer, { PeerJSOption } from 'peerjs';
2import { showModal } from '../utils/modal';
3import { useSessionStorage } from '../utils/storage';
4import { showSnackbar } from "../utils/snackbar";
5import { Game, Move, MoveResult } from './game';
6import { Coord } from './coord';
7import { generateHash } from '../utils/hashing';
8import { delay } from '../utils/delay';
9import { copyToClipboard } from '../utils/clipboard';
10import { detect } from 'detect-browser';
11
12interface LocalData {
13 id?: string;
14 peerId?: string;
15}
16interface Pos {
17 x: number;
18 y: number;
19}
20interface PeerData {
21 mousePos?: Pos;
22 move?: Move;
23}
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: string, text: string) => {
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 Game {
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 = 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: NodeJS.Timeout | undefined = undefined;
125 conn.on('data', (data: string) => {
126 if (!game) return;
127
128 const peerData = JSON.parse(data) as PeerData;
129 const { mousePos, move } = peerData;
130
131 if (mousePos) {
132 const isValidPos = mousePos.x !== -1 && mousePos.y !== -1;
133 if (isValidPos) {
134 const xRatio = mousePos.x;
135 const yRatio = mousePos.y;
136 const xTable = xRatio * table.clientWidth;
137 const yTable = yRatio * table.clientHeight;
138 const x = table.offsetLeft + xTable;
139 const y = table.offsetTop + yTable;
140 const style = `left:${x - otherMouseElement.clientWidth / 2}px;top:${y}px;display:block;`;
141 otherMouseElement.setAttribute('style', style);
142 if (lastTimerId !== undefined) {
143 clearTimeout(lastTimerId);
144 }
145 lastTimerId = setTimeout(() => {
146 otherMouseElement.setAttribute('style', '');
147 lastTimerId = undefined;
148 }, 3000);
149 } else if (lastTimerId !== undefined) {
150 otherMouseElement.setAttribute('style', '');
151 }
152 }
153
154 const isWaitingForRemoteMove = peerId === game.currentPlayerId;
155 if (isWaitingForRemoteMove && move) {
156 const isMoveLegal = game.getPiece(move.source) === game.currentPlayerId && game.getPiece(move.dest) === null;
157 if (isMoveLegal) {
158 game.makeMove(move.source, move.dest);
159 } else {
160 console.log({ isMoveLegal, move });
161 }
162 }
163 });
164 conn.on('error', (err) => {
165 showMessageAndLeave(`Connection error`, err);
166 });
167 conn.on('close', () => {
168 showMessageAndLeave(`Connection closed`, 'Connection closed for some reason')
169 });
170 };
171
172 const { host, protocol, pathname, search } = location;
173 const queryParams = new URLSearchParams(search);
174
175 const peer = id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
176 window.onunload = () => peer.destroy();
177 peer.on('connection', (conn) => handleConn(conn));
178 peer.on('error', async (err) => {
179 await showMessageAndLeave('Peer error encountered', err);
180 });
181 peer.on('open', (trueId) => {
182 id = trueId;
183 peerId = queryParams.get('peerId') ?? peerId;
184 setData({ id, peerId });
185
186 if (peerId) {
187 console.log(`${id} connecting to peer "${peerId}"`);
188 const conn = peer.connect(peerId);
189 handleConn(conn);
190 } else {
191 queryParams.set('peerId', id);
192 const newSearch = queryParams.toString();
193 const newUrl = `${protocol}//${host}${pathname}?${newSearch}`;
194 shareLinkButton.onclick = () => {
195 showSnackbar('Url copied to clipboard');
196 copyToClipboard(newUrl);
197 };
198 shareContainer.classList.remove('invisible');
199 }
200 });
201 peer.on('disconnected', async () => {
202 await showMessageAndLeave('Peer disconnected', 'For some reason')
203 });
204 while (game === undefined) {
205 console.log('Waiting to connect to peer');
206 await delay(500);
207 }
208 return game;
209 }
210}
211
212const isBrowserSupported = () => {
213 const browser = detect();
214 switch (browser && browser.name) {
215 case 'chrome':
216 case 'firefox':
217 return true;
218 default:
219 return false;
220 }
221}
222export const playGame = async () => {
223 if (isBrowserSupported()) {
224 await PeerGame.createPeerGame();
225 } else {
226 showMessageAndLeave(
227 'Unsupported browser',
228 '<p>Your current browser is not yet supported.</p>' +
229 '<p>This PeerJs issue is tracking support in Microsoft Edge and Safari: </p>' +
230 '<p><a href="https://github.com/peers/peerjs/issues/899" target="_blank">https://github.com/peers/peerjs/issues/899</a></p>')
231 }
232};