-
Notifications
You must be signed in to change notification settings - Fork 5
/
blackout.ino
1522 lines (1288 loc) · 41.2 KB
/
blackout.ino
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
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// DISPEL!
// (c) 2020 Scott Cartier
#define null 0
#define DONT_CARE 0
// Defines to enable debug code
#define RENDER_DEBUG_AND_ERRORS 0
#define USE_DATA_SPONGE 0
#define DEBUG_SETUP 0
#define DEBUG_AUTO_WIN 0
#define REDUNDANT_ROLE_CHECKS 0
#if USE_DATA_SPONGE
#warning DATA SPONGE ENABLED
byte sponge[108];
#endif
// SPONGE LOG
// 7/20: Reduced datagram size in blinklib (game doesn't use them anyway): >100
// 7/19/2021: 15 to init, 12 to complete a game
// 12/9: After new animations: 31 to init, 29 to complete a game
// 11/22: Setup & win animations: 25
// 11/21: Added an animation timer per-face instead of per-tile
// 11/20: Changed to Blackout! which removed all the tool patterns and difficulty structures: 70-75
// 11/3: 48, 39
// 11/10/2020: 25
void __attribute__((noinline)) showAnimation(byte animIndex, byte newAnimRate);
byte __attribute__((noinline)) randRange(byte min, byte max);
void __attribute__((noinline)) setSolidColorOnState(byte color, byte *state);
void __attribute__((noinline)) setYellowOnState(byte *state);
void __attribute__((noinline)) updateToolColor(byte color);
void __attribute__((noinline)) resetGame();
void __attribute__((noinline)) setupNextRainbowColor(byte value);
void __attribute__((noinline)) showAnimation_Init();
void __attribute__((noinline)) updateStateWithTools(byte *state);
void __attribute__((noinline)) showAnimation_Tool();
byte faceOffsetArray[] = { 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5 };
#define CCW_FROM_FACE(f, amt) faceOffsetArray[6 + (f) - (amt)]
#define CW_FROM_FACE(f, amt) faceOffsetArray[(f) + (amt)]
#define OPPOSITE_FACE(f) CW_FROM_FACE((f), 3)
#define TOGGLE_COMMAND 1
#define TOGGLE_DATA 0
struct FaceValue
{
byte value : 4;
byte toggle : 1;
byte ack : 1;
};
struct FaceStateComm
{
FaceValue faceValueOut;
byte lastCommandIn;
bool neighborPresent;
bool debug;
};
FaceStateComm faceStatesComm[FACE_COUNT] =
{
{ { 0, TOGGLE_DATA, TOGGLE_DATA } }, // Initialize here so we don't need to do it in code during setup()
{ { 0, TOGGLE_DATA, TOGGLE_DATA } },
{ { 0, TOGGLE_DATA, TOGGLE_DATA } },
{ { 0, TOGGLE_DATA, TOGGLE_DATA } },
{ { 0, TOGGLE_DATA, TOGGLE_DATA } },
{ { 0, TOGGLE_DATA, TOGGLE_DATA } }
};
enum Command
{
Command_None, // no data
// Setup
Command_AssignRole, // Working tile assigns roles to neighbors
Command_AssignToolPattern, // Working tile assigns patterns to Tool tiles
// Play
Command_RequestPattern,
Command_RequestRotation,
Command_RequestColor,
Command_ToolPattern,
Command_ToolRotation,
Command_ToolColor,
Command_ResetToolColor,
Command_SetGameState,
Command_PuzzleSolved,
Command_Reset
};
void __attribute__((noinline)) enqueueCommOnFace(byte f, Command command, byte data);
struct CommandAndData
{
Command command : 4;
byte data : 4;
};
#define COMM_QUEUE_SIZE 8
CommandAndData commQueues[FACE_COUNT][COMM_QUEUE_SIZE];
#define COMM_INDEX_ERROR_OVERRUN 0xFF
#define COMM_INDEX_OUT_OF_SYNC 0xFE
byte commInsertionIndexes[FACE_COUNT] = { 0, 0, 0, 0, 0, 0 };
byte numNeighbors = 0;
#define ErrorOnFace(f) (commInsertionIndexes[f] > COMM_QUEUE_SIZE)
// -------------------------------------------------------------------------------------------------
// TILES
// -------------------------------------------------------------------------------------------------
enum TileRole
{
TileRole_Unassigned,
TileRole_Working,
TileRole_Tool
};
TileRole tileRole = TileRole_Unassigned;
struct ToolState
{
byte pattern;
byte color;
byte rotation;
};
#define TOOL_PATTERN_UNASSIGNED 0b1110 // unused pattern is our "unassigned" key value
#define COLOR_WHITE 3
ToolState assignedTool = { TOOL_PATTERN_UNASSIGNED, 0, 0 };
// Twelve possible patterns (not including all on & all off)
byte patterns[] =
{
0b0000, // 1
0b0001, // 2
0b0010,
0b0100,
0b0011, // 3
0b0101,
0b1010,
0b1001,
0b0111, // 4
0b1011,
0b1101,
0b1111 // 5
};
// -------------------------------------------------------------------------------------------------
// ANIMATION
// -------------------------------------------------------------------------------------------------
enum AnimCommand
{
AnimCommand_SolidBase,
AnimCommand_SolidOverlay,
AnimCommand_SolidWithOverlay,
AnimCommand_BaseAndOverlay,
AnimCommand_LerpBaseToOverlay,
AnimCommand_LerpOverlayToBase,
AnimCommand_LerpBaseToBaseHalf,
AnimCommand_LerpBaseHalfToBase,
AnimCommand_LerpOverlayIfNonZeroToBase,
AnimCommand_LerpBaseToOverlayIfNonZero,
AnimCommand_Pause,
AnimCommand_PauseHalf,
AnimCommand_FadeInBase,
AnimCommand_FadeOutBase,
AnimCommand_FadeOutBaseIfOverlayR,
AnimCommand_RandomRotateBaseAndOverlayR,
AnimCommand_RandomToolOnBase,
AnimCommand_Loop,
AnimCommand_Done
};
// Animation sequences
// Each set of commands defines what colors are shown on each face and in what order
AnimCommand animSequences[] =
{
// CONSTANT COLOR - BASE ONLY
// Used: Show patterns on rune tiles
AnimCommand_SolidBase,
AnimCommand_Loop,
// OVERLAY TO BASE - one shot
// Used: 1. When starting the game - fade in rune tiles from solid yellow
// 2. Rainbowpalooza (uses delay on outer half)
AnimCommand_SolidOverlay,
AnimCommand_PauseHalf,
AnimCommand_LerpOverlayToBase,
AnimCommand_SolidBase,
AnimCommand_Done,
// PUZZLE STATE CHANGED - one shot
// Used: When a rune tile changes and the puzzle updates - flashes white and fades to new state
AnimCommand_SolidWithOverlay,
AnimCommand_LerpOverlayIfNonZeroToBase,
AnimCommand_SolidBase,
AnimCommand_Done,
// INIT
// Used: Fireworks init - random color+face fading out
AnimCommand_RandomRotateBaseAndOverlayR,
AnimCommand_FadeOutBaseIfOverlayR,
AnimCommand_Loop,
// BASE + PULSE OVERLAY
// Used: During setup on puzzle tile to pulse the difficulty wedges
AnimCommand_LerpBaseToOverlayIfNonZero,
AnimCommand_LerpOverlayIfNonZeroToBase,
AnimCommand_Loop,
// TOOL
// Used: During setup on rune tiles to show random tools fading in+out
AnimCommand_RandomToolOnBase,
AnimCommand_FadeInBase,
AnimCommand_FadeOutBase,
AnimCommand_Loop,
// WORKING PULSE
// Used: While idle, the working tile will play this anim to differentiate itself from the tool tiles
AnimCommand_LerpBaseToBaseHalf,
AnimCommand_LerpBaseHalfToBase,
AnimCommand_SolidBase,
AnimCommand_Done,
};
#define ANIM_SEQ_INDEX__BASE 0
#define ANIM_SEQ_INDEX__OVERLAY_TO_BASE_DELAYED (ANIM_SEQ_INDEX__BASE+2)
#define ANIM_SEQ_INDEX__OVERLAY_TO_BASE (ANIM_SEQ_INDEX__OVERLAY_TO_BASE_DELAYED+2)
#define ANIM_SEQ_INDEX__STATE_CHANGED (ANIM_SEQ_INDEX__OVERLAY_TO_BASE_DELAYED+5)
#define ANIM_SEQ_INDEX__INIT (ANIM_SEQ_INDEX__STATE_CHANGED+4)
#define ANIM_SEQ_INDEX__BASE_PULSE_OVERLAY (ANIM_SEQ_INDEX__INIT+3)
#define ANIM_SEQ_INDEX__TOOL_SETUP (ANIM_SEQ_INDEX__BASE_PULSE_OVERLAY+3)
#define ANIM_SEQ_INDEX__WORKING_PULSE (ANIM_SEQ_INDEX__TOOL_SETUP+4)
// Shift by 2 so it fits in a byte
#define ANIM_RATE_SLOW (1000>>2)
#define ANIM_RATE_FAST (300>>2)
#define WORKING_PULSE_RATE 1000
Timer workingPulseTimer;
// Rainbow order bitwise 1=b001=R, 3=b011=R+G, 2=b010=G, 6=b110=G+B, 4=b100=B, 5=b101=B+R
byte rainbowSequence[] = { 1, 3, 2, 6, 4, 5 };
byte rainbowIndex = 0;
// -------------------------------------------------------------------------------------------------
// GAME
// -------------------------------------------------------------------------------------------------
enum GameState
{
GameState_Init, // tiles were just programmed - waiting for click of Target tile
GameState_Setup, // let player choose number of tool tiles and difficulty
GameState_Play, // gameplay
GameState_Done // player matched the Target tile
};
GameState gameState = GameState_Init;
byte numToolTiles = 0;
#define NUM_DIFFICULTY_LEVELS 6
byte difficulty = 0;
uint32_t startingSeed = 0;
byte rootFace = 0; // so Tool tiles to know which face is the Target tile
struct FaceStateGame
{
byte animIndexStart;
byte animIndexCur;
byte animRateDiv4; // lerp/pause rate
Timer animTimer; // timer used for lerps and pauses during animation
bool animDone;
uint16_t savedColor; // used during pauses in animation
// Used during setup
ToolState neighborTool;
};
FaceStateGame faceStatesGame[FACE_COUNT];
// Store our current puzzle state
byte startingState[3]; // how the puzzle starts
byte colorState[3]; // the current state displayed
byte overlayState[3]; // used for animations or to hide things
#define RESET_TIMER_DELAY 1000
Timer resetTimer; // prevents cyclic resets
uint32_t randState;
#if DEBUG_AUTO_WIN
bool autoWin = false;
#endif
// =================================================================================================
//
// SETUP
//
// =================================================================================================
#include <avr/io.h>
void setup()
{
// Temporary random seed is generated detiministically from our tile's serial number
uint32_t serial_num_32 = 2463534242UL; // We start with Marsaglia's seed...
// ... and then fold in bits form our serial number
for( byte i=0; i< SERIAL_NUMBER_LEN; i++ ) {
serial_num_32 ^= getSerialNumberByte(i) << (i * 3);
}
randState=serial_num_32;
#if USE_DATA_SPONGE
// Use our data sponge so that it isn't compiled away
if (sponge[0])
{
sponge[0] = 3;
}
#endif
/*
FOREACH_FACE(f)
{
resetCommOnFace(f);
}
*/
//setColor(GREEN);
showAnimation_Init();
}
// =================================================================================================
//
// LOOP
//
// =================================================================================================
void loop()
{
// Detect button clicks
handleUserInput();
// Update neighbor presence and comms
updateCommOnFaces();
switch (gameState)
{
case GameState_Init: break; // do nothing - waiting for click in handleUserInput()
case GameState_Setup:
if (tileRole == TileRole_Working)
{
setupWorking();
}
else
{
setupTool();
}
break;
case GameState_Play: playWorking(); break;
case GameState_Done: doneWorking(); break;
}
render();
// Call hasWoken() explicitly to clear the flag
hasWoken();
}
// =================================================================================================
//
// INPUT
//
// =================================================================================================
void __attribute__((noinline)) updateDifficulty(byte newDifficulty)
{
difficulty = newDifficulty;
if (difficulty >= NUM_DIFFICULTY_LEVELS)
{
difficulty = 0;
}
byte difficultyOverlay = ~(0xFE << difficulty);
overlayState[0] = overlayState[1] = difficultyOverlay; overlayState[2] = 0x0;
generateToolsAndPuzzle();
}
void handleUserInput()
{
#if DEBUG_AUTO_WIN
if (buttonMultiClicked())
{
byte clicks = buttonClickCount();
if (clicks == 5)
{
if (tileRole == TileRole_Working)
{
autoWin = true;
}
}
}
#endif
if (buttonDoubleClicked())
{
if (gameState == GameState_Setup && tileRole == TileRole_Working && numNeighbors > 0)
{
// Start the game
gameState = GameState_Play;
resetWorkingPulseTimer();
FOREACH_FACE(f)
{
if (faceStatesComm[f].neighborPresent)
{
enqueueCommOnFace(f, Command_SetGameState, GameState_Play);
}
}
setYellowOnState(overlayState);
showAnimation(ANIM_SEQ_INDEX__OVERLAY_TO_BASE, ANIM_RATE_FAST);
updateWorkingState();
}
else if (gameState == GameState_Play && tileRole == TileRole_Working)
{
// Double clicking the Working tile during a game will reset all surrounding tool tiles
FOREACH_FACE(f)
{
faceStatesGame[f].neighborTool.color = COLOR_WHITE;
enqueueCommOnFace(f, Command_ResetToolColor, DONT_CARE);
}
updateWorkingState();
}
else if (gameState == GameState_Play && tileRole == TileRole_Tool)
{
// Double clicking a Tool tile during a game will reset it back to white
updateToolColor(COLOR_WHITE);
}
}
if (buttonSingleClicked() && !hasWoken())
{
switch (gameState)
{
case GameState_Init:
// Clicking a tile during init changes it to the Working tile and let's the player
// select the difficulty and add/remove tool tiles
tileRole = TileRole_Working;
gameState = GameState_Setup;
setSolidColorOnState(0b100, colorState);
showAnimation(ANIM_SEQ_INDEX__BASE_PULSE_OVERLAY, ANIM_RATE_FAST);
updateDifficulty(difficulty); // called to update the faces to show the current difficulty
// Clear this so the Working tile will re-detect
numToolTiles = 0;
// Button clicking provides our entropy for the initial random seed
randState = millis();
generateNewStartingSeed();
#if DEBUG_AUTO_WIN
autoWin = false;
#endif
break;
case GameState_Setup:
// Clicking the Working tile during setup changes the difficulty
if (tileRole == TileRole_Working)
{
updateDifficulty(difficulty + 1);
}
break;
case GameState_Play:
// Clicking a Tool tile during the game cycles through Red/Green/Blue/White
if (tileRole == TileRole_Tool)
{
updateToolColor((assignedTool.color == COLOR_WHITE) ? 0 : (assignedTool.color + 1));
}
break;
case GameState_Done:
// Clicking any tile after winning will reset so the player can play again
resetGame();
break;
}
}
// No matter what state we're in, triple click resets the game and other connected tiles
if (buttonMultiClicked())
{
if (buttonClickCount() == 3)
{
resetGame();
}
}
}
// Change the assigned color for the tool and inform the Working tile
void __attribute__((noinline)) updateToolColor(byte color)
{
assignedTool.color = color;
showAnimation_Tool();
enqueueCommOnFace(rootFace, Command_ToolColor, assignedTool.color);
}
void __attribute__((noinline)) resetTileState()
{
gameState = GameState_Init;
tileRole = TileRole_Unassigned;
assignedTool = { TOOL_PATTERN_UNASSIGNED, 0 };
showAnimation_Init();
}
// Reset this tile and tell all connected tiles to reset
// This gets propagated to neighbors, using a timer to prevent infinite loops
void __attribute__((noinline)) resetGame()
{
if (!resetTimer.isExpired())
{
return;
}
resetTimer.set(RESET_TIMER_DELAY);
resetTileState();
// Propagate the reset out to the cluster
FOREACH_FACE(f)
{
// Nuke all pending comm packets by resetting the insertion index
// This should fix things if the queue somehow gets overrun
commInsertionIndexes[f] = 0;
// If the tile was in the middle of sending a comm packet on this face
// then it has already sent the command. Thus this first reset command
// might get ignored because only the data will be sent. Queue two
// commands to be sure it gets through. There is a timer for protection
// so that they don't both take effect.
enqueueCommOnFace(f, Command_Reset, DONT_CARE);
enqueueCommOnFace(f, Command_Reset, DONT_CARE);
}
}
byte __attribute__((noinline)) rotateSixBits(byte inByte, byte rotateAmount)
{
return ((inByte << rotateAmount) | (inByte >> (6 - rotateAmount))) & 0x3F;
}
// =================================================================================================
//
// COMMUNICATIONS
//
// =================================================================================================
void __attribute__((noinline)) sendValueOnFace(byte f, FaceValue faceValue)
{
byte outVal = *((byte*)&faceValue);
setValueSentOnFace(outVal, f);
}
void resetCommOnFace(byte f)
{
// Clear the queue
commInsertionIndexes[f] = 0;
// Put the current output into its reset state.
// In this case, all zeroes works for us.
// Assuming the neighbor is also reset, it means our ACK == their TOGGLE.
// This allows the next pair to be sent immediately.
// Also, since the toggle bit is set to TOGGLE_DATA, it will toggle into TOGGLE_COMMAND,
// which is what we need to start sending a new pair.
faceStatesComm[f].faceValueOut.value = 0;
faceStatesComm[f].faceValueOut.toggle = TOGGLE_DATA;
faceStatesComm[f].faceValueOut.ack = TOGGLE_DATA;
sendValueOnFace(f, faceStatesComm[f].faceValueOut);
}
// Called by the main program when this tile needs to tell something to
// a neighbor tile.
void __attribute__((noinline)) enqueueCommOnFace(byte f, Command command, byte data)
{
// Check here so callers don't all need to check
if (!faceStatesComm[f].neighborPresent)
{
return;
}
if (commInsertionIndexes[f] >= COMM_QUEUE_SIZE)
{
// Buffer overrun - might need to increase queue size to accommodate
// commInsertionIndexes[f] = COMM_INDEX_ERROR_OVERRUN;
return;
}
byte index = commInsertionIndexes[f];
commQueues[f][index].command = command;
commQueues[f][index].data = data;
commInsertionIndexes[f]++;
}
// Called every iteration of loop(), preferably before any main processing
// so that we can act on any new data being sent.
void updateCommOnFaces()
{
numNeighbors = 0;
FOREACH_FACE(f)
{
FaceStateComm *faceStateComm = &faceStatesComm[f];
// Is the neighbor still there?
if (isValueReceivedOnFaceExpired(f))
{
// Lost the neighbor - go back into reset on this face
faceStateComm->neighborPresent = false;
resetCommOnFace(f);
continue;
}
faceStateComm->neighborPresent = true;
numNeighbors++;
// If there is any kind of error on the face then do nothing
// The error can be reset by removing the neighbor
if (ErrorOnFace(f))
{
continue;
}
// Read the neighbor's face value it is sending to us
byte val = getLastValueReceivedOnFace(f);
FaceValue faceValueIn = *((FaceValue*)&val);
//
// RECEIVE
//
// Did the neighbor send a new comm?
// Recognize this when their TOGGLE bit changed from the last value we got.
if (faceStateComm->faceValueOut.ack != faceValueIn.toggle)
{
// Got a new comm - process it
byte value = faceValueIn.value;
if (faceValueIn.toggle == TOGGLE_COMMAND)
{
// This is the first part of a comm (COMMAND)
// Save the command value until we get the data
faceStateComm->lastCommandIn = (Command) value;
}
else
{
// This is the second part of a comm (DATA)
// Use the saved command value to determine what to do with the data
processCommForFace((Command) faceStateComm->lastCommandIn, value, f);
}
// Acknowledge that we processed this value so the neighbor can send the next one
faceStateComm->faceValueOut.ack = faceValueIn.toggle;
}
//
// SEND
//
// Did the neighbor acknowledge our last comm?
// Recognize this when their ACK bit equals our current TOGGLE bit.
if (faceValueIn.ack == faceStateComm->faceValueOut.toggle)
{
// If we just sent the DATA half of the previous comm, check if there
// are any more commands to send.
if (faceStateComm->faceValueOut.toggle == TOGGLE_DATA)
{
if (commInsertionIndexes[f] == 0)
{
// Nope, no more comms to send - bail and wait
continue;
}
}
// Send the next value, either COMMAND or DATA depending on the toggle bit
// Toggle between command and data
faceStateComm->faceValueOut.toggle = ~faceStateComm->faceValueOut.toggle;
// Grab the first element in the queue - we'll need it either way
CommandAndData commandAndData = commQueues[f][0];
// Send either the command or data depending on the toggle bit
if (faceStateComm->faceValueOut.toggle == TOGGLE_COMMAND)
{
faceStateComm->faceValueOut.value = commandAndData.command;
}
else
{
faceStateComm->faceValueOut.value = commandAndData.data;
// No longer need this comm - shift everything towards the front of the queue
for (byte commIndex = 1; commIndex < COMM_QUEUE_SIZE; commIndex++)
{
commQueues[f][commIndex - 1] = commQueues[f][commIndex];
}
// Adjust the insertion index since we just shifted the queue
/*
if (commInsertionIndexes[f] == 0)
{
// Shouldn't get here - if so something is funky
commInsertionIndexes[f] = COMM_INDEX_OUT_OF_SYNC;
continue;
}
else
*/
{
commInsertionIndexes[f]--;
}
}
}
}
FOREACH_FACE(f)
{
// Update the value sent in case anything changed
sendValueOnFace(f, faceStatesComm[f].faceValueOut);
}
}
void processCommForFace(Command command, byte value, byte f)
{
// Early out while we're still in reset
if (!resetTimer.isExpired())
{
return;
}
//FaceStateComm *faceStateComm = &faceStatesComm[f];
//byte oppositeFace = OPPOSITE_FACE(f);
//faceStateComm->debug = true;
switch (command)
{
case Command_AssignRole:
// Grab our new role
if (tileRole != TileRole_Working)
{
tileRole = (TileRole) value;
rootFace = f;
gameState = GameState_Setup;
showAnimation(ANIM_SEQ_INDEX__TOOL_SETUP, ANIM_RATE_SLOW);
}
break;
case Command_AssignToolPattern:
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
assignedTool.pattern = value;
assignedTool.color = COLOR_WHITE;
#if DEBUG_SETUP
showAnimation_Tool();
#endif
// Send our rotation in case Working tile doesn't have it
enqueueCommOnFace(f, Command_ToolRotation, f);
}
break;
case Command_SetGameState:
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
gameState = (GameState) value;
if (gameState == GameState_Play)
{
showAnimation_Tool();
setYellowOnState(overlayState);
showAnimation_BurstOutward(f);
}
}
break;
case Command_RequestPattern:
// Only the working tile can request Tool info - update the root face
rootFace = f;
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
enqueueCommOnFace(f, Command_ToolPattern, assignedTool.pattern);
}
break;
case Command_RequestRotation:
// Only the working tile can request Tool info - update the root face
rootFace = f;
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
// We send the face that received this comm so the requester can compute our relative rotation
enqueueCommOnFace(f, Command_ToolRotation, f);
}
break;
case Command_RequestColor:
// Only the working tile can request Tool info - update the root face
rootFace = f;
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
enqueueCommOnFace(f, Command_ToolColor, assignedTool.color);
}
break;
case Command_ToolPattern:
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Working)
#endif
{
faceStatesGame[f].neighborTool.pattern = value;
// Got the pattern - now request the rotation
enqueueCommOnFace(f, Command_RequestRotation, DONT_CARE);
}
break;
case Command_ToolRotation:
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Working)
#endif
{
// Figure out how the Tool tile is rotated relative to the Working tile
byte oppositeFace = OPPOSITE_FACE(value);
faceStatesGame[f].neighborTool.rotation = CCW_FROM_FACE(f, oppositeFace);
if (gameState == GameState_Play)
{
// Got the rotation - now request the color
enqueueCommOnFace(f, Command_RequestColor, DONT_CARE);
}
}
break;
case Command_ToolColor:
if (tileRole == TileRole_Working)
{
faceStatesGame[f].neighborTool.color = value;
updateWorkingState();
}
break;
case Command_ResetToolColor:
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
updateToolColor(COLOR_WHITE);
}
break;
case Command_PuzzleSolved:
#if REDUNDANT_ROLE_CHECKS
if (tileRole == TileRole_Tool)
#endif
{
// BUGFIX : While doing the winning rainbow burst, resetting the tiles by clicking a rune
// tile had a race condition where the hex tile would send out one last 'PuzzleSolved'
// message before resetting, causing the tile you just clicked to go back to
// rainbow. Now, once you reset the game on a tile, it cannot be considered
// solved again.
if (gameState != GameState_Init)
{
gameState = GameState_Done;
setupNextRainbowColor(value);
showAnimation_BurstOutward(f);
}
}
break;
case Command_Reset:
resetGame();
break;
case Command_None:
/* Empty */
break;
}
}
// =================================================================================================
//
// GAME STATE: SETUP
// Let the player remove/attach Tool tiles, select difficulty, set a random seed
//
// =================================================================================================
// -------------------------------------------------------------------------------------------------
// WORKING
// -------------------------------------------------------------------------------------------------
void setupWorking()
{
// Check if a neighbor tile was added or removed
if (numNeighbors != numToolTiles)
{
numToolTiles = numNeighbors;
// Generate the tools and the puzzle based on them
if (numNeighbors > 0)
{
generateToolsAndPuzzle();
}
}
}
void generateToolsAndPuzzle()
{
// Generate a puzzle based on the difficulty, number of tool tiles, and random seed
// Nibble 4 (19:16) represents sixteen different puzzles using the same tools
// It needs to be part of the starting seed so it can be shown to the player,
// but we don't want it to factor into the seed for tool selection because tools
// shouldn't change based on it.
// FUTURE SCOTT NOTES: This is for the non-existing feature of sharing starting seeds
/*
uint32_t puzzleSeed = startingSeed;
puzzleSeed |= (uint32_t) numToolTiles << 24;
puzzleSeed |= (uint32_t) difficulty << 28;
*/
byte toolSlotIndex = randRange(0, 6 - numToolTiles + 2);
FOREACH_FACE(f)
{
if (!faceStatesComm[f].neighborPresent)
{
continue;
}
// Choose the tools sequentially, but start from a random tool if less than six neighbors
faceStatesGame[f].neighborTool.pattern = patterns[difficulty+toolSlotIndex];
// Make it a Tool tile and assign its pattern
enqueueCommOnFace(f, Command_AssignRole, TileRole_Tool);
enqueueCommOnFace(f, Command_AssignToolPattern, faceStatesGame[f].neighborTool.pattern);
toolSlotIndex++;
}
// Now that we're generating the actual puzzle, use a seed that includes [19:16]
// randState = puzzleSeed;
// Fill up the starting state with our tools
startingState[0] = startingState[1] = startingState[2] = 0;
byte randColor = randRange(0, 3); // randomly start at R G or B
FOREACH_FACE(f)
{
if (faceStatesComm[f].neighborPresent)
{
faceStatesGame[f].neighborTool.color = randColor;
faceStatesGame[f].neighborTool.rotation = randRange(0, FACE_COUNT);
// Go sequentially through RGB
// Could just select a random color every time, but that might result in some puzzles being all one color
randColor = (randColor == 2) ? 0 : (randColor + 1);
}
}
updateStateWithTools(startingState);
// Clear Tool colors at start
FOREACH_FACE(f)
{
if (faceStatesComm[f].neighborPresent)
{
faceStatesGame[f].neighborTool.color = COLOR_WHITE;
}