# Atomas clone in Python

Here is my crappy clone of Atomas, a puzzle game where you combine small atoms into more valuable ones.

# Pytomas (an Atomas build for Python)
# Author: Qwerp-Derp

from random import randint

# Initial variables
element_table = []
atom_range = [1, 3]
specials_list = ["+"] * 6 + ["-"] * 3 + ["B", "C"]
carry_over = " "
moves = 0
score = 0

# Initialises starting list
for a in range(6):
element_table.append(randint(atom_range[0], atom_range[1]))

# Processing for Plus atom
def plus_process():
global element_table
final_atom = 0

def plus_code_block():
# Main code block for plus
global element_table, final_atom, score
temp_list_start = element_table[:user_input + 1]
temp_list_end = element_table[user_input + 1:]
final_atom = temp_list_end[0]
# Continually matches two atoms together
try:
matches = 0
while temp_list_start[-1] == temp_list_end[0]:
matches += 1
score += (temp_list_end[0] * 2) + 1
if temp_list_end[0] >= final_atom and matches != 1:
final_atom += 2
else:
final_atom += 1
temp_list_start = temp_list_start[:-1]
temp_list_end = temp_list_end[1:]
if matches == 1:
final_atom += 1
except IndexError:
pass
final_matches = []
# Matches atoms on "other side" to each other
# (Since the Atomas playing field is a loop,
# and my list is linear, this is the best I can do)
if not len(temp_list_start):
while len(temp_list_end) not in [0, 1]:
if temp_list_end[0] == temp_list_end[-1]:
final_matches.append(temp_list_end[0])
temp_list_end = temp_list_end[1:-1]
else:
break
if not len(temp_list_end):
while len(temp_list_start) not in [0, 1]:
if temp_list_start[0] == temp_list_start[-1]:
final_matches.append(temp_list_start[0])
temp_list_start = temp_list_start[1:-1]
else:
break
# Figures out the score for the "final atom"
for b in range(len(final_matches)):
if final_matches[b] >= final_atom:
final_atom += 2
else:
final_atom += 1
# Makes the final gameboard
element_table = temp_list_start + [final_atom] + temp_list_end

# Configuration of how to put the + in
if len(element_table) not in [0, 1]:
if element_table[user_input] == element_table[user_input + 1] and element_table[user_input] != "+":
plus_code_block()
else:
element_table = element_table[:user_input + 1] + ["+"] + element_table[user_input + 1:]
elif user_input == len(element_table) - 1:
if element_table[user_input] == element_table[0] and element_table[user_input] != "+":
plus_code_block()
else:
element_table.append("+")
else:
element_table.append("+")

# Processing for Minus atom
def minus_process():
global carry_over, element_table
# Grabs the atom from the gameboard
minus_atom = element_table[user_input]
del element_table[user_input]
while True:
# Gets user input for turning grabbed atom
# into a Plus atom
plus_input = raw_input("Turn to plus? [y/n]: ")
if plus_input == "y":
carry_over = "+"
break
elif plus_input == "n":
carry_over = "X" + str(minus_atom)
break
else:
print("That is not a valid input. Try again.")

# Processing for Black Plus atom
def black_plus_process():
global element_table, score
# Manipulates gameboard for processing
if element_table[user_input - 1] > element_table[user_input + 1]:
black_atom = element_table[user_input - 1] + 2
else:
black_atom = element_table[user_input + 1] + 2
element_table[user_input - 1] = black_atom
element_table[user_input + 1] = black_atom
score += (black_atom * 2) + 1
plus_process()

# Processing for Clone atom
def clone_process():
global carry_over
clone_atom = element_table[user_input]
carry_over = "X " + clone_atom

# User interface
while len(element_table) < 18:
moves += 1
if moves % 15 == 0:
atom_range = [x + 1 for x in atom_range]
# Checks if a move is "carried over"
# (From the Minus/Clone atom)
if carry_over[0] == "X":
turn_atom = int(carry_over[2:])
print("X " + str(turn_atom))
elif carry_over[0] == "+":
print("+")
else:
turn_atom = randint(atom_range[0], atom_range[1])
special_check = randint(1, 30)
# Display specials
if special_check <= 11:
print(specials_list[special_check - 1])
else:
print("X " + str(turn_atom))
# Display current state of game
print(element_table)
user_input = input("Enter # (0 to " + str(len(element_table) - 1) + "): ")
# Specials check
if special_check in [1, 2, 3, 4, 5, 6] or carry_over == "+":
plus_process()
elif special_check in [7, 8, 9]:
minus_process()
elif special_check == 10 and len(element_table) not in [0, 1]:
black_plus_process()
elif special_check == 11:
clone_process()
else:
# No special
if len(element_table) not in [0, 1]:
element_table = element_table[:user_input + 1] + [turn_atom] + element_table[user_input + 1:]
else:
element_table.append(turn_atom)

# Outside of the loop
print("Game over! Your score was: " + str(score))


Is there any code in this program that can be improved (e.g. style issues, redundant code)?

Also, I'm thinking of including all of the element names in to this as well. Is the best approach to just stick it all in, or should I put it into an external file?

# Bugs?

I don't know the rules of the Game, so this is something you could add in your docstring: what rule a specific function implement.

Anyway, it seems like, when using the plus atom and inserting between two atoms that are of different value, you let the '+' there but never check later that there isn't two identical atoms around that plus to merge them.

Similarly, I’m not sure about the black_plus either: you’re modifying atoms around the selected one and then inserting a plus between the selected atom and a newly modified one; meaning you have a situation like:

[..., converted_black, selected_atom, '+', converted_black, ...]


which doesn't sound like a great move to play. But considering the rarity of the black_plus, it seems like it should be:

[..., converted_black, '+', converted_black, ...]


where the first converted_black is the selected atom. In short, compare element_table[user_input] to element_table[user_input + 1] instead of using element_table[user_input - 1].

# Easy insertions

Your movements either affect the selected atom or insert something after it. You handle selection easily using element_table[user_input] but have a pretty convoluted logic to insert element after the given atom. Consider using list.insert instead: you specify the index before which you want to insert an element and the element to insert; so you will most likely use element_table.insert(user_input + 1, <something>) and call it a day. The good thing about insert is that, if you use an index too large, it append at the end of the list instead.

# User input

You take great care of what the user enters when handling the minus atom but you’re nowhere near of it when dealing with atom selection in your main loop. You should also make use of raw_input there and validate it is an int in the required range.

# Carry over

You are storing 3 different type of information as a string in this variable. Consider using proper types and, as such, build a class hierarchy for your atoms. This way, you can have a unique entry point, say handle_atom for each type of atom that would return the carry_over, let's say a RegularAtom instance for clone or minus or a PlusAtom instance for minus, other methods returning None. That way, in your main loop, your turn_atom would be either one of the atom returned or, if None is returned, a newly chosen one.

Speaking of which, choosing your atoms would be done using random.choice over a list containing class names:

[PlusAtom] * 6 + [MinusAtom] * 3 + [BlackPlusAtom, CloneAtom] + [RegularAtom] * 19


And that's it, no more magic number to know which has been chosen.

# Proposed improvements

"""Pytomas (an Atomas build for Python)"""

import random

__author__ = 'Yourself'

class RegularAtom(object):
RANGE_BEGIN = 1
RANGE_END = 3

def __init__(self, atom_value=None):
if atom_value is None:
atom_value = random.randint(RegularAtom.RANGE_BEGIN, RegularAtom.RANGE_END)
self.atom_value

def handle_atom(self, user_input, board):
board.insert(user_input + 1, self.atom_value)

@classmethod
def reset(cls):
cls.RANGE_BEGIN = 1
cls.RANGE_END = 3

def __str__(self):
return str(self.atom_value)

class MinusAtom(object):
def handle_atom(self, user_input, board):
atom = board.pop(user_input)
# Do we need to take care of that?
carry_over = PlusAtom() if atom == '+' else RegularAtom(atom)
if self.user_convert_to_plus():
return PlusAtom()
return carry_over

def user_convert_to_plus(self):
while True:
plus_input = raw_input("Turn to plus? [y/n]: ").lower()
if plus_input in 'yn':
return plus_input == 'y'
print("That is not a valid input. Try again.")

def __str__(self):
return '-'

class CloneAtom(object):
def handle_atom(self, user_input, board):
atom = board[user_input]
# Same remark than MinusAtom
carry_over = PlusAtom() if atom == '+' else RegularAtom(atom)
return carry_over

def __str__(self):
return 'C'

class PlusAtom(object):
def handle_atom(self, user_input, board):
board.insert(user_input + 1, '+')

def __str__(self):
return '+'

class BlackPlusAtom(PlusAtom):
def handle_atom(self, user_input, board):
atom1 = board[user_input]
atom2 = board[user_input + 1]
# Again, not sure it is needed
if atom1 == '+':
atom1 = atom2
if atom2 == '+':
atom2 = atom1
black_value = max(atom1, atom2) + 2
board[user_input] = black_value
board[user_input + 1] = black_value
board.score += 2 * black_value + 1
super(BlackPlusAtom, self).handle_atom(user_input, board)

def __str__(self):
return 'B'

class Board(list):
ATOM_CHOICES = [PlusAtom] * 6 + [MinusAtom] * 3 + [BlackPlusAtom, CloneAtom] + [RegularAtom] * 19

def __init__(self):
# Fill the board with initial atoms
super(Board, self).__init__(random.randint(RegularAtom.RANGE_BEGIN, RegularAtom.RANGE_END) for _ in xrange(6))
self.score = 0

def play(self, size_limit=18):
# Reset data to be able to play several games in a row
self.score = 0
RegularAtom.reset()
moves = 0
turn_atom = None
while len(self) < size_limit:
moves += 1
if moves % 15 == 0:
RegularAtom.RANGE_BEGIN += 1
RegularAtom.RANGE_END += 1
if turn_atom is None:
turn_atom_class = random.choice(self.ATOM_CHOICES)
turn_atom = turn_atom_class()
print self
print 'Current atom:', turn_atom
user_input = self.select_atom()
turn_atom = turn_atom.handle_atom(user_input, self)
self.manage_plus()

def select_atom(self):
while True:
value = raw_input("Enter # (0 to {}): ".format(len(self) - 1))
try:
index = int(value)
except ValueError:
pass
else:
if 0 <= index < len(self):
return index

def handle_plus(self):
start_search = 0
while True:
try:
index = self.index('+', start_search)
except ValueError:
# No '+' found in list
return

final_atom = 0
while True:
# self[index] is either '+' or a newly created atom
# Try to combine once more
before = self[index - 1]
after = self[index + 1]
if before == after:
# There are two special cases to handle: '+' at the
# beginning and '+' at the end. This deletion pattern
# combined to the overriden __delitem__ takes
# care of them
del self[index]
if not index:
del self[index]
index = len(self) - 1
else:
index -= 1
del self[index]
del self[index]
if not final_atom:
final_atom = before + 2
else:
final_atom += 1 + int(before > final_atom)
self.insert(index, final_atom)
else:
self.score += final_atom
break
start_search = index + 1

def __getitem__(self, index):
"""Simulate cyclic list by having out-of-bounds indexes
wrap back to the beginning.
"""
index = self._wrap(index)
super(Board, self).__getitem__(index)

def __setitem__(self, index, value):
"""Simulate cyclic list by having out-of-bounds indexes
wrap back to the beginning.
"""
index = self._wrap(index)
super(Board, self).__setitem__(index, value)

def __delitem__(self, index):
"""Simulate cyclic list by having out-of-bounds indexes
wrap back to the beginning.
"""
index = self._wrap(index)
super(Board, self).__delitem__(index)

def _wrap(self, index):
length = len(self)
while index >= length:
index -= length
return index

if __name__ == '__main__':
board = Board()
board.play()
print 'Game Over! Your score was:', board.score


I haven't tested the code and, as said earlier, I don't know the game. So I might have missed some little things. But you might get the spirit with that.