# A game of atomic proportions

Your task make a bot that plays Atomas, with the highest score.

## How the game works:

The gameboard starts with a ring of 6 "atoms", with numbers ranging from 1 to 3. You can "play" an atom between two atoms, or on another atom, depending on the atom itself.

You can either have a normal atom, or a special atom.

### The normal atom:

You can play a normal atom between any two available atoms on the board.

You start off with atoms in the range 1 to 3, but the range increases by 1 once every 40 moves (so after 40 moves, the range becomes 2 to 4).

If there are atoms on the board that are lower than the range, it has a 1 / no. of atoms of that number on the board chance of spawning.

Let's say you have a 2 to play, and the board looks like this:

   1 1 2 1


Let's place the 2 to the right of the 1.

The board now becomes:

   1 1 2 1 2


Note: the board wraps around, so the 1 on the far left is actually next to the 2 on the far right. This will be important later.

There are 4 types of "special" atoms, and they are:

### The + atom:

This atom is played between two atoms. It has a 1 in 5 chance of spawning.

If the atoms on both sides of the + atom are the same, fusion occurs. Here's how it works:

The two atoms fuse together to create an atom one higher.
(So, two 3 atoms fuse together to form one 4 atom.)
While the atoms on both sides of the fused atom are equal:
If the atoms on the side >= the fused atom:
The new fused atom = the old fused atom's value + 2.
If the atoms on the side < the fused atom:
The new fused atom = the old fused atom's value + 1.


Example:

   1 1 3 2 2 3  (the 1 on the left-hand side "wraps back"
to the 3 on the right-hand side)

Let's use the + on the two 2's in the middle.

-> 1 1 3 3 3    (the two 2's fused together to make a 3)
-> 1 1 5        (the two 3's fused with the 3, and because 3 >= 3,
the new fused atom = 3 + 2 = 5)
-> 6            (the two 1's fused with the 5, since the board wraps,
and because 1 < 5, the new fused atom = 5 + 1 = 6)

Because the atoms on the sides of the 6 don't exist, fusion stops,
and the board is now [6].


If the atoms on both sides of the + atom are different, then the + stays on the board.

Example:

   1 3 2 3 1 1

Let's use the + on the 2 and 3 in the middle.

-> 1 3 2 + 3 1 1 (2 != 3, so the + stays on the board)


### The - atom:

This atom is played on another atom. It has a 1 in 10 chance of spawning.

The - atom removes an atom from the board, and gives you a choice to either:

• play the removed atom next round, or
• turn it into a + atom to play next round.

Example:

   1 3 2 3 1 1

Let's use the - on the left-hand 2.

-> 1 3 3 1 1    (the 2 is now removed from the board)

Let's turn it into a +, and place it in between the 3's.

-> 1 4 1 1      (the two 3's fused together to make a 4)
-> 5 1          (the two 1's fused with the 4, and because 1 < 4,
the new fused atom = 4 + 1 = 5)


### The black + atom (B):

This atom is played between 2 atoms. It has a 1 in 80 chance of spawning, and only spawns once your score > 750.

This atom is basically the same as the + atom, except that it fuses any two atoms together, even +'s. From then on, it follows the + rule (it only fuses atoms together if the atoms on both sides of the fused atom are equal).

The fused atom as a result of the black + is equal to:

• the higher number atom in the fusion + 3
• 4 if the two fused atoms are +'s

Example:

   1 3 2 1 3 1

Let's use the black + on the 2 and 1 in the middle.

-> 1 3 5 3 1    (the 2 and 1 fused together to make a 2 + 3 = 5)
-> 1 6 1        (+ rule)
-> 7            (+ rule)


Another example:

   2 + + 2

Let's use the black + on the two +'s.

-> 2 4 2        (the two +'s fused together to make a 4)
-> 5            (+ rule)


### The clone atom (C):

This atom is played on another atom. It has a 1 in 60 chance of spawning, and only spawns once your score > 1500.

The clone atom allows you to choose an atom, and play it next round.

Example:

   1 1 2 1

Let's use the clone on the 2, and place it to the right of the 1.

-> 1 1 2 1 2


Here is my build of the game, in Python 2:

import random
import subprocess

logs='atoms.log'
atom_range = [1, 3]
board = []
score = 0
move_number = 0
carry_over = " "
previous_moves = []

specials = ["+", "-", "B", "C"]

def plus_process(user_input):
global board, score, previous_moves, matches
previous_moves = []
matches = 0

def score_calc(atom):
global score, matches
if matches == 0:
score += int(round((1.5 * atom) + 1.25, 0))
else:
if atom < final_atom:
outer = final_atom - 1
else:
outer = atom
score += ((-final_atom + outer + 3) * matches) - final_atom + (3 * outer) + 3
matches += 1

if len(board) < 1 or user_input == "":
board.append("+")
return None
board_start = board[:int(user_input) + 1]
board_end = board[int(user_input) + 1:]
final_atom = 0
while len(board_start) > 0 and len(board_end) > 0:
if board_start[-1] == board_end[0] and board_end[0] != "+":
if final_atom == 0:
final_atom = board_end[0] + 1
elif board_end[0] >= final_atom:
final_atom += 2
else:
final_atom += 1
score_calc(board_end[0])
board_start = board_start[:-1]
board_end = board_end[1:]
else:
break
if len(board_start) == 0:
while len(board_end) > 1:
if board_end[0] == board_end[-1] and board_end[0] != "+":
if final_atom == 0:
final_atom = board_end[0]
elif board_end[0] >= final_atom:
final_atom += 2
else:
final_atom += 1
score_calc(board_end[0])
board_end = board_end[1:-1]
else:
break
if len(board_end) == 0:
while len(board_start) > 1:
if board_start[0] == board_start[-1] and board_start[0] != "+":
if board_start[0] >= final_atom:
final_atom += 2
else:
final_atom += 1
score_calc(board_start[0])
board_start = board_start[1:-1]
else:
break
if matches == 0:
board = board_start + ["+"] + board_end
else:
board = board_start + [final_atom] + board_end
for a in range(len(board) - 1):
if board[a] == "+":
if board[(a + 1) % len(board)] == board[a - 1]:
board = board[:a - 1] + board[a:]
plus_process(a)
break

def minus_process(user_input, minus_check):
global carry_over, board
carry_atom = board[int(user_input)]
if user_input == len(board) - 1:
board = board[:-1]
else:
board = board[:int(user_input)] + board[int(user_input) + 1:]
if minus_check == "y":
carry_over = "+"
elif minus_check == "n":
carry_over = str(carry_atom)

def black_plus_process(user_input):
global board
if board[int(user_input)] == "+":
if board[int(user_input) + 1] == "+":
inter_atom = 4
else:
inter_atom = board[int(user_input) + 1] + 2
else:
if board[int(user_input)] + 1 == "+":
inter_atom = board[int(user_input)] + 2
else:
inter_list = [board[int(user_input)], board[int(user_input) + 1]]
inter_atom = (inter_list.sort())[1] + 2
board = board[int(user_input) - 1:] + [inter_atom] * 2 + board[int(user_input) + 1:]
plus_process(int(user_input) - 1)

def clone_process(user_input):
global carry_over
carry_over = str(board[int(user_input)])

def regular_process(atom,user_input):
global board
if user_input == "":
board.append(random.randint(atom_range[0], atom_range[1]))
else:
board = board[:int(user_input) + 1] + [int(atom)] + board[int(user_input) + 1:]

def gen_specials():
special = random.randint(1, 240)
if special <= 48:
return "+"
elif special <= 60 and len(board) > 0:
return "-"
elif special <= 64 and len(board) > 0 and score >= 750:
return "B"
elif special <= 67 and len(board) > 0 and score >= 1500:
return "C"
else:
small_atoms = []
for atom in board:
if atom not in specials and atom < atom_range[0]:
small_atoms.append(atom)
small_atom_check = random.randint(1, len(board))
if small_atom_check <= len(small_atoms):
return str(small_atoms[small_atom_check - 1])
else:
return str(random.randint(atom_range[0], atom_range[1]))

def specials_call(atom, user_input):
specials_dict = {
"+": plus_process,
"-": minus_process,
"B": black_plus_process,
"C": clone_process
}
if atom in specials_dict.keys():
if atom == "-":
minus_process(user_input[0], user_input[1])
else:
specials_dict[atom](user_input[0])
else:
regular_process(atom,user_input[0])

def init():
global board, score, move_number, carry_over, previous_moves
board = []
score = 0

for _ in range(6):
board.append(random.randint(1, 3))

while len(board) <= 18:
move_number += 1
if move_number % 40 == 0:
atom_range[0] += 1
atom_range[1] += 1
if carry_over != " ":
special_atom = carry_over
carry_over = " "
elif len(previous_moves) >= 5:
special_atom = "+"
else:
special_atom = gen_specials()
previous_moves.append(special_atom)
bot_command = "python yourBot.py"
bot = subprocess.Popen(bot_command.split(),
stdout = subprocess.PIPE,
stdin = subprocess.PIPE)
to_send="/".join([
# str(score),
# str(move_number),
str(special_atom),
" ".join([str(x) for x in board])
])
bot.stdin.write(to_send)
with open(logs, 'a') as f:f.write(to_send+'\n')
bot.stdin.close()
specials_call(special_atom, all_user_input)

print("Game over! Your score is " + str(score))

if __name__ == "__main__":
for a in range(20):
with open(logs, 'a') as f:f.write('round '+str(a)+'-'*50+'\n')
init()


## How the bot thing works:

### Input

• Your bot will get 2 inputs: the atom that is currently in play, and the state of the board.
• The atom will be like so:
• + for a + atom
• - for a - atom
• B for a Black + atom
• C for a Clone atom
• {atom} for a normal atom
• The state of the board will be like so:
• atom 0 atom 1 atom 2... atom n, with the atoms separated by spaces (atom n wraps back to atom 1, to simulate a "ring" gameboard)
• These two will be separated by a /.

Example inputs:

1/1 2 2 3   (the atom in play is 1, and the board is [1 2 2 3])
+/1         (the atom in play is +, and the board is [1] on its own)


### Output

• You will output a string, depending on what the atom in play is.

• If the atom is meant to be played between two atoms:

• Output the gap you want to play the atom in. The gaps are like in between each atom, like so:

atom 0, GAP 0, atom 1, GAP 1, atom 2, GAP 2... atom n, GAP N


(gap n indicates you want to place the atom between atom 1 and atom n) So output 2 if you want to play the atom on gap 2.

• If the atom is meant to be played on an atom:
• Output the atom you want to play it on, so 2 if you want to play the atom on atom 2.
• If the atom is a -:
• Output the atom you want to play it on, followed by a space, followed by a y/n choice of turning the atom into a + later, so 2, "y" if you want to play the atom on atom 2, and you want to turn it into a +. Note: this requires 2 inputs, instead of 1.

Example outputs:

(Atom in play is a +)
2   (you want to play the + in gap 2 - between atom 2 and 3)
(Atom in play is a -)
3 y  (you want to play the - on atom 3, and you want to change it to a +)
2 n  (you want to play the - on atom 2, and you don't want to change it)

• To make the bot work, you have to go to the Popen bit (at around the end of the code), and replace it with whatever makes your program run as a Pythonic list (so if your program is derp.java, replace ["python", "bot.py"] with ["java", "derp.java"]).

• Place the entire code of your bot into the answer. If it doesn't fit, it doesn't count.
• Each user is allowed to have more than 1 bot, however, they should all be in separate answer posts.
• Also, give your bot a name.

## Scoring:

• The bot with the highest score wins.
• Your bot will be tested for 20 games, and the final score is the average of the 20 games.
• The tie-breaker will be the time of the upload of the answer.

{language}, {bot name}
Score: {score}


Good luck!

• How does the generated + for a - atom work ? If you chose y will you be guaranteed to get a + on the next move ? – Ton Hospel Oct 11 '16 at 10:01
• I suggest changing your bot driver so it can handle any standalone program that takes input on STDIN and gives a result on STDOUT. That should give language independence and most languages used on this site can easily do that. Of course this means defining a strict I/O format, e.g. input_atom\natom0 atom1 .... atomn\n for STDIN – Ton Hospel Oct 11 '16 at 10:08
• The code seems to be able to put + in the element list, but this is nowhere found in the textual description – Ton Hospel Oct 11 '16 at 16:25
• Ah, I see you made the program able to call external bots. However, you also need to pass the current move number and score on STDIN otherwise the bot cannot predict the chances of each atom happening in the future – Ton Hospel Oct 18 '16 at 14:15
• Idk if people will spend time creating a solution if the controller isn't improved. I like the question, but not the implementation. – mbomb007 Nov 2 '16 at 16:14

## Python, draftBot, Score = 889

import random
def h(b):
s=0
for x in b:
try:
s+=int(x)
except:
s+=0
return s
def d(i):g=i.split("/");a=g[0];b=g[1].split(" ");return(a,b)
def p(a,_,j):
v=[]
for x in _:
try:
v.append(int(x))
except:
v.append(0)
try:
v=v[:j+1]+[int(a)]+v[j+1:]
except:
v=v[:j+1]+[a]+v[j+1:]
r1=[[]];b=[x for x in v];m=range(len(b)+1)
for k in m:
for i in m:
for j in range(i):
c = b[j:i + 1]
if len(c)%2==0 and c==c[::-1] and 0 not in c:r1.append(c)
b.insert(0, b.pop())
q1=max(r1,key=len)
r2=[[]];b=[x for x in v];m=range(len(b)+1)
for k in m:
for i in m:
for j in range(i):
c = b[j:i + 1]
if len(c)>2 and len(c)%2==1 and c==c[::-1] and "+" in c and 0 not in c:r2.append(c)
b.insert(0, b.pop())
q2=max(r2,key=h)
with open('f.log', 'a') as f:f.write('pal '+str(_)+' : '+str(q1)+' : '+str(q2)+'\n')
if q2!=[]:return 100+h(q2)
else:return len(q1)
i=raw_input()
(a,b)=d(i)
if a in ['C','B']:print('0')
elif a=='-':print("0 y" if random.randint(0, 1) == 1 else "0 n")
else:q,j=max((p(a,b,j),j)for j in range(len(b)));print(str(j))


I found that the controller:

• crashes when score exceeds 1500;
• does not properly merge atoms in same cases.

## Python, RandomBot, Score = 7.95

Nothing too fancy, just a random bot.

import random

game_input = raw_input().split("/")
current_atom = game_input[0]
board = game_input[1].split(" ")

if current_atom != "-":
print(random.randint(0, len(board) - 1))
else:
random_choice = " y" if random.randint(0, 1) == 1 else " n"
print(str(random.randint(0, len(board) - 1)) + random_choice)


## Python, BadPlayer, Score = 21.45

import random

try:
raw_input
except:
raw_input = input

game_input = raw_input().split("/")
current_atom = game_input[0]
board = game_input[1].split(" ")

def get_chain(board, base):
chain = []
board = board[:]
try:
while board[base] == board[base + 1]:
chain = [board[base]] + chain + [board[base + 1]]
del board[base]
del board[base]
base -= 1
except IndexError:
pass
return chain

def biggest_chain(board):
chains = []
base = 0
i = 0
while i < len(board) - 1:
chains.append([i, get_chain(board, i)])
i += 1
return sorted(chains, key=lambda x: len(x[1]) / 2)[-1]

def not_in_chain():
a, b = biggest_chain(board)
if len(b) == 0:
print(random.randint(0, len(board) - 1))
elif random.randint(0, 1) == 0:
print(random.randint(a + len(b)/2, len(board) - 1))
else:
try:
print(random.randint(0, a - len(b)/2 - 1))
except:
print(random.randint(a + len(b)/2, len(board) - 1))

if current_atom in "+B":
a, b = biggest_chain(board)
if len(b) == 0:
print(0)
else:
print(a)
elif current_atom == "C":
not_in_chain()
elif current_atom == "-":
a, b = biggest_chain(board)
if len(b) == 0:
print(str(random.randint(0, len(board) - 1)) + " n")
elif random.randint(0, 1) == 0:
print(str(random.randint(a + len(b)/2, len(board) - 1)) + " n")
else:
try:
print(str(random.randint(0, a - len(b)/2 - 1)) + " n")
except:
print(str(random.randint(0, len(board) - 1)) + " n")
else:
not_in_chain()


Just a very bad bot that often make the controller crash

• How does it make the controller crash? And if it does, it is a problem with the controller, or your bot? – mbomb007 Oct 24 '16 at 18:34
• @mbomb007 I don't recall why it crash, but the crashes were in the controller – TuxCrafting Oct 24 '16 at 19:01
• This bot should work without any bugs, just alter the code a bit to accommodate for the updated "stdin" thing. – clismique Nov 3 '16 at 1:51