Skip to content

Commit

Permalink
show other users cursors
Browse files Browse the repository at this point in the history
fix issue with local changes being applied twice
  • Loading branch information
chrisrude committed Aug 15, 2023
1 parent 73c2ce9 commit 26e01b3
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 14 deletions.
29 changes: 29 additions & 0 deletions drumline-client/src/lib/components/ClueGrid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import {
clear,
clearSegment,
cursor,
markSegment,
memoizedGenerateGridAttributes,
set,
Expand Down Expand Up @@ -341,6 +342,16 @@
highlightBand = newBand;
}
cursorLocation = newLocation;
// send notification via update
setTimeout(() => {
// todo: just call this sync?
const cursorUpdate = cursor({
row: cursorLocation[0],
col: cursorLocation[1]
});
apply(cursorUpdate);
}, 10);
};
const onPath = (i: number, j: number) => {
Expand Down Expand Up @@ -420,6 +431,17 @@
const band_group = attributesAt([i, j]).band_group;
return !band_group.is_corner && band_group.side === side;
};
// true if another solver's cursor (other than ours) is at
// this location
const hasCursor = (i: number, j: number): boolean => {
for (const cursor of gameState.cursors.values()) {
if (cursor[0] === i && cursor[1] === j) {
return true;
}
}
return false;
};
</script>

<svelte:window
Expand Down Expand Up @@ -468,6 +490,7 @@
<div
class="letter center-cell"
class:selected={cursorLocation[0] === i && cursorLocation[1] === j}
class:hasCursor={gameState && hasCursor(i, j)}
/>
{:else}
<!-- this is ok because the keyboard events are handled higher up -->
Expand Down Expand Up @@ -506,6 +529,7 @@
class:bandSideS={isBandSide(i, j, 'bottom')}
class:bandCornerSW={isBandCorner(i, j, 'left')}
class:bandSideW={isBandSide(i, j, 'left')}
class:hasCursor={gameState && hasCursor(i, j)}
on:click={() => highlight([i, j], true)}
on:mousedown={(event) => onDragStart(event, i, j)}
on:mouseenter={(event) => onDragOver(event, i, j)}
Expand Down Expand Up @@ -709,6 +733,11 @@
color: white;
}
.grid .hasCursor {
outline: solid 4px var(--color-theme-1);
outline-offset: -8px;
}
.button-bar {
display: flex;
justify-content: flex-start;
Expand Down
3 changes: 2 additions & 1 deletion drumline-client/src/lib/network/networked_game_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ReconnectWsClient } from './reconnect_ws_client';
import { SolveClient, type SolveClientCallback } from './solve_client';
export { NetworkedGameState };

// todo: this class is poorly defined and probably shouldn't exist,
// but it's good enough for now. It should be folded into SolveClient.
class NetworkedGameState extends GameState {
readonly solve_client: SolveClient;
readonly ws_client: ReconnectWsClient;
Expand All @@ -19,7 +21,6 @@ class NetworkedGameState extends GameState {
this.solve_client.close();
};
apply_from_ui = (action: GameActions) => {
this.apply(action);
this.solve_client.apply(action);
};
}
15 changes: 10 additions & 5 deletions drumline-client/src/lib/network/solve_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ class SolveClient {
}
// we expect that we have a game state, as the rest of the
// app is asking us to update it.
storedGameState.update((game_state) => {
if (action.action !== 'cursor') {
const game_state = get(storedGameState);
if (null === game_state) {
throw new Error('No game state');
}
game_state.apply(action);
return game_state;
});
storedGameState.set(game_state);
}

// add to our pending actions. Do this first
// in case the server fails to respond, or the
Expand All @@ -93,8 +94,12 @@ class SolveClient {
return;
}

// add to our received actions
this._applied_actions.push(action);
// was this a cursor update? don't include it in our
// applied actions
if (action.action !== 'cursor') {
// add to our received actions
this._applied_actions.push(action);
}
this._handleActionCallback(action, this._pending_actions);

return;
Expand Down
29 changes: 23 additions & 6 deletions drumline-lib/lib/game_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
areActionsEqual,
clear,
clearSegment,
cursor,
joinPuzzle,
leavePuzzle,
markSegment,
Expand All @@ -13,9 +14,7 @@ export {
export type {
ClearActionType,
ClearSegmentActionType,
ClueListActionType,
CreatePuzzleActionType,
GameActionKinds,
ClueListActionType, CreatePuzzleActionType, CursorActionType, GameActionKinds,
GameActionType,
GameActions,
GridActionType,
Expand All @@ -35,6 +34,7 @@ type GameActions =
| LeavePuzzleActionType
| SetActionType
| ClearActionType
| CursorActionType
| MarkSegmentActionType
| ClearSegmentActionType;

Expand All @@ -43,6 +43,7 @@ const GAME_ACTIONS = [
'leavePuzzle',
'set',
'clear',
'cursor',
'markSegment',
'clearSegment'
] as const;
Expand Down Expand Up @@ -93,6 +94,10 @@ type ClearActionType = GridActionType & {
action: 'clear';
};

type CursorActionType = GridActionType & {
action: 'cursor';
}

// clue list actions
type MarkSegmentActionType = ClueListActionType & {
action: 'markSegment';
Expand Down Expand Up @@ -126,6 +131,16 @@ function clear(at: GridLocationType): ClearActionType {
};
}

function cursor(at: GridLocationType): CursorActionType {
return {
action: 'cursor',
user_id: '',
change_count: -1,
row: at.row,
col: at.col
};
}

function markSegment(
clue_list: ClueListIdentifier,
idx_cell_start: number,
Expand Down Expand Up @@ -184,17 +199,19 @@ function stringToAction(str: string): GameActions {
validateProperties(obj, [], ['action']);
switch (obj.action) {
case 'set':
strProperties.push('text');
// fall through
case 'clear':
case 'cursor':
numProperties.push('col');
numProperties.push('row');
strProperties.push('text');
break;
case 'markSegment':
numProperties.push('idx_cell_start');
numProperties.push('idx_cell_end');
case 'clearSegment':
numProperties.push('index');
strProperties.push('kind');
numProperties.push('idx_cell_start');
numProperties.push('idx_cell_end');
break;
case 'joinPuzzle':
strProperties.push('solve_id');
Expand Down
14 changes: 14 additions & 0 deletions drumline-lib/lib/game_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type { AnswerSegment, CellType, GameStateType, Grid };
import type {
ClearSegmentActionType,
ClueListActionType,
CursorActionType,
GameActionType,
GridActionType,
MarkSegmentActionType,
Expand Down Expand Up @@ -107,6 +108,7 @@ type GameStateType = {
class GameState implements GameStateType {
readonly band_answer_segments: AnswerSegments[];
readonly center: number;
readonly cursors: Map<string, [number, number]>;
readonly grid: Grid;
is_solved: boolean;
readonly row_answer_segments: AnswerSegments[];
Expand All @@ -118,6 +120,7 @@ class GameState implements GameStateType {

this.band_answer_segments = Array.from({ length: num_bands }, () => new AnswerSegments());
this.center = Math.floor(size / 2);
this.cursors = new Map();
this.grid = Array.from({ length: size }, () =>
Array.from({ length: size }, () => new Cell())
);
Expand All @@ -139,6 +142,10 @@ class GameState implements GameStateType {
const grid_action = action as GridActionType;
this.applyGridAction(grid_action);
break;
case 'cursor':
const cursor_action = action as CursorActionType;
this.applyCursorAction(cursor_action);
break;
}
};

Expand Down Expand Up @@ -178,6 +185,13 @@ class GameState implements GameStateType {
}
};

applyCursorAction = (cursor_action: CursorActionType): void => {
this.cursors.set(
cursor_action.user_id,
[cursor_action.row, cursor_action.col]
);
}

getAnswerSegments = (kind: ClueListKind, index: number): AnswerSegments => {
const is_row = 'row' === kind;
if (!is_row && 'band' !== kind) {
Expand Down
10 changes: 10 additions & 0 deletions drumline-lib/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@ const validateProperties = (
}
}
}
for (const property of numProperties) {
if (!obj.hasOwnProperty(property)) {
throw new Error(`Invalid action: ${property} is missing`);
}
}
for (const property of strProperties) {
if (!obj.hasOwnProperty(property)) {
throw new Error(`Invalid action: ${property} is missing`);
}
}
};
14 changes: 13 additions & 1 deletion drumline-server/src/websockets/client_state_store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserId } from "@chrisrude/drumline-lib";
import { CursorActionType, UserId, actionToString } from "@chrisrude/drumline-lib";
import { WebSocket } from 'ws';

export { ClientStateStore };
Expand Down Expand Up @@ -37,10 +37,12 @@ class ClientsBySolve {
class ClientState {
solve_id: string | null;
user_id: UserId | null;
last_cursor_update: string | null;

constructor() {
this.solve_id = null;
this.user_id = null;
this.last_cursor_update = null;
}
}

Expand Down Expand Up @@ -104,4 +106,14 @@ class ClientStateStore {
get_clients_for_solve = (solve_id: string): Set<WebSocket> => {
return this.clients_by_solve.get(solve_id);
}

update_cursor = (ws: WebSocket, cursor: CursorActionType): void => {
console.log('update_cursor', cursor);
const str = actionToString(cursor);
this.get_client_state(ws).last_cursor_update = str;
}

get_cursor = (ws: WebSocket): string | null => {
return this.get_client_state(ws).last_cursor_update;
}
}
19 changes: 18 additions & 1 deletion drumline-server/src/websockets/echo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CursorActionType,
GameActions,
JoinPuzzleActionType,
LeavePuzzleActionType,
Expand Down Expand Up @@ -105,6 +106,15 @@ class EchoServer extends WebSocketServer {
const strData = old_updates[i];
ws.send(strData, { binary: false });
}

// finally, send all cursor locations to the client
const other_clients = this._client_state.get_clients_for_solve(solve_id);
other_clients.forEach((client: WebSocket) => {
const cursor = this._client_state.get_cursor(client);
if (cursor && client != ws) {
ws.send(cursor, { binary: false });
}
});
}

on_leave_puzzle = (ws: WebSocket, _leavePuzzle: LeavePuzzleActionType) => {
Expand All @@ -131,7 +141,12 @@ class EchoServer extends WebSocketServer {
// change from private to public uuid
action.user_id = client_state.user_id.public_uuid;

await this._store.add_solve_action(solve_id, action);
// add to store, unless it's a cursor update
if (action.action === 'cursor') {
this._client_state.update_cursor(ws, action as CursorActionType);
} else {
await this._store.add_solve_action(solve_id, action);
}

// find clients to update
const solve_clients = this._client_state.get_clients_for_solve(solve_id);
Expand All @@ -147,6 +162,8 @@ class EchoServer extends WebSocketServer {
on_incoming_message = async (ws: WebSocket, data: RawData, isBinary: boolean) => {
const t0 = performance.now();

// todo: rate-limit incoming messages?

// decode message
if (isBinary) {
console.error('Received binary message, ignoring');
Expand Down

0 comments on commit 26e01b3

Please sign in to comment.