-
Notifications
You must be signed in to change notification settings - Fork 2
/
StateMachineAgent.java
1254 lines (1082 loc) · 37.2 KB
/
StateMachineAgent.java
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
import java.util.ArrayList;
import java.util.Random;
import java.util.Vector;
public class StateMachineAgent {
// Instance variables
private Path best = null; //best path from init to goal the agent knows atm
private ArrayList<Character> possibleBest;
private StateMachineEnvironment env;
private char[] alphabet;
private ArrayList<Episode> episodicMemory;
private Vector<Integer> addedInPlan;
//These are used as indexes into the the sensor array
private static final int IS_NEW_STATE = 0;
private static final int IS_GOAL = 1;
//Sensor values
public static final int NO_TRANSITION = 0;
public static final int TRANSITION_ONLY = 1;
public static final int GOAL = 2;
//Global state data
private ArrayList<int[]> equivalentStates;
private ArrayList<int[]> nonEquivalentStates;
private ArrayList<int[]> agentTransitionTable;
public static final int UNKNOWN_TRANSITION = -1; //Used to represent an unknown transition in the transition table
public static final int DELETED = -2; //Indicates that a given row in the transition table has been deleted
public static final int GOAL_STATE = 0;
public static final int INIT_STATE = 1;
public static final char UNKNOWN_COMMAND = ' '; //a character guaranteed not
//to be in the alphabet
// The state the agent is in based off it's own version of the state machine
private int currentState = 1;
// A path which the agent expects will take it to the goal
// In other words, a method of testing it's hypothesis about two states being the same
private ArrayList<Episode> currentPlan = null;
//next command to execute in the current plan
private int planIndex = -1;
// The hypothesis that the agent is currently testing
// The agent believes currentHypothesis[0] == currentHypothesis[1] where each entry is a state in the FSM
private int[] currentHypothesis;
// As the agent adds states to it's personal mapping of the environment, it has to number them
// accordingly. This variable keeps track of the next stateID it has not yet used
private int currentStateID = 1;
//Reset limit
public static final int MAX_RESETS = 1;
//Tells the agent whether or not to use the reorientation reset
private boolean reorientation = true;
// Turns debug printing on and off
boolean debug = true;
//DEBUG
int reorientFailures = 0;
int resetCount = 0;
/**
* The constructor for the agent simply initializes it's instance variables
*/
public StateMachineAgent() {
//int[][] testTransitions = new int[][] {{2, 1, 0},{1, 0, 2},{2, 2, 2}};
int[][] testTransitions = new int[][]{{0,1},{1,2},{2,2}};
//int[][] testTransitions = new int[][]{{0,1},{1,1}};
//env = new StateMachineEnvironment(testTransitions, 3, 3);
addedInPlan = new Vector<Integer>();
env = new StateMachineEnvironment(testTransitions, 2, 2);
alphabet = env.getAlphabet();
episodicMemory = new ArrayList<Episode>();
//Need a first episode for makeMove
episodicMemory.add(new Episode(UNKNOWN_COMMAND, NO_TRANSITION, INIT_STATE));
equivalentStates = new ArrayList<int[]>();
nonEquivalentStates = new ArrayList<int[]>();
agentTransitionTable = new ArrayList<int[]>();
int[] zeroRow = new int[alphabet.length];
int[] firstState = new int[alphabet.length];
//%%%TODO: Make the first element in a transition row the number of that state
for (int i = 0; i < zeroRow.length; i++) {
zeroRow[i] = /*UNKNOWN_TRANSITION*/0;
firstState[i] = UNKNOWN_TRANSITION;
}
agentTransitionTable.add(zeroRow);
agentTransitionTable.add(firstState);
possibleBest = new ArrayList<Character>();
}
/**
* Runs through the "Brute Force" algorithm for the agent, setting the
* "best" passphrase to the result
*/
public void bruteForce() {
// Generate an initial path
generatePath();
// DEBUG: try the path that was successful (sanity check)
//tryPath(best);
//best.printpath();
// Trim moves off the successful path until we only have the
// necessary moves remaining. Make this the new best path
best = trimPath(best);
// // DEBUG: Print out what the agent has determined the shortests path is
//best.printpath();
}
/**
* Guesses randomly until a path to the goal is generated
*
* @return
* The path to the goal that was found
*/
public Path generatePath() {
ArrayList<Character> randomPath = new ArrayList<Character>();
//Use our reset method to make random actions until we reach the goal
reset();
resetCount++;
//Pull the episodes we've just created out of memory and parse them into
//a path
for (int i = 0; i < episodicMemory.size(); i++){
randomPath.add(i, episodicMemory.get(i).command);
}
best = new Path(randomPath);
return best;
}
/**
* Given a full string of moves, tryPath will enter the moves
* one by one and determine if the entered path is successful
*
* CAVEAT: This method returns 'true' even if the goal is reached
* prematurely (before the path has been passed)
*
* @param best
* An ArrayList of Characters representing the path to try
*
* @return
* A boolean which is true if the path was reached the goal and
* false if it did not
*/
public boolean tryPath(Path best) {
boolean[] sensors;
// Enter each character in the path
for (int i = 0; i < best.size(); i++) {
sensors = env.tick(best.get(i));
int encodedSensorResult = encodeSensors(sensors);
episodicMemory.add(new Episode(best.get(i), encodedSensorResult, INIT_STATE));
if (sensors[IS_GOAL]) {
//DEBUG
//System.out.println("Given path works");
// If we successfully find the goal, return true
return true;
}
}
//DEBUG
//System.out.println("Given path fails");
// If we make it through the entire loop, the path was unsuccessful
return false;
}
/**
* trimPassphrase takes in a passphrase (which has been confirmed as
* successful) and removes one character at a time until it is able to
* determine the shortest version of the passphrase that is still
* successful
*
* @param toTrim
* The passphrase to trim characters from
* @return
* toTrim reduced to the least amount of characters possible (not including equivalencies)
*/
public Path trimPath(Path toTrim) {
// Make a copy of the passed-in passphrase so as not to modify it
Path trimmed = toTrim.copy();
char removed; //Allows us to keep track of the removed character and add it back in if necessary
for (int i = 0; i < trimmed.size(); i++) {
// Trim the current character from the passphrase and test the
// result
removed = trimmed.get(i);
trimmed.remove(i);
if (tryPath(trimmed)) {
// If the result is successful, decrement the index, as we
// have now no longer seen the element at index i
i--;
}
else {
// If the result is unsuccessful, the removed element is an
// important character and must be added back in to the
// passphrase
smartReset();
resetCount++;
trimmed.add(i, removed);
//Set the best path equal to the reset path if the reset path is shorter
Path maybeBest = getMostRecentPath();
if (maybeBest.size() < best.size()) {
best = maybeBest;
}
}
}
return trimmed;
}
/**
* getMostRecentPath
*
* Gets the most recent path present in Episodic Memory
* @return The most recent path in episodic memory
*/
public Path getMostRecentPath() {
int lastGoal = findLastGoal(episodicMemory.size() - 2) + 1;
ArrayList<Character> pathChars = new ArrayList<Character>();
for (int i = lastGoal; i < episodicMemory.size(); i++) {
pathChars.add(episodicMemory.get(i).command);
}
return new Path(pathChars);
}
/**
* Resets the agent by having it act randomly until it reaches the goal.
* This will be changed to a more intelligent scheme later on
*/
public void reset() {
char toCheck;
boolean[] sensors;
int encodedSensorResult;
//Currently, the agent will just move randomly until it reaches the goal
//and magically resets itself
do {
toCheck = generateRandomAction();
sensors = env.tick(toCheck);
encodedSensorResult = encodeSensors(sensors);
episodicMemory.add(new Episode(toCheck, encodedSensorResult, INIT_STATE));
/*if (episodicMemory.size() > 500000000) {
System.exit(0);
}*/
} while (!sensors[IS_GOAL]); // Keep going until we've found the goal
}
/**
* Generates a random action for the Agent to take
*
* @return A random action for the Agent to take
*/
public char generateRandomAction() {
Random random = new Random();
return alphabet[random.nextInt(alphabet.length)];
}
/**
* A more intelligent reset for the agent that will cause the agent to try to find a path to the goal
* by examining its episodic memory
*/
public void smartReset() {
if (reorientation) {
boolean successCode;
for(int i = 0; i < MAX_RESETS; i++) {
successCode = smartResetHelper();
if (successCode) {
return;
}
}
reorientFailures++;
reset();
}
else {
reset();
}
}
/**
* An intelligent reset method for the agent that resets by searching its previous moves
*/
public boolean smartResetHelper() {
int matchedStringEndIndex = maxMatchedStringIndex();
char transitionCharacter;
boolean[] sensors;
int sensorEncoding;
int lastGoal = findLastGoal(episodicMemory.size()) + 1;
String action;
if (matchedStringEndIndex == -1) {
return false;
}
for (int i = matchedStringEndIndex + 1; i < lastGoal; i++) {
transitionCharacter = episodicMemory.get(i).command;
sensors = env.tick(transitionCharacter);
sensorEncoding = encodeSensors(sensors);
action = "" + transitionCharacter + sensorEncoding;
episodicMemory.add(new Episode(transitionCharacter, sensorEncoding, INIT_STATE));
if (sensorEncoding == GOAL) {
return true;
}
//System.err.println(episodicMemory.get(i) + " " + action);
if (!episodicMemory.get(i).equals(action)) {
//We're lost, so attempt another reset
return false;
}
//if (episodicMemory.size() > 5000000) {
// System.exit(0);
//}
}
return false;
}
/**
* mapStateMachine
*
* Continually makes moves until the state machine has been mapped
*
*/
private void mapStateMachine() {
char currCommand;
while (!mappingComplete()) {
currCommand = selectNextCommand();
makeMove(currCommand);
printStateMachine();
}
printStateMachine();
}
/**
* mappingComplete
*
* Checks to see if the mapping of the state machine has been completed
*
* @return True if the mapping is complete, else false
*/
private boolean mappingComplete() {
for (int i = 0; i < agentTransitionTable.size(); i++) {
//Skip the check if the current row has been deleted
if (agentTransitionTable.get(i)[0] == DELETED) {
continue;
}
//Make sure every space in the transition table has been filled
for (int j = 0; j < alphabet.length; j++) {
if (agentTransitionTable.get(i)[j] == UNKNOWN_TRANSITION) {
return false;
}
}
}
return true;
}
/**
* Finds the ending index of the longest substring in episodic memory before
* the previous goal matching the final string of actions the agent has
* taken
*
* @return The ending index of the longest substring matching the final string of actions
* the agent has taken
*/
private int maxMatchedStringIndex() {
int lastGoalIndex = findLastGoal(episodicMemory.size());
if (lastGoalIndex == -1) {
return -1;
}
//If we've just reached the goal, then there is nothing to match
if (lastGoalIndex == episodicMemory.size() - 1)
{
return -1;
}
//Find the longest matching subsequence
int maxStringIndex = -1;
int maxStringLength = 0;
int currStringLength;
for (int i = lastGoalIndex-1; i >= 0; i--) {
currStringLength = matchedMemoryStringLength(i);
if (currStringLength > maxStringLength) {
maxStringLength = currStringLength;
maxStringIndex = i+1;
}
}//for
if (maxStringIndex < 0) {
return 0;
}
else {
return maxStringIndex;
}
}//maxMatchedStringIndex
/**
* Starts from a given index and the end of the Agent's episodic memory and moves backwards, returning
* the number of consecutive matching characters
* @param endOfStringIndex The index from which to start the backwards search
* @return the number of consecutive matching characters
*/
private int matchedMemoryStringLength(int endOfStringIndex) {
int length = 0;
int indexOfMatchingAction = episodicMemory.size() - 1;
boolean match;
for (int i = endOfStringIndex; i >= 0; i--) {
//We want to compare the command from the prev episode and the
//sensors from the "right now" episode to the sequence at the
//index indicated by 'i'
char currCmd = episodicMemory.get(indexOfMatchingAction - 1).command;
int currSensors = episodicMemory.get(indexOfMatchingAction).sensorValue;
char prevCmd = episodicMemory.get(i).command;
int prevSensors = episodicMemory.get(i+1).sensorValue;
match = ( (currCmd == prevCmd) && (currSensors == prevSensors) );
if (match) {
length++;
indexOfMatchingAction--;
}
else {
return length;
}
}//for
return length;
}//matchedMemoryStringLength
/**
* Searches backwards through the list of move-result pairs from the given index
* @param toStart The index from which to start the backwards search
* @return The index of the previous goal
*/
private int findLastGoal(int toStart) {
for (int i = toStart - 1; i > 0; i --) {
if (episodicMemory.get(i).sensorValue == GOAL) {
return i;
}
}
return -1;
}
/**
* Takes in an agent's sensor data and encodes it into an integer
* @param sensors The agent's sensor data
* @return the integer encoding of that sensor data
*/
private int encodeSensors(boolean[] sensors) {
int encodedSensorResult;
if (sensors[IS_GOAL]) {
encodedSensorResult = GOAL;
}
else if (sensors[IS_NEW_STATE]) {
encodedSensorResult = TRANSITION_ONLY;
}
else {
encodedSensorResult = NO_TRANSITION;
}
return encodedSensorResult;
}
/**
* hasTransition
*
* A helper method to determine if one state has a transition to another
* @param fromState The state to transition from
* @param toState The state to tranisition to
* @return The index into the alphabet array for the transition character, or
* -1 if no such character exists
*/
private int hasTransition(int fromState, int toState) {
for (int i = 0; i < agentTransitionTable.get(fromState).length; i++) {
if (agentTransitionTable.get(fromState)[i] == toState) {
return i;
}
}
return -1;
}
/**
* makePlanToState
*
* creates a new plan to reach a given state (see {@link #currentPlan}) from
* a given state
*
* @param startID id of the state to start at
* @param targetID id of the state we want to reach
*/
private void makePlanToState(int startID, int targetID) {
//each path is a sequence of commands to reach the target
//state from the Nth state
String[] paths = new String[agentTransitionTable.size()];
for (int i = 0; i < paths.length; i++) {
paths[i] = "";
}
findAllPaths(paths, startID, targetID);
parsePathToPlan(paths, startID, targetID);
planIndex = -1;
//TODO: Debug
System.out.println("Plan from " + startID + " to " + targetID);
printPlan(currentPlan);
if (currentPlan == null) {
System.out.println("foo");
}
//if (currentPlan != null) System.exit(0);
}//makePlanToState
/**
* findAllPaths
*
* Helper function for makePlanToState that uses targetID to construct all paths
*
* @param paths array of possible paths to reach goal
* @param startID first destination to reach goal from
* @param targetID the state to reach/goal
*
*/
private void findAllPaths(String[] paths, int startID, int targetID){
//Create a queue that initially only contains the target state
ArrayList<Integer> queue = new ArrayList<Integer>();
queue.add(targetID);
int currState;
int transitionChar;
//loop through each state and add paths to reach currState to the queue
while (!queue.isEmpty()) {
//Grab the element at the front of the queue
currState = queue.remove(0);
//Move through each state that doesn't have a path yet. Find the
//transition from that state to the current state.
for (int i = 1; i < agentTransitionTable.size(); i++) {
//skip the ones that have a path
if (!paths[i].equals("")) continue;
transitionChar = hasTransition(i, currState);
//If state i has a transition to the current state and has no
//path, set the path for state i equal to the transition
//character from state i to the current state added to the front
//of the shortest path to the current state, and add state i
//onto the queue.
if (transitionChar != -1) {
paths[i] = alphabet[transitionChar] + paths[currState];
queue.add(i);
//if we find path to currstate from startID we can ignore the other states
if (i == startID) break;
}
}//for
//if we have found a path from the startID to the targetID we can ignore the other states
if (! paths[startID].equals("")) break;
}//while
}
/**
* parsePathToPlan
*
* Accepts path to state and converts it into a plan comprised of a series of episodic memories
*
* @param paths array of possible paths to reach goal
* @param startID first destination to reach goal from
* @param targetID the state to reach/goal
*
*/
private void parsePathToPlan(String[] paths, int startID, int targetID){
if (!paths[startID].equals("")) { //if there is a path, enter
ArrayList<Episode> plan = new ArrayList<Episode>();
String pathToParse = paths[startID];
int[] transitionRow = agentTransitionTable.get(startID);
int sensorValue = TRANSITION_ONLY;
int episodeState = startID;
//for each command in the path
for (int i = 0; i < pathToParse.length(); i++) {
//add the current episode
plan.add(new Episode(pathToParse.charAt(i), sensorValue, episodeState));
//define the next sensor values in the plan
//TODO: for now this isn't correct. The code only
//cares about whether the agent should sense goal or not. In
//the future, we should have the correct sensor values here
//so we can recognize if the expected sensor values don't
//match actual and abort the plan then rather than waiting
//until we should reach the goal
if (targetID == 0 && i == pathToParse.length() - 1) {
sensorValue = GOAL;
} else {
sensorValue = TRANSITION_ONLY;
}
//figure out what state the command takes us to
int charIndex = findAlphabetIndex(pathToParse.charAt(i));
if (charIndex == -1) {
System.out.println("character: " + pathToParse.charAt(i));
}
if (transitionRow[charIndex] == GOAL_STATE) {
episodeState = INIT_STATE; //magic teleport to goal
} else {
episodeState = transitionRow[charIndex];
}
//update to transition row assoc'd with new curr state
if (transitionRow[charIndex] != -1) {
transitionRow = agentTransitionTable.get(transitionRow[charIndex]);
}
}//for
//Tack the goal state on the end to complete the plan
plan.add(new Episode(UNKNOWN_COMMAND, sensorValue, episodeState));
//Voila!
currentPlan = plan;
//Any valid plan must have at least two steps
if (plan.size() < 2) {
currentPlan = null;
}
}//if
}
/**
* getFirstUnkown
*
* Given an index into the transition table, this method discovers the first
* unknown transition in that row in the table and
*
* @param rowIndex index of the row in the transition table
*
* @return the letter in the alphabet that corresponds to that entry or
* UNKNOWN_COMMAND if it was not found
*/
private char getFirstUnknown(int rowIndex) {
int[] row = agentTransitionTable.get(rowIndex);
if (row[0] == DELETED) {
return UNKNOWN_COMMAND;
}
for(int i = 0; i < row.length; ++i) {
if (row[i] == UNKNOWN_TRANSITION) {
return alphabet[i];
}
}
return UNKNOWN_COMMAND; // no unknown transition
}//getFirstUnknown
private char getUnknown(int rowIndex) {
char c = generateRandomAction();
int[] row = agentTransitionTable.get(rowIndex);
if (row[indexOfCharacter(c)] == UNKNOWN_TRANSITION) {
return c;
}
return getUnknown(rowIndex);
}
/**
* selectNextCommand
*
* returns the command the agent should take next depending upon its current
* state, progress, plan, knowledge etc.
*
* SIDE EFFECTS: a new plan may be created or the current plan advanced
*
* @return the command to take
*
*/
private char selectNextCommand() {
char cmd = ' '; //the command to return
if (currentPlan != null) {
int x = 2;
}
//If I've never found a path to the goal I can only act randomly until I
//find the goal
if (best == null) {
return generateRandomAction();
}
//%%%BUG: Sometimes the WRONG COMMAND is being returned when a plan exists.
//If I have an active plan, extract the next action from that plan
else if (currentPlan != null && currentPlan.size() != 0) {
Episode currEp = currentPlan.get(planIndex+1);
return currEp.command;
}
//If there is no plan, then select an action that I've never done before
//from the state that I believe I'm in (explore)
else {
for (int i = 0; i < alphabet.length; i++) {
if (agentTransitionTable.get(currentState)[i] == UNKNOWN_TRANSITION) {
cmd = getUnknown(currentState);
if (cmd != UNKNOWN_COMMAND) return cmd;
}
}
}
//Find and delete any unreachable states
for (int i = 2; i < agentTransitionTable.size(); i++) {
if (agentTransitionTable.get(i)[0] != DELETED) {
makePlanToState(INIT_STATE, i);
if (currentPlan == null) {
agentTransitionTable.get(i)[0] = DELETED;
}
}
}
//if we reach this point there is no unknown transition from the current
//state. Find the lowest numbered state that has an unknown transition
//and make a plan to get there
int state = 0;
while (cmd == UNKNOWN_COMMAND) {
state++;
cmd = getFirstUnknown(state);
}
//make a plan to reach that unknown state
this.currentPlan = null;
makePlanToState(this.currentState, state);
if (currentPlan != null) {
cmd = currentPlan.get(planIndex+1).command;
return cmd;
}
//if something went wrong just act randomly
//(I don't think this should ever happen.)
return generateRandomAction();
}
/**
* Returns the index of the given character in the alphabet array
* @param toCheck the character to find the index of
* @return the index of toCheck
*/
private int indexOfCharacter(char toCheck) {
for (int i = 0; i < alphabet.length; i++) {
if (alphabet[i] == toCheck) {
return i;
}
}
return -1;
}
/**
* acceptCurrentHypothesis
*
* the current hypothetic equivlency is not believed to be true. Update the
* list of known equivalents and also update the transition table
From the journal:
If were testing a state equivalency hypothesis, then your hypothesis
becomes "fact" and update your equivalencies list and the transition table
appropriately. As part of this merging you should check to see if there
any two rows that match exactly (unknown transitions don't match). If
there are, then they are the same state, merge them.
*/
private void acceptCurrentHypothesis() {
//Delete all states added while doing this plan, then reset the list
for (int i : addedInPlan) {
agentTransitionTable.get(i)[0] = DELETED;
}
//Replace states in previous episodes with correct ones
int k = 0;
for (int i = episodicMemory.size() - currentPlan.size(); i < episodicMemory.size(); i++) {
episodicMemory.get(i).stateID = currentPlan.get(k).stateID;
k++;
}
equivalentStates.add(currentHypothesis);
if (currentHypothesis[1] == INIT_STATE) {
currentHypothesis[1] = currentHypothesis[0];
currentHypothesis[0] = INIT_STATE;
}
mergeTwoStates(currentHypothesis[0], currentHypothesis[1]);
for (int i = 0; i < agentTransitionTable.size(); i++) {
for (int j = i + 1; j < agentTransitionTable.size(); j++) {
if (isCompatibleRow(agentTransitionTable.get(i), agentTransitionTable.get(j), true)) {
mergeTwoStates(i, j);
}
}
}
}
/**
* mergeTwoStates
*
* Takes two equivalent states and merges them together
*/
private void mergeTwoStates(int state1, int state2) {
//Merge the two states together
System.out.println("State " + state1 + " has been merged with State " + state2);
for (int i = 0; i < alphabet.length; i++) {
if (agentTransitionTable.get(state2)[i] != UNKNOWN_TRANSITION && agentTransitionTable.get(state1)[i] == UNKNOWN_TRANSITION) {
agentTransitionTable.get(state1)[i] = agentTransitionTable.get(state2)[i];
}
}
//Change all transitions to state2 to transitions to state1
for (int i = 0; i < agentTransitionTable.size(); i++) {
for (int j = 0; j < alphabet.length; j++) {
if (agentTransitionTable.get(i)[j] == state2) {
agentTransitionTable.get(i)[j] = state1;
}
}
}
//Mark state2 as deleted
agentTransitionTable.get(state2)[0] = DELETED;
}
/**
* cleanupFailedPlan
*
* if a plan fails, then the current hypotheses are assumed to be
* incorrect. The two states are added the {@link #nonEquivalentStates}
* list. A new state is added to the transition table and a new epmem is
* added to the episodic memory that indicates we transitioned to that
* state. this.currentState is also updated.
- if you were testing an equivalency then your
hypothesis becomes false. Record the non-equivalency
appropriately
- otherwise you must have been trying to get to a
state as per 7d above. I'm not sure what to do
here as this indicates that a previous equivalency
is actually false. Ignore for now.
*/
private void cleanupFailedPlan(int mergedSensors) {
//Reset the list of states added while following this plan
addedInPlan = new Vector<Integer>();
if(currentPlan.size() <= 1)
{
//ignore for now? reset?
System.out.println("Plan should never be of length 1!");
System.exit(-1);
}
//Add the current hypothesis to the list of non equivalent states if the hypothesis exists
if (currentHypothesis != null)
{
nonEquivalentStates.add(currentHypothesis);
}
currentHypothesis = null;
Episode lastEpisode = episodicMemory.get(episodicMemory.size() - 1);
char action = lastEpisode.command;
int lastState = lastEpisode.stateID;
int actionIndex = findAlphabetIndex(action);
//Remove the current plan and reset the plan index
currentPlan = null;
planIndex = -1;
}//cleanupFailedPlan
/**
* A helper method which determines a given letter's
* location in the alphabet
*
* @param letter
* The letter who's index we wish to find
* @return
* The index of the given letter (or -1 if the letter was not found)
*/
private int findAlphabetIndex(char letter) {
// Iterate the through the alphabet to find the index of letter
for(int i = 0; i < alphabet.length; i++){
if(alphabet[i] == letter)
return i;
}
// Error if letter is not found
return -1;
}
/**
* isCompatibleRow
*
* given two rows in the transition table, this method verifies that they
* are "compatible" i.e., all corresponding entries that are both not
* unknown are the same value.
*
*/
private boolean isCompatibleRow(int[] row1, int[] row2, boolean hypothesisMerged) {
//System.out.println("Checking if rows are compatible");
//A deleted row is incompatible with everything
if (row1[0] == DELETED || row2[0] == DELETED) {
return false;
}
//%%%IMPORTANT: If the rule that a state must have so many transitions to itself is removed, this will
//no longer function properly and will need to be changed
//The goal row should NEVER be merged
boolean goal1 = true;
boolean goal2 = true;
for (int i = 0; i < row1.length; i++) {
if (row1[i] != GOAL_STATE) {
goal1 = false;
}
if (row2[i] != GOAL_STATE) {
goal2 = false;
}
}
if (goal1 || goal2) {
return false;
}
boolean knownShared = false;
if (!hypothesisMerged) knownShared = true;
// Go through each entry in the rows to compare them
for(int i = 0; i < row1.length; i++) {
// If the rows are not equivalent
if(row1[i] != row2[i]) {
// And neither of them are unknown, the rows are not equivalent
if( !(row1[i] == UNKNOWN_TRANSITION || row2[i] == UNKNOWN_TRANSITION)){
return false;
}
}
// Ensure there is at least one known match between the two rows
//TODO: Put this back in? Removed by :AMN: and HNK because the current state is likely to
// be brand new and thus have -1 for all transitions out. How can this ever pass?
else if (row1[i] != UNKNOWN_TRANSITION && row2[i] != UNKNOWN_TRANSITION) {
knownShared = true;
}
}
return knownShared;
//return true;
}
/**
* makeMove
*
* issues a given command and updates the episodic memory, transition table,
* current plan, etc. as a result.
*
* @param cmd the command to issue
*/
private void makeMove(char cmd) {
//TODO: REMOVE (DEBUG)
System.out.println("Executing command: " + cmd);
possibleBest.add(cmd);
//Complete the current episode with the given command
Episode currEp = this.episodicMemory.get(this.episodicMemory.size() - 1);
currEp.command = cmd;
boolean[] sensors = env.tick(cmd);
int mergedSensors = encodeSensors(sensors);
int commandIndex = findAlphabetIndex(cmd);
if (mergedSensors == GOAL) {
if (best == null || possibleBest.size() < best.size()) {
best = new Path(possibleBest);
}
possibleBest = new ArrayList<Character>();
}
//if we're in the middle of a plan it needs to be updated
if (this.currentPlan != null && this.currentPlan.size() != 0) {
//Advance the plan and verify that the sensors match
this.planIndex++;
Episode currPlanEp = this.currentPlan.get(this.planIndex+1);