This repository was archived by the owner on Dec 25, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathGame.tsx
More file actions
138 lines (114 loc) · 3.7 KB
/
Copy pathGame.tsx
File metadata and controls
138 lines (114 loc) · 3.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { GraphQLClient } from 'graphql-request';
import { GameBoard } from './GameBoard';
import { fetchBoard, updateBoardField } from './board';
import { loadKeyPair, loadLastMove, storeLastMove } from './storage';
import { publicKeyToAnimal } from './animals';
import type { Configuration } from './types';
type Props = {
config: Configuration;
};
export const Game: React.FC<Props> = ({ config }) => {
const keyPair = useMemo(() => {
return loadKeyPair();
}, []);
const animal = useMemo(() => {
return publicKeyToAnimal(keyPair.publicKey());
}, [keyPair]);
const client = useMemo(() => {
return new GraphQLClient(config.endpoint);
}, [config.endpoint]);
// Latest document view id of the game board
const [viewId, setViewId] = useState<string>();
// State of the game board
const [fields, setFields] = useState<string[]>();
// Remember document view id of our last move, this helps us to detect if we
// are the last player who updated the board
const [lastMove, setLastMove] = useState<string | null>(() => {
return loadLastMove();
});
// Remember what the last update was, this helps us to detect if the node
// could send us something new
const [lastUpdate, setLastUpdate] = useState<string>();
// Block the user interface when we just made a move and we're waiting for
// updates from the node
const [ready, setReady] = useState(true);
const updateBoard = useCallback(async () => {
const board = await fetchBoard(
client,
config.schemaId,
config.documentId,
config.boardSize,
);
// Make sure to only affect the board state when we really have something
// new for the client. This prevents overriding temporarily set local-only
// state.
if (lastUpdate !== board.viewId) {
setViewId(board.viewId);
setFields(board.fields);
setLastUpdate(board.viewId);
setReady(true);
}
}, [
client,
config.boardSize,
config.documentId,
config.schemaId,
lastUpdate,
]);
const onSetField = useCallback(
async (fieldIndex: number) => {
// Do not do allow making a move when we're waiting for the latest state
// from the node
if (!viewId || !ready) {
return;
}
// Do not allow making a move when player already did it one round ago
if (lastMove === viewId) {
return;
}
setReady(false);
// Apply update locally first to see the changes directly
setFields((value) => {
if (!value) {
return;
}
value[fieldIndex - 1] = animal;
return [...value];
});
// Send update to node.
//
// The method gives us back the "latest" view id, that is, we assume that
// our last write is now the latest edge of the operation graph. But who
// knows, maybe some concurrent write by someone decided something else!
const latestViewId = await updateBoardField(
client,
keyPair,
config.schemaId,
viewId,
fieldIndex,
animal,
);
// Set and persist last move so we remember it when we come back later
setLastMove(latestViewId);
storeLastMove(latestViewId);
},
[viewId, client, lastMove, keyPair, config, animal, ready],
);
useEffect(() => {
const interval = window.setInterval(() => {
updateBoard();
}, config.updateIntervalMs);
updateBoard();
return () => {
window.clearInterval(interval);
};
}, [client, updateBoard, config.updateIntervalMs]);
return (
<>
{fields && (
<GameBoard fields={fields} animal={animal} onSetField={onSetField} />
)}
</>
);
};