4
\$\begingroup\$

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?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Style

  • There is a lot of comment that would be better suited as docstrings; especially those before functions definitions.
  • Python has its own conventions for metadata, such as __author__ or __version__.
  • You should wrap your top-level code in a function and/or put it under an if __name__ == '__main__': clause.
  • You use way too much global variables. Consider using parameters and return values instead.

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.