-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathlocal.js
679 lines (554 loc) · 24.6 KB
/
local.js
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
// Start a WebSocket connection with the server using SocketIO
var socket = io();
/* ------------------------------------------------------------
GAME STATE:
{
nextTurnTimestamp,
turnIndex,
currentGist: {id, url, owner},
players:
[
{id, login,avatar_url}, { ... }, { ... }, ...
]
}
-------------------------------------------------------------- */
let gameState = {
nextTurnTimestamp: null,
turnIndex: 0,
currentGist: null,
players: []
};
// SAVING LOCAL STATE -- GLOBAL VARS (ugh)
var animationId;
// Later this shouldn't be hard-coded:
const turnDuration = 60000;
// Meant to be temporary:
var currentAccessToken;
/* -------------------------------------------------
LIST OF IDs, DYNAMIC ELEMENTS:
- loginmodal container for login screen
- loginbutton <button> to log in
- editor <textarea> collab code editor
- timeleft <p> shows minutes:seconds
- currentturn name of current player
- nextturn name of next player
- playerlist <ol> list of player names
- myname <span> user's name
- currentgist <p> displays latest gist info
---------------------------------------------------- */
var loginModalView = document.getElementById('loginmodal');
var loginButtonView = document.getElementById('loginbutton');
var editorInputView = document.getElementById('editor');
var timeLeftView = document.getElementById('timeleft');
var currentTurnView = document.getElementById('currentturn');
var nextTurnView = document.getElementById('nextturn');
var playerListView = document.getElementById('playerlist');
var currentGistView = document.getElementById('currentgist');
/* -------------------------------------------------
GITHUB AUTHENTICATION
---------------------------------------------------- */
// If GitHub tempcode is available as a parameter, get access_token from server and log in!
if ( window.location.href.match(/\?code=(.*)/) ) {
// Code for matching URL param from https://github.com/prose/gatekeeper
let tempCode = window.location.href.match(/\?code=(.*)/)[1];
// Remove parameter from URL, updating this entry in the client's browser history
history.replaceState(null, '', '/');
// TODO: show loading animation while waiting???
// Send tempCode to server in exchange for GitHub access token
get('/github-auth?code=' + tempCode).then(function(access_token){
// Save to local state
currentAccessToken = access_token;
// Get user data
return getJSON('https://api.github.com/user?access_token=' + currentAccessToken);
}).then(loginUser).catch(handleError);
// Otherwise, if user has not yet started the login process,
} else {
// Get the client ID environment variable from the server
get('/github-client')
.then(function(clientId){
console.log('>>>>>>>>>>>>>> Received response from /github-client route!:');
console.log(clientId);
// Render loginLinkView with the GitHub auth request URL
// TODO: review GitHub SCOPES when I try adding more features
document.getElementById('loginlink').setAttribute('href', 'https://github.com/login/oauth/authorize?client_id=' + clientId + '&scope=gist');
document.getElementById('loginlink').style.display = 'block';
// Hide "...loading..." placeholder
document.getElementById('loginloading').style.display = 'none';
}, handleError).catch(handleError);
}
/* -------------------------------------------------
ACE EDITOR SETUP
https://ace.c9.io
---------------------------------------------------- */
var editor = ace.edit('editor');
var Range = ace.require('ace/range').Range;
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode('ace/mode/javascript');
editor.setReadOnly(true);
/* ---------------------------------------------------------------------------------------------------------------------------------------------------
EVENTS:
Event Name Sent By Sent To Data Client Functions: Description
------------------ ---------- ------------------ -------------------------- ----------------------------- ---------------------------------------------
playerJoined Client Server {login, avatar_url} loginUser When new player completes login process
playerJoined Server All other clients {id, login, avatar_url} handlePlayerJoined Update other clients with new player data
gameState Server One client See game state model! handleGameState Initialize game state for new player that just logged in,
and trigger new gist creation if game is just starting!
playerLeft Server All other clients id Update other clients to remove disconnected player
turnChange Server All clients onDisconnect (Boolean) handleTurnChange Trigger clients to change the turn
newGist Client Server {id, url, owner} handleNewGist Broadcast new Gist data
editorTextChange Client Server "just a string!" handleLocalEditorTextChange Broadcast changes to code editor content
editorScrollChange Client Server {scrollLeft, scrollTop} handleLocalEditorScrollChange Broadcast changes to code editor content
editorCursorChange Client Server { handleLocalEditorCursorChange Broadcast cursor moves or selection changes
cursor: {column, row},
range: {
end: {column, row},
start: {column, row}
}
}
disconnect Client Server ... ... When clients disconnect from server (SocketIO function)
connection Client Server ... ... When clients connect to server (SocketIO function)
---------------------------------------------------------------------------------------------------------------------------------------------------- */
/* ------------------------------------------------------------
LOCAL EVENT LISTENERS
-------------------------------------------------------------- */
editor.getSession().on('change', handleLocalEditorTextChange);
editor.getSession().selection.on('changeCursor', handleLocalEditorCursorChange);
editor.getSession().on('changeScrollLeft', handleLocalEditorScrollChange);
editor.getSession().on('changeScrollTop', handleLocalEditorScrollChange);
/* -------------------------------------------------
SERVER EVENT LISTENERS
---------------------------------------------------- */
socket.on('editorTextChange', handleServerEditorTextChange);
socket.on('editorCursorChange', handleServerEditorCursorChange);
socket.on('editorScrollChange', handleServerEditorScrollChange);
socket.on('gameState', handleGameState);
socket.on('playerJoined', handlePlayerJoined);
socket.on('playerLeft', handlePlayerLeft);
socket.on('turnChange', handleTurnChange);
socket.on('newGist', handleNewGist);
// When client disconnects, stop the timer!
socket.on('disconnect', function(){
console.log('* * * * * * * * DISCONNECTED FROM SERVER * * * * * * * *');
window.cancelAnimationFrame(animationId);
timeLeftView.textContent = '....';
});
// Log in with authenticated user's GitHub data
function loginUser (userData) {
console.log('**************** Logged in! GitHub User Data: *********************');
console.log(userData);
// Update views with user's GitHub name and avatar
updateLoggedInView(userData.login, userData.avatar_url);
// Notify server that user logged in
socket.emit('playerJoined', {login: userData.login, avatar_url: userData.avatar_url});
}
// Send editorInputView data to server
function handleLocalEditorTextChange (event) {
// If user is the current player, they can broadcast
if (socket.id === getCurrentPlayer().id ) {
// Send data to server
socket.emit( 'editorTextChange', editor.getValue() );
}
}
// Send cursor and selection data to server
function handleLocalEditorCursorChange (event) {
// Cursor object:
// {column, row}
// Selection Range object:
// { end: {column, row}, start: {column, row} }
// Send to server:
socket.emit( 'editorCursorChange', { cursor: editor.getSession().selection.getCursor(), range: editor.getSession().selection.getRange() } );
}
// Send scroll data to server
function handleLocalEditorScrollChange (event) {
// Send to server:
socket.emit('editorScrollChange', { scrollLeft: editor.getSession().getScrollLeft(), scrollTop: editor.getSession().getScrollTop() });
}
// TODO: Test 'input' event some more in different browsers!
// maybe add support for IE < 9 later?
// When receiving new editorInputView data from server
function handleServerEditorTextChange (data) {
updateEditorView(data);
}
// When receiving new cursor/selection data from server
function handleServerEditorCursorChange (data) {
// Set Ace editor's cursor and selection range to match
var updatedRange = new Range(data.range.start.row, data.range.start.column, data.range.end.row, data.range.end.column);
editor.getSession().selection.setSelectionRange( updatedRange );
}
// When receiving new scroll data from server
function handleServerEditorScrollChange (data) {
// Set Ace editor's scroll position to match
editor.getSession().setScrollLeft(data.scrollLeft);
editor.getSession().setScrollTop(data.scrollTop);
}
// Initialize client after logging in, using game state data from server
function handleGameState (serverGameState) {
gameState.nextTurnTimestamp = serverGameState.nextTurnTimestamp;
gameState.turnIndex = serverGameState.turnIndex;
gameState.currentGist = serverGameState.currentGist;
gameState.players = serverGameState.players;
console.log("handleGameState called");
console.dir(gameState);
// Update editor content
updateEditorView(serverGameState.editor.content);
// Update editor cursor and selection range
if (serverGameState.editor.cursorAndSelection !== null) {
var updatedRange = new Range(serverGameState.editor.cursorAndSelection.range.start.row, serverGameState.editor.cursorAndSelection.range.start.column, serverGameState.editor.cursorAndSelection.range.end.row, serverGameState.editor.cursorAndSelection.range.end.column);
editor.getSession().selection.setSelectionRange( updatedRange );
}
// Update editor scroll position
if (serverGameState.editor.scroll !== null) {
editor.getSession().setScrollLeft(serverGameState.editor.scroll.scrollLeft);
editor.getSession().setScrollTop(serverGameState.editor.scroll.scrollTop);
}
// If no Gist exists, create it!
if (gameState.currentGist == null) {
createNewGist();
} else { // Otherwise, if a Gist does exist, display it!
updateCurrentGistView(gameState.currentGist);
}
// Update UI
updatePlayerListView(gameState.players);
updateTimeLeftView(gameState.nextTurnTimestamp);
updateCurrentTurnView(getCurrentPlayer().login);
updateNextTurnView(getNextPlayer().login);
toggleMyTurnHighlight();
// If this client is the current player, let them type!
if ( socket.id === getCurrentPlayer().id ) {
editor.setReadOnly(false);
}
}
// When a new player joins, update using data from server
function handlePlayerJoined (newPlayerData) {
// Add new player
gameState.players.push(newPlayerData);
// Update the UI
updatePlayerListView(gameState.players);
updateCurrentTurnView(getCurrentPlayer().login);
updateNextTurnView(getNextPlayer().login);
}
// When a player disconnects, update using data from server
function handlePlayerLeft (playerId) {
// Update turnIndex only if disconnected player comes BEFORE current player in the players array
if ( getPlayerIndexById(playerId, gameState.players) < gameState.turnIndex ) {
gameState.turnIndex--;
}
// Remove disconnected player from player list
removePlayer(playerId, gameState.players);
// Remove view for disconnected player from the player list view
playerListView.removeChild( document.getElementById(playerId) );
}
// When receiving turnChange event from server
function handleTurnChange (onDisconnect) {
console.log('%c turnChange event received! TIME: ' + new Date().toString().substring(16,25), 'color: blue; font-weight: bold;');
// Update the timestamp of the next turn, reset the clock!
gameState.nextTurnTimestamp = Date.now() + turnDuration;
// If turn change was NOT triggered by current player disconnecting, and a Gist exists,
if (!onDisconnect && gameState.currentGist != null) {
// Temporarily save previous player info before changing turn
var previousPlayer = getCurrentPlayer();
console.log("previousPlayer login: " + previousPlayer.login);
// And if this client is the one whose turn is ending, then fork and/or edit the Gist before passing control to next player!
if (socket.id === previousPlayer.id) {
console.log("This user's turn is about to end.");
// If this client (the previous player) does NOT own the current Gist,
if (previousPlayer.login !== gameState.currentGist.owner) {
// Fork/edit current Gist on behalf of this client (the previous player, whose turn is ending), and send new ID to server
forkAndEditGist(gameState.currentGist.id, editor.getValue());
console.log("handleTurnChange: now forking and editing gist " + gameState.currentGist.id + " owned by " + gameState.currentGist.owner + "(on behalf of player " + previousPlayer.login + ")");
// Otherwise, just edit the current Gist
} else {
editGist(gameState.currentGist.id, editor.getValue());
console.log("handleTurnChange: now editing gist " + gameState.currentGist.id + " owned by " + gameState.currentGist.owner + "(on behalf of player " + previousPlayer.login + ")");
}
}
}
changeTurn();
console.log("TURN CHANGED! turnIndex: " + gameState.turnIndex + ", # players: " + gameState.players.length, ", current player: " + getCurrentPlayer().id + " - " + getCurrentPlayer().login);
// If user is no longer the current player, prevent them from typing/broadcasting!
if ( socket.id !== getCurrentPlayer().id ) {
editor.setReadOnly(true);
// Otherwise if user's turn is now starting,
} else {
console.log("User's turn is starting. Allow typing!");
// let the user type/broadcast again
editor.setReadOnly(false);
}
// Update UI
togglePlayerHighlight(getCurrentPlayer().id);
updateTimeLeftView(gameState.nextTurnTimestamp);
updateCurrentTurnView(getCurrentPlayer().login);
updateNextTurnView(getNextPlayer().login);
toggleMyTurnHighlight();
}
// When receiving "newGist" event from server,
function handleNewGist (gistData) {
console.log("called handleNewGist at " + new Date().toString().substring(16,25), 'color: green; font-weight: bold;');
console.log(gistData);
// Update local state
gameState.currentGist = gistData;
// Update views
updateCurrentGistView(gistData);
}
/* -------------------------------------------------
FUNCTIONS TO UPDATE VIEWS
---------------------------------------------------- */
// Update views for logged in user
function updateLoggedInView (userName, userAvatar) {
// Hide loginModalView
loginModalView.style.opacity = '0';
window.setTimeout(function(){
loginModalView.style.display = 'none';
}, 900);
}
// UI highlights to notify user when it's their turn
function toggleMyTurnHighlight () {
// If user is the next player, highlight text box
if ( socket.id === getCurrentPlayer().id ) {
document.body.classList.add('myturn');
} else {
document.body.classList.remove('myturn');
}
}
// Highlight name of specified player in playerListView
function togglePlayerHighlight (playerId) {
var highlightedPlayerElement = document.querySelector('.highlight');
var nextPlayerElement = document.getElementById(playerId);
// Remove highlight from the currently-highlighted element if it exists:
if (highlightedPlayerElement) {
highlightedPlayerElement.classList.remove('highlight');
}
// Add highlight to specified player element (if element exists)
if (nextPlayerElement) {
nextPlayerElement.classList.add('highlight');
}
}
// Update list of players
function updatePlayerListView (playerArray) {
// First reorder the player array so current user is at the top, without modifying the turn order
var clientIndex = getPlayerIndexById(socket.id, playerArray);
var playersTopSegment = playerArray.slice(clientIndex);
var playersBottomSegment = playerArray.slice(0, clientIndex);
var reorderedPlayers = playersTopSegment.concat(playersBottomSegment);
// Delete the contents of playerListView each time
while (playerListView.firstChild) {
playerListView.removeChild(playerListView.firstChild);
}
// Append player names to playerListView
reorderedPlayers.forEach(function(player){
// Create an <li> node with player's name and avatar
var playerElement = document.createElement('li');
playerElement.id = player.id;
// Display user's GitHub avatar image
var userAvatarElem = document.createElement('img');
userAvatarElem.src = player.avatar_url;
userAvatarElem.classList.add('avatar');
// If this player is the current player, highlight their name
if (player.id === getCurrentPlayer().id) {
playerElement.classList.add('highlight');
}
// Append player <li> to playerListView <ol>
playerListView.appendChild(playerElement);
// Append image and text node to player <li>
playerElement.appendChild(userAvatarElem);
playerElement.appendChild(document.createTextNode(player.login));
});
}
// Using data from server, update text in code editor
function updateEditorView (editorData) {
editor.setValue(editorData);
editor.selection.clearSelection();
}
// Update timeLeftView to display the time remaining in mm:ss format
function updateTimeLeftView (nextTurnTimestamp) {
// Animate countdown timer
function step(timestamp) {
var millisRemaining = nextTurnTimestamp - Date.now();
var secondsRemaining = Math.floor(millisRemaining / 1000);
var minutes = Math.floor(secondsRemaining / 60);
var seconds = secondsRemaining % 60;
// Format mm:ss string, padded with zeroes if needed
timeLeftView.textContent = ((minutes.toString().length > 1) ? minutes.toString() : '0' + minutes.toString()) + ':' + ((seconds.toString().length > 1) ? seconds.toString() : '0' + seconds.toString());
if (millisRemaining > 100) {
animationId = window.requestAnimationFrame(step);
}
}
animationId = window.requestAnimationFrame(step);
}
// Update currentTurnView with current player's name
function updateCurrentTurnView (playerName) {
//console.log('Called updateCurrentTurnView with: ' + playerName);
currentTurnView.textContent = playerName;
// If user is the current player, highlight their name
if (socket.id === getCurrentPlayer().id) {
currentTurnView.classList.add('highlightme');
currentTurnView.textContent = "It's your turn!";
} else {
currentTurnView.classList.remove('highlightme');
}
}
// Update nextTurnView with next player's name
function updateNextTurnView (playerName) {
//console.log('Called updateNextTurnView with: ' + playerName);
nextTurnView.textContent = playerName;
// If user is the next player, highlight their name
if (socket.id === getNextPlayer().id) {
nextTurnView.classList.add('highlightme');
nextTurnView.textContent = "You're up next!";
} else {
nextTurnView.classList.remove('highlightme');
}
}
// Append to a list of gist links for this game
function updateCurrentGistView (gistData) {
currentGistView.innerHTML = '<strong>Latest code:</strong> <a href="' + gistData.url + '">' + gistData.url + '</a>';
}
/* -------------------------------------------------
GITHUB API FUNCTIONS
---------------------------------------------------- */
// Make a POST request via AJAX to create a Gist for the current user
function createNewGist() {
console.log('called createNewGist at ' + new Date().toString().substring(16,25), 'color: red; font-weight: bold;');
// use currentAccessToken
// use https://developer.github.com/v3/gists/#create-a-gist
var gistObject = {
"description": "A saved mob programming session with Learn Teach Code!",
"public": true,
"files": {
"mob-coding-challenge.js": {
"content": editor.getValue() + '\n'
}
}
};
postWithGitHubToken('https://api.github.com/gists', gistObject).then(function(responseText){
//console.log(responseText);
console.log('createNewGist: response received at ' + new Date().toString().substring(16,25), 'color: red; font-weight: bold;');
var gistObject = JSON.parse(responseText);
// Save new Gist data locally and update UI
handleNewGist({id: gistObject.id, url: gistObject.html_url, owner: gistObject.owner.login});
// Send gist data to server
socket.emit('newGist', gameState.currentGist);
}, handleError);
}
// Make a POST request via AJAX to update a given Gist with the current code
function editGist(gistId, codeEditorContent) {
console.log('called editGist at ' + new Date().toString().substring(16,25), 'color: orange; font-weight: bold;');
// use https://developer.github.com/v3/gists/#edit-a-gist
var gistObject = {
"description": "A saved mob programming session with Learn Teach Code!",
"public": true,
"files": {
"mob-coding-challenge.js": {
"content": codeEditorContent + '\n'
}
}
};
postWithGitHubToken('https://api.github.com/gists/' + gistId, gistObject).then(function(responseText){
//console.log(responseText);
console.log('editGist: response received at ' + new Date().toString().substring(16,25), 'color: orange; font-weight: bold;');
console.dir(JSON.parse(responseText));
}, handleError);
}
// Make a POST request via AJAX to fork a given Gist, then commit to it with editGist()
function forkAndEditGist(gistId, codeEditorContent) {
console.log('called forkAndEditGist at ' + new Date().toString().substring(16,25), 'color: red; font-weight: bold;');
// use https://developer.github.com/v3/gists/#fork-a-gist
// TODO later: see if I can refactor this function, maybe have it return a promise, so I can chain it with editGist better?
var gistObject = {"test": ""};
postWithGitHubToken('https://api.github.com/gists/' + gistId + '/forks', gistObject).then(function(responseText){
//console.log(responseText);
console.log('forkAndEditGist: response received at ' + new Date().toString().substring(16,25), 'color: red; font-weight: bold;');
var gistObject = JSON.parse(responseText);
console.dir(gistObject);
// Then edit the new gist:
editGist(gistObject.id, codeEditorContent);
// Save new Gist data locally and update UI
handleNewGist({id: gistObject.id, url: gistObject.html_url, owner: gistObject.owner.login});
// Send new gist data to server
socket.emit('newGist', gameState.currentGist);
}, handleError);
}
/* -------------------------------------------------
HELPER FUNCTIONS
---------------------------------------------------- */
// Returns a promise, as a simple wrapper around XMLHTTPRequest
// via http://eloquentjavascript.net/17_http.html
function get(url) {
return new Promise(function(succeed, fail) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function() {
if (req.status < 400)
succeed(req.responseText);
else
fail(new Error("Request failed: " + req.statusText));
});
req.addEventListener("error", function() {
fail(new Error("Network error"));
});
req.send(null);
});
}
// Returns a promise for a POST request, similar to get() above
function postWithGitHubToken(url, postDataObject) {
return new Promise(function(succeed, fail) {
var req = new XMLHttpRequest();
req.open("POST", url, true);
// Set header for POST, like sending form data
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
// Set header for GitHub auth
req.setRequestHeader('Authorization', 'token ' + currentAccessToken);
req.addEventListener("load", function() {
if (req.status < 400)
succeed(req.responseText);
else
fail(new Error("Request failed: " + req.statusText));
});
req.addEventListener("error", function() {
fail(new Error("Network error"));
});
req.send(JSON.stringify(postDataObject));
});
}
// Return object from parsed JSON data from a given URL
// via http://eloquentjavascript.net/17_http.html
function getJSON(url) {
return get(url).then(JSON.parse, handleError);
}
// Lazy error handling for now
function handleError(error) {
console.log("Error: " + error);
};
function changeTurn() {
gameState.turnIndex = (gameState.turnIndex + 1) % gameState.players.length;
}
function getCurrentPlayer() {
return gameState.players[gameState.turnIndex];
}
function getNextPlayer() {
var nextPlayerIndex = (gameState.turnIndex + 1) % gameState.players.length;
return gameState.players[nextPlayerIndex];
}
function getPlayerById(id, playerList){
for (var i = 0; i < playerList.length; i++) {
if (playerList[i].id === id) {
return playerList[i];
}
}
return -1;
}
function getPlayerIndexById(id, playerList) {
for (var i = 0; i < playerList.length; i++) {
if (playerList[i].id === id) {
return i;
}
}
return -1;
}
function removePlayer(id, playerList) {
for (var i = 0; i < playerList.length; i++) {
if (playerList[i].id === id) {
playerList.splice(i, 1);
}
}
}