A game of atomic proportions

21

5

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()
        all_user_input = bot.stdout.readline().strip("\n").split(" ")
        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"]).

Answer-specific Specs:

  • 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.
  • So your answer will be formatted like this:

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

Good luck!

clismique

Posted 2016-10-11T09:16:20.067

Reputation: 6 600

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 – 2016-10-11T10:01:24.860

@TonHospel Yes, that's right. – clismique – 2016-10-11T10:01:49.997

4I 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 – 2016-10-11T10:08:03.333

@TonHospel Is it okay if I'm asking the bot for an optional input (So, the bot could experience 1 or 2 inputs per turn, depending on the atom)? – clismique – 2016-10-11T10:49:39.317

Sure, you can make up any protocol you like, but I would avoid it. As soon as you go beyond the trivial input then output you start to have to worry about buffering and deadlock in the communication protocol. Nor do I see the need. Is it for the for the - y case ? Simply call the bot twice then – Ton Hospel – 2016-10-11T11:05:47.573

To be clear, I'm suggesting the standalone bot gets called on every turn, NOT that it is kept running while input and output keep happening. I don't see a need to keep history or even current score in the bot. However the bot should get the move number as input too (to know the placing atom range and when it will change) – Ton Hospel – 2016-10-11T13:48:59.890

Scoring +. The text says You earn 2 * matched atom points for every atom you match, but the code says score += (temp_list_end[0] * 2) + 1 which is 1 more. Which is it ? – Ton Hospel – 2016-10-11T14:32:24.153

1The code seems to be able to put + in the element list, but this is nowhere found in the textual description – Ton Hospel – 2016-10-11T16:25:19.257

I love this game! I have it on my phone, and it can be addicting. I gotta say though, it took a while to figure it out completely without a tutorial. Does your implementation use the same scoring method that the game does? – mbomb007 – 2016-10-11T18:10:58.853

Let us continue this discussion in chat.

– mbomb007 – 2016-10-11T18:33:35.090

Could the code include built-in graphics, or does it have to have an ASCII output? – JungHwan Min – 2016-10-11T23:47:02.677

@JHM It has to be ASCII, I'm afraid. – clismique – 2016-10-12T08:19:43.390

1Ah, 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 – 2016-10-18T14:15:56.513

@Qwerp-Derp You're using 750 and 1500 as the score requirement for dark pluses and neutrinos, but you don't use Atomas's method of Scoring. It looks like getting to that score may be more difficult than you wanted using your controller. I'm not sure.

– mbomb007 – 2016-10-24T18:46:46.753

1Idk if people will spend time creating a solution if the controller isn't improved. I like the question, but not the implementation. – mbomb007 – 2016-11-02T16:14:22.660

@TonHospel Included. – clismique – 2016-11-03T01:50:30.777

@mbomb007 I fixed the implementation a bit - there shouldn't be any bugs anymore. If there's anything else I can fix, please tell me. – clismique – 2016-11-03T01:51:14.193

IMHO, the scoring should be changed. http://atomas.wikia.com/wiki/Score

– mbomb007 – 2016-11-03T13:48:52.447

@mbomb007 Fixed... – clismique – 2016-11-03T22:47:36.403

The challenge is good, it's just that the work required to create a good answer is pretty high, at least according to my standards. My highscore IRL is Np 93; 129K. – mbomb007 – 2016-12-09T22:41:38.450

Why the atom_range is not initialized at the init() function? – mdahmoune – 2017-07-08T11:51:24.650

@mdahmoune It's been half a year since I wrote that code. From what I recall, atom_range is global, and it would be a pain to initialise it at init() (cause global hell). – clismique – 2017-07-08T11:58:32.923

@mdahmoune 1. Yes, atom_range is supposed to grow over time, spawning elements can change. And for the bot thing, I agree that more variables will help, but the bot honestly doesn't need those elements. Sorry for replying so late. – clismique – 2017-07-08T12:32:47.250

for atom_range what I try to say, is that when we execute the init() function the second, third,... time, the atom_range is not reset to [1, 3] – mdahmoune – 2017-07-08T12:35:00.520

@mdahmoune Yeah, good point. I'm on mobile now, though, will fix up the code in the question tomorrow morning. In the meantime, you can edit the controller. – clismique – 2017-07-08T12:38:34.460

I believe there is a problem with regular_process because it does not add the atom parameter when calling specials_call(atom, user_input), in place it add to the board a random atom. – mdahmoune – 2017-07-08T22:59:09.960

I built a draft bot that achieve 1500 as score, but the controller crashes just after... I think it du to the "C" atom... – mdahmoune – 2017-07-08T23:04:31.377

@mdahmoune Can you give me a GitHub Gist to your code? I might have a look at the issue. – clismique – 2017-07-08T23:13:38.250

Yes of course, just give me the time to clean it.. – mdahmoune – 2017-07-08T23:16:33.107

I think also that your controller does not handle the case where the board is like ... 1 + 2 ... and the bot put 1 between + and 2, the controller does not merge the two 1... – mdahmoune – 2017-07-08T23:29:40.530

Real behavior controller eg showing the above problem:|1/1 3 3 2 2 2|3/1 3 3 2 2 2 12|3/1 3 3 2 2 2 12 11|+/1 3 3 2 2 2 12 11 13|2/1 4 2 2 2 12 11 13|12/1 4 2 2 2 12 11 13 11|13/1 4 2 2 2 12 11 13 11 13|+/1 4 2 2 2 12 11 13 11 13 11|+/1 4 3 2 12 11 13 11 13 11|1/1 + 4 3 2 12 11 13 11 13 11|11/1 + 11 4 3 2 12 11 13 11 13 11|+/1 12 + 11 4 3 2 12 11 13 11 13 11|+/1 + 12 + 11 4 3 2 12 11 13 11 13 11|1/1 + + 12 + 11 4 3 2 12 11 13 11 13 11|11/1 + 12 + 12 + 11 4 3 2 12 11 13 11 13 11|4/1 + 12 + 12 + 11 4 3 2 12 11 13 11 13 11 13|12/1 + 12 + 12 + 11 4 3 2 12 11 13 11 13 11 13 12 – mdahmoune – 2017-07-09T08:53:46.290

sometime when the draft bot achieves 750 point, the controller crashes, I think we need to cast user_input into int in function named clone_process(). – mdahmoune – 2017-07-09T09:07:16.133

I made a version of Atomas you can play via text input. It'll take you forever to finish a game, though. https://ideone.com/U7ZUnD

– mbomb007 – 2017-11-29T21:40:10.767

Answers

1

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.

mdahmoune

Posted 2016-10-11T09:16:20.067

Reputation: 2 605

0

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)

clismique

Posted 2016-10-11T09:16:20.067

Reputation: 6 600

0

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

TuxCrafting

Posted 2016-10-11T09:16:20.067

Reputation: 4 547

How does it make the controller crash? And if it does, it is a problem with the controller, or your bot? – mbomb007 – 2016-10-24T18:34:34.117

@mbomb007 I don't recall why it crash, but the crashes were in the controller – TuxCrafting – 2016-10-24T19:01:18.557

This bot should work without any bugs, just alter the code a bit to accommodate for the updated "stdin" thing. – clismique – 2016-11-03T01:51:50.377