\$\begingroup\$

The FITTEST - Geometric mean score: ~ 922 (2K runs)

My approach is to:

Find out what kills the species and define desired behaviour (functional) Implement desired behaviour in code (technical) Give it a priority. Is it more important or less important than other desired behaviour. Optimize the Geometric mean score by tweaking parameters of the solutions.

I tested over 2000 sets of parameters with the same 50 seeds. The most promising sets were selected and were scored using 250 identical seeds and the ones with the highest rank were the input for the next round of test. So I managed to create a genetic algorithm to find the optimal genetic algorithm for this problem as suggested by user mbomb007.

The desired behaviour:

The species should learn which colours are safe and which are bad. The species should mainly focus its decision where to move to based on the 3 cells right in front, but if no good moves are available, vertical or backward moves should be considered The species should also look what is beyond the 8 cells around him and use that in information in decision making The species should learn to identify traps. Some species incorrectly assume that walls are good and try to move to them all the time and therefore get stuck in front of walls. If they are the species with the highest fittest score at that moment their DNA with the wrong assumption about the wall is duplicated many times into newborns. After some time all species are stuck in front of walls and none of them reaches the goal to score points. How to stop the morons?

Data storage methods:

We want the species to learn things, to adapt to its environment, to become the fittest. Inevitably this only works, if learning's can be stored somehow. The learning will be 'stored' in the 100 DNA bits. It is a strange way of storing, because we can not change the value of our DNA. So we assume that the DNA already stores information of bad and good moves. If for a certain species the correct information is stored in its DNA he will move fast forward and produce many new species with its DNA.

I found out the geometric mean score is sensitive to how the information is stored. Lets assume we read the first 4 bits of the 100 bits of DNA and want to store this in an integer variable. We can do this in several ways:

decimal data storage: by using the 'built-in' dnarange function, example: 4bits 1011 will become `1x2^3 + 0x2^2 + 1x2^1 + 1x2^0 = 15. Possible values (for 4 bits): [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] streaks data storage: by using the dnaStreakRange function (defined below), example: 4bits 1011 will become 1x1 + 0x1 + 1x1+ 1x2 = 4 . Possible values (for 4 bits): [0, 1, 2, 3, 6, 10]

int dnaStreakRange(dna_t &d, int start, int chunklen) { int score = 0; int streak = 0; for(int i = 0; i < chunklen; i++) { if(d[start + i]) score += ++streak; else streak = 0; }; return score; }

bitsum data storage: by using the dnaCountRange function (defined below), example: 4bits 1011 will become 1x1 + 0x1 + 1x1 + 1x1 = 3 . Possible values (for 4 bits): [0, 1, 2, 3, 4]

int dnaCountRange(dna_t &d, int start, int chunklen) { int score = 0; for(int i = 0; i < chunklen; i++) { if(d[start + i]) score ++; }; return score; }

Difference between storage methods are:

The decimal storage method is vulnerable for a single bit change to the DNA. When the bitsum value changes from 1011 to 0011 its value changes from 3 to 2 which is a minor change.

to the DNA. When the bitsum value changes from 1011 to 0011 its value changes from 3 to 2 which is a minor change. The decimal storage method is homogeneous. Each of the possible values has the same change to occur. The chance that you read the value of 15 from a 4 bit storage memory block is 1/16=6%. The streak storage method is not homogeneous. The chance that a streak 4 bit value is less or equal that 6 is (15-3)/16=81% (all 16 combinations except 0111,1110,111). Below a visual which shows the shape of the distribution. As you can see at the blue arrow the chance of a 4 bit streak to be less or equal to 6 is 81%:

Prioritize solutions.

When the ColorScorePlayer has identified two forward moves with identical scores an arbitrary choice is made. IMHO, you should never use the random function v.rng.rint() function. Instead you should use this opportunity of equal scores as a hook to evaluate solutions for second order effects.

First order effect get the highest priority. If equal scores are reaches, the solution with the priority 2 prevails and so on. By tweaking the parameters of a solution you can influence the chance that equal scores occur and in that way change weight of priority 1 and priority 2 solutions.

Implementation of desired behaviour

Learn which colours are safe:

33% of the 16 colors are bad and therefore when the score of a move is below 63/3 the move will not be allowed. Therefore threshold = 63/3=21 , where 63 is the max score for 6 bits and 33%=1/3 (can be looked up in the graph above).

If no good moves are available, move vertical or backward:

When no forward moves are allowed, vertical moves will be compared with each other in the same way. If also no vertical moves are allowed, the backward moves are ranked. This is achieved via the weightMove variable.

Look what is beyond:

When 2 or 3 moves have identical scores, the 3x3 box around those moves will determine (via x2 and y2 loops) what is the best option (via the mainSubScore variable). The most right column in that 3x3 box is leading.

coord_t adjustedColorPlayer(dna_t d, view_t v) { const int chunklen = 6,threshold = 63/3; int xBestScore=0, yBestScore=0; long bestScore=-1, weightMove, weightMove2, mainScore; for(int x = -1; x <= 1; x++) { if (x < 0) weightMove = 1000; // moving backward if (x== 0) weightMove = 10000; //moving vertical if (x > 0) weightMove = 100000; //moving forward for(int y = -1; y <= 1; y++) { if(v(x, y) == OUT_OF_BOUNDS || (x==0&&y==0) ) continue; mainScore = dnarange(d,v(x,y)*chunklen,chunklen); if (mainScore<threshold+1) { mainScore = 0; //not a suitable move because too low score }else{ mainScore*= weightMove; // when equal score, use sub score by examining 5x5 box to rank moves for(int x2 = x-1; x2 <= x+1; x2++){ if (x2 < x) weightMove2 = 1; // moving backward if (x2== x) weightMove2 = 10; //moving vertical if (x2 > x) weightMove2 = 100; //moving forward for(int y2 = x-1; y2 <= y+1; y2++){ if(v(x2, y2) != OUT_OF_BOUNDS){ long mainSubScore = dnarange(d,v(x2,y2)*chunklen,chunklen); if (mainSubScore>=threshold+1) mainScore+=mainSubScore*weightMove2; } } } } if(mainScore > bestScore) { bestScore = mainScore; xBestScore = x; yBestScore = y; } } } return{xBestScore,yBestScore}; }

Score: 123(2K runs) First 50 Scores (18 games have scored only 1 point): 1 10 1 79947 3 1 11 125 7333287 23701 310869 53744 1 2 2 2 2 1 1 57556 2 688438 60 1 2 2636261 26306 1 125369 1 1 1 61895 27 1 36 1 91100 87636 1 2 47497 53 16 1 11 222384 1 1 1

Identify traps:

I examined the DNA of the species with the highest score when an arbitrary game ended using the a bitsum4 storage (so colour score has range [0,4]):

scored 0: Teleport backward, both walls, 1x safe

scored 1: Trap backward (so harmless), Teleport backward, 1x safe

scored 2: Trap forward (so dangerous), 1x safe

scored 3: Teleport forward, 5x safe

scored 4: Teleport forward, 1x safe

From this it can be concluded that walls and teleports get a correct score. Traps are not identified since they are depending on direction and on color of origin, while scoring is done on color of destination. Therefore there is a need for also storing data on the color of origin, so v(0,0) . In an ideal world we would like to store information for 16 colors x 8 directions x 3 bits = 384 bits.

Unfortunately, there is only 100 bits available and we can not use it all since we also need some memory for the solution explained above. Therefore we will make 4 colour bins:

0: colour 0 - colour 3,

1: colour 4 - colour 7,

2: colour 8 - colour 11,

3: colour 12 - colour 16

and 4 move direction bins

0: move vertical or backward,

1: move forward up,

2: move forward,

3: move forward down

When the decimal score is 4 or higher (100,101,110,111), it is assumed a trap is associated with this cell, as a result this move will not be picked when equal scores arise. So trap identification is a second order effect and the 'look what's beyond' will be a third priority solution.

int dnaLookup2(dna_t &d, int start, int chunklen, int storageMethod) { // Definition of storageMethod: 0=decimal, 1=streak, 2=bitsum int score = 0, streak = 0; for(int i = start; i < start+chunklen; i++) { int value = d[i]; if (storageMethod==0) { score = (score << 1) |value; }else{ if (storageMethod==1){ if(value) score += ++streak; else streak = 0; }else{ if(value) score ++; } } }; return score; } coord_t theTrapFighter(dna_t d, view_t v) { // Definition of storageMethod: 0=decimal, 1=streak, 2=bitsum const int colorMemStorageMethod = 1, colorMemBlockSize = 3; const int trapMemStorageMethod = 0, trapMemBlockSize = 3; const int trapMemTopThreshold = 4, nDirBins = 4, nColorBins = 4; int xBestScore=0, yBestScore=0; long bestScore=-1, weightMove, weightMove2, mainScore; for(int x = -1; x <= 1; x++) { if (x < 0) weightMove = 1000; // moving backward if (x== 0) weightMove = 10000; //moving vertical if (x > 0) weightMove = 100000; //moving forward for(int y = -1; y <= 1; y++) { int color = v(x, y); if(color == OUT_OF_BOUNDS || (x==0&&y==0) ) continue; mainScore = dnaLookup2(d,color*colorMemBlockSize, colorMemBlockSize,colorMemStorageMethod); if (mainScore==0) { //not a suitable move because too low score }else{ mainScore*= weightMove; //lookup trap likelihood int directionBin = 0; if (nDirBins==3) directionBin = x>0?y+1:-1; if (nDirBins==4) directionBin = x>0?y+2:0; // put 16 colors in nColorBins bins int colorBin = v(0,0)*nColorBins/N_COLORS; colorBin = colorBin>(nColorBins-1)?(nColorBins-1):colorBin; if (directionBin >= 0 && dnaLookup2( d, colorMemBlockSize*16 +trapMemBlockSize*(nColorBins*directionBin+colorBin), trapMemBlockSize, trapMemStorageMethod ) >=trapMemTopThreshold){ //suspect a trap therefore no sub score is added }else{ // when equal score, use sub score by examining 5x5 box to rank moves for(int x2 = x-1; x2 <= x+1; x2++){ if (x2 < x) weightMove2 = 1; // moving backward if (x2== x) weightMove2 = 10; //moving vertical if (x2 > x) weightMove2 = 100; //moving forward for(int y2 = x-1; y2 <= y+1; y2++){ int color2 = v(x2, y2); if(color2 != OUT_OF_BOUNDS){ mainScore+=weightMove2 * dnaLookup2(d,color2*colorMemBlockSize, colorMemBlockSize,colorMemStorageMethod); } } } } } if(mainScore > bestScore) { bestScore = mainScore; xBestScore = x; yBestScore = y; } } } return{xBestScore,yBestScore}; }

Score: 580 (2K runs) First 50 Scores (13 games have scored only 1 point): 28,044 14,189 1 2,265,670 2,275,942 3 122,769 109,183 401,366 61,643 205,949 47,563 138,680 1 107,199 85,666 31 2 29 1 89,519 22 100,908 14,794 1 3,198,300 21,601 14 3,405,278 1 1 1 2 74,167 1 71,242 97,331 201,080 1 1 102 94,444 2,734 82,948 1 4 19,906 1 1 255,159

Wrong assumption about the wall is duplicated many times into newborns by morons:

Some species incorrectly assume that walls are good and try to move to them all the time and therefore get stuck in front of walls. They can also get stuck in infinite loops of teleporters. The effect is the same in both cases.

The main problem is that after a few hundred iterations some genes get very dominant. If these are the 'right' genes, you can get very high scores (>1 million points). If these are wrong, you are stuck, since you need the diversity to find the 'right' genes.

Morons fighting: Solution 1: colour inversion

The first solution I tried was an effort to utilize a part of unused memory which is still very diverse. Lets assume you have allocated 84 bits to your colour memory and trap finding memory. The remaining 16 bits will be very diverse. We can fill 2 decimal8 variables which have values on the interval [0,255] and they are homogeneous, which means that each value has a chance of 1/256. The variables will be called inInverse and inReverse .

If inInverse equals 255 (a 1/256 chance), then we will inverse the interpretation of the colour scores. So the wall which the moron assume to be safe because of it is high score, will get a low score and therefore will become a bad move. The disadvantage is that this will also effect the 'rights' genes, so we will have less very high scores. Furthermore this inInverse species will have to reproduce itself and its children will also get parts of the dominant DNA. The most important part is that it brings back the diversity.

If inReverse equals 255 (a 1/256 chance), then we will reverse the order of the storage positions of the colour scores. So before colour 0 was stored in bits 0-3. Now colour 15 will be stored in that position. The difference with the inInverse approach is that the inReverse will undo the work done so far. We are back at square one. We have created a species which has similar genes as when the game started (except for the trap finding memory)

Via optimization it is tested if it is wise to use the inInverse and inReverse at the same time. After the optimization it was concluded the score did not increase. The problem is that we have more diverse gen population, but this also affects the 'right DNA'. We need another solution.

Morons fighting: Solution 2: hash code

The species has 15 possible starting position and currently there is a too large chance he will follow exactly the same path if he starts at the same starting position. If he is a moron who loves walls, he will get stuck on the same wall over and over again. If he by luck managed to reach a far ahead wall, he will start to dominate the DNA pool with his wrong assumptions. What we need is that his offspring will follow a slightly different path (since for him it is too late anyway), and will not get stuck on the far ahead wall, but on a more nearby wall. This can be achieved by introducing a hashcode.

A hashcode should have the purpose to uniquely identify and label the current position on the board. The purpose is not to find out what the (x,y) position is, but to answer the questions have my ancestors been before on this location?

Let's assume you would have the complete board in front of you and you would make a jpg of each 5 by 5 cell possible square. You would end up with (53-5)x(15-5)=380 images. Let's give those images numbers from 1 to 380. Our hashcode should be seen as such an ID, with that different that it does not run from 1 to 330, but has missing IDS, so e.g. 563, 3424, 9424, 21245, etc.

unsigned long hashCode=17; for(int x = -2; x <= 2; x++) { for(int y = -2; y <= 2; y++) { int color = v(x, y)+2; hashCode = hashCode*31+color; } }

The prime numbers 17 and 31 are in there to prevent the information added in the beginning in the loop to disappear. Later more on how to integrate our hashcode into the rest of the program.

Lets replace the "look what's beyond" subscoring mechanism with another subscoring mechanism. When two or three cells have equal main scores there will be a 50% chance the top one will be picked, a 50% chance that the bottom cells is picked and a 0% chance that the middle one will be picked. The chance will not be determined by the random generator, but by bits from the memory, since in that way we make sure that in the same situation the same choice is made.

In an ideal world (where we have an infinite amount of memory), we would calculate a unique hashcode for our current situation, e.g. 25881, and go to memory location 25881 and read there if we should pick the top or bottom cell (when there is an equal score). In that way we would be in the exact same situation (when we e.g. travel the board for the second time and start at the same position) make the same decisions. Since we do not have infinite memory we will apply a modulo of the size of the available memory to the hashcode. The current hashcode is good in the sense that the distribution after the modulo operation is homogeneous.

When offspring travels the same board with slightly changed DNA he will in most cases (>99%) make exactly the same decision. But the further he comes the larger the chance becomes that his path fill be different from his ancestors. So the chance that he will get stuck on this far ahead wall is small. While getting stuck on the same nearby wall as his ancestor is relatively large, but this is not so bad, since he will not generate much offspring. Without the hashcode approach, the chance of getting stuck on the nearby and far away wall is almost the same

Optimization

After the optimization, it was concluded that the trap identification table is not needed and 2 bits per color is sufficient. The remainder of the memory 100-2x16=68 bits is used to store the hash code. It seems that the hash code mechanism is able to avoid traps.

I have optimized for 15 parameters. This code included the best set of tweaked parameters (so far):

int dnaLookup(dna_t &d, int start, int chunklen, int storageMethod,int inInverse) { // Definition of storageMethod: 0=decimal, 1=streak, 2=bitsum int score = 0; int streak = 0; for(int i = start; i < start+chunklen; i++) { int value = d[i]; if (inInverse) value = (1-d[i]); if (storageMethod==0) { score = (score << 1) |value; }else{ if (storageMethod==1){ if(value) score += ++streak; else streak = 0; }else{ if(value) score ++; } } }; return score; } coord_t theFittest(dna_t d, view_t v) { // Definition of storageMethod: 0=decimal, 1=streak, 2=bitsum const int colorMemStorageMethod = 2, colorMemBlockSize = 2, colorMemZeroThreshold = 0; const int useTrapMem = 0, trapMemStorageMethod = -1, trapMemBlockSize = -1; const int trapMemTopThreshold = -1, nDirBins = -1, nColorBins = -1; const int reorderMemStorageMethod = -1, reorderMemReverseThreshold = -1; const int reorderMemInverseThreshold = -1; // Definition of hashPrority: -1: no hash, 0:hash when 'look beyond' scores equal, // 1: hash replaces 'look beyond', 2: hash replaces 'trap finder' and 'look beyond' // 3: hash replaces everything ('color finder', 'trap finder' and 'look beyond') const int hashPrority = 2; int inReverse = reorderMemReverseThreshold != -1 && (dnaLookup(d,92,8,reorderMemStorageMethod,0) >= reorderMemReverseThreshold); int inInverse = reorderMemInverseThreshold != -1 && (dnaLookup(d,84,8,reorderMemStorageMethod,0) >= reorderMemInverseThreshold); int trapMemStart=N_COLORS*colorMemBlockSize; unsigned long hashCode=17; int moveUp=0; if (hashPrority>0){ for(int x = -2; x <= 2; x++) { for(int y = -2; y <= 2; y++) { int color = v(x, y)+2; hashCode = hashCode*31+color; } } unsigned long hashMemStart=N_COLORS*colorMemBlockSize; if (useTrapMem==1 && hashPrority<=1) hashMemStart+=nDirBins*nColorBins*trapMemBlockSize; if (hashPrority==3) hashMemStart=0; int hashMemPos = hashCode % (DNA_BITS-hashMemStart); moveUp = dnaLookup(d,hashMemStart+hashMemPos,1,0,inInverse); } int xBestScore=0, yBestScore=0; long bestScore=-1, weightMove, weightMove2, mainScore; for(int x = -1; x <= 1; x++) { if (x < 0) weightMove = 1000; // moving backward if (x== 0) weightMove = 10000; //moving vertical if (x > 0) weightMove = 100000; //moving forward for(int y = -1; y <= 1; y++) { int color = v(x, y); if (inReverse) color = 15-v(x, y); if(color == OUT_OF_BOUNDS || (x==0&&y==0) ) continue; //when MoveUp=1 -> give move with highest y most points (hashScore=highest) //when MoveUp=0 -> give move with lowest y most points (hashScore=lowest) int hashScore = (y+2)*(2*moveUp-1)+4; mainScore = dnaLookup( d, color*colorMemBlockSize, colorMemBlockSize, colorMemStorageMethod, inInverse ); if (mainScore<colorMemZeroThreshold+1) { mainScore = 0; //not a suitable move because too low score }else{ mainScore*= weightMove; //lookup trap likelihood int directionBin = 0; if (nDirBins==3) directionBin = x>0?y+1:-1; if (nDirBins==4) directionBin = x>0?y+2:0; // put 16 colors in nColorBins bins int colorBin = v(0,0)*nColorBins/N_COLORS; if (inReverse) colorBin = (15-v(0,0))*nColorBins/N_COLORS; colorBin = colorBin>(nColorBins-1)?(nColorBins-1):colorBin; if (useTrapMem && directionBin >= 0 && dnaLookup( d, trapMemStart+trapMemBlockSize*(nColorBins*directionBin+colorBin), trapMemBlockSize, trapMemStorageMethod, 0 )>=trapMemTopThreshold){ //suspect a trap therefore no sub score is added }else{ if (hashPrority>=1){ mainScore+=hashScore; } else{ // when equal score, use sub score by examining 5x5 box to rank moves for(int x2 = x-1; x2 <= x+1; x2++){ if (x2 < x) weightMove2 = 1; // moving backward if (x2== x) weightMove2 = 10; //moving vertical if (x2 > x) weightMove2 = 100; //moving forward for(int y2 = x-1; y2 <= y+1; y2++){ int color2 = v(x2, y2); if (inReverse) color2 = 15-v(x2, y2); if(color2 != OUT_OF_BOUNDS){ long mainSubScore = dnaLookup( d, color2*colorMemBlockSize, colorMemBlockSize, colorMemStorageMethod, inInverse ); if (mainSubScore>=colorMemZeroThreshold+1){ mainScore+=mainSubScore*weightMove2; } } } } } } } if (hashPrority==2 || (useTrapMem<=0 && hashPrority>=1)) mainScore+=hashScore*10; if (hashPrority==3) mainScore=hashScore*weightMove; if(mainScore > bestScore) { bestScore = mainScore; xBestScore = x; yBestScore = y; } } } return{xBestScore,yBestScore}; }

Score: 922 (2K runs) First 50 Scores (9 games have scored only 1 point): 112,747 3 1 1,876,965 8 57 214,921 218,707 2,512,937 114,389 336,941 1 6,915 2 219,471 74,289 31 116 133,162 1 5 633,066 166,473 515,204 1 86,744 17,360 2 190,697 1 6 122 126,399 399,045 1 4,172,168 1 169,119 4,990 77,432 236,669 3 30,542 1 6 2,398,027 5 1 2 8

This is my very first C++ program. I have as most of you now background in gnome analysis. I want to thank the organizers, since I really enjoyed working on this.

If you have any feedback, please leave a comment below. Apologies for the long texts.