"""
Author: mbomb007
Game: Atomas
Last Edited: Nov. 29, 2017
Directions:
1. Copy code
2. Paste at: repl.it/languages/Python
3. Uncomment main() at the bottom
4. Remove any testing code below the call to main()
5. Run to play
6. Notify me in chat if you find a bug: chat.stackexchange.com/users/138114/
"""
from random import choice, randint
# TODO: Switch back to using a recursive reaction_check()
# TODO?: Change to Object-Oriented for Atoms
# TODO: display help, options
# - option for circle print
# - option for showing all collapse steps
# - option for index confirm?
def gcd(a, b):
while b: a, b = b, a % b
return a
def _lcm(a, b): return a * b // gcd(a, b)
def lcm(*args): return reduce(_lcm, args)
# constants
START_ATOMS = 6
LIMIT = 18
SPAWN_RANGE = 6
CCW = -1 # which direction in the list is counter-clockwise (-1, 1)?
# vars
L = []
turn = 0
highest = 3
# specials
MIN_ELEMENT = 1 # used to check whether element or special
PLUS = 0
MINUS = -1
DARK_PLUS = -2
NEUTRINO = -3
SPECIALS = [PLUS, MINUS, DARK_PLUS, NEUTRINO]
SPECIAL_STRING = {
PLUS : '+',
MINUS : '-',
DARK_PLUS : 'x',
NEUTRINO : '?'
}
# chance to spawn every turn
SPAWN_RATE = {
PLUS : 5,
MINUS : 20,
DARK_PLUS : 90,
NEUTRINO : 60
}
# guaranteed to spawn at least once every X turns
MAX_WAIT = SPAWN_RATE
# only appear after this score
SPAWN_WALL = {
PLUS : 0,
MINUS : 0,
DARK_PLUS : 750,
NEUTRINO : 1500
}
# 180
ODDS_TOTAL = lcm(*SPAWN_RATE.values())
# [36, 9, 2, 3] ->
# [36, 45, 47, 50] for IF comparisons
SPECIAL_ODDS = []
for i in xrange(len(SPAWN_RATE)):
SPECIAL_ODDS.append(ODDS_TOTAL // SPAWN_RATE[ SPECIALS[i] ] + (SPECIAL_ODDS[i-1] if i > 0 else 0))
INC_PLUS = 1
INC_DARK = 3
last_plus = 1
last_minus = 1
last_dark = 1
last_neutrino = 1
# scoring
score_atom = 1
score_value = 0
PLUS_VALUE = 1
################################################################################
def atom_to_string(atom):
if atom >= MIN_ELEMENT:
return str(atom)
else:
return SPECIAL_STRING[atom]
def get_value(atom):
if atom >= MIN_ELEMENT:
return atom
else:
return PLUS_VALUE
# TODO?
def pretty_print(board, tab = 0):
print "\t"*tab + " ".join(map(atom_to_string, board))
def score_simple(a):
return int(round( 1.5 * a + 1.25 ))
# http://a...content-available-to-author-only...a.com/wiki/Score
# middle, outer, number of chain reaction
def score_chain(m, o, r):
if o < m - 1: o = m - 1
return ( -m + o + 3 ) * r - m + 3 * o + 3
def can_react(idx):
"""Return whether a reaction can occur at a specific index"""
if len(L) < 3:
return False
if L[idx] == PLUS:
return L[(idx-1)%len(L)] >= MIN_ELEMENT and L[(idx-1)%len(L)] == L[(idx+1)%len(L)]
elif L[idx] == DARK_PLUS:
return True
else:
return False
def reaction(idx, chain = -1, dark = False):
"""Combine atoms at a specific index"""
global L, score_value
l_idx = (idx-1)%len(L)
r_idx = (idx+1)%len(L)
left = L[l_idx]
right = L[r_idx]
if len(L) > 2 and (dark or (left >= MIN_ELEMENT and left == right)):
chain += 1
if chain == -1:
# print intermediate
pretty_print(L, 1)
# first time through?
if chain < 1:
a = get_value(max(left, right))
# score the simple reaction
score_value += score_simple(a)
# it's possible for a DARK_PLUS to combine with PLUS atoms
L[idx] = a + (INC_DARK if dark else INC_PLUS)
else:
# score the chain reaction
score_value += score_chain(L[idx], left, chain)
# assume both left and right atoms are equal, since dark plus doesn't affect
L[idx] = max(L[idx], left) + INC_PLUS + (L[idx] == left)
old_len = len(L)
# remove later atoms first
if l_idx > r_idx:
l_idx, r_idx = r_idx, l_idx
del L[r_idx]
del L[l_idx]
# shift index because atoms were removed
# if idx was 0, both were after, so don't shift
# if idx was len(L)-1, then both atoms removed were in front, so subtract 2.
if idx == 0:
pass
elif idx == old_len-1:
idx -= 2
else:
idx -= 1
# recurse. dark plus only helps on the first time
return reaction(idx, chain)
if chain >= 0:
# print after done
pretty_print(L, 1)
# return the index and chain achieved, since chain could continue in another reaction
return (idx, chain)
def reaction_check(idx, chain):
"""Perform any reactions possible"""
counter = 0
# check every location
while counter < len(L):
check_idx = (idx + counter * CCW) % len(L)
if can_react(check_idx):
old_chain = chain
idx, chain = reaction(check_idx, chain)
if old_chain != chain:
break
counter += 1
return (idx, chain)
# TODO: Switch back to using a recursive reaction_check(), because this sucks
def perform_all_reactions(index, chain):
old_chain = chain
index, chain = reaction_check(index, chain)
while old_chain != chain:
old_chain = chain
index, chain = reaction_check(index, chain)
return index, chain
def get_next_atom():
"""Get the next atom for the user to place"""
global last_plus, last_minus, last_dark, last_neutrino
# current atom
atom = None
# special atom?
if last_dark >= MAX_WAIT[DARK_PLUS] - 1:
atom = DARK_PLUS
last_dark = 1
# (x) > (+)
last_plus = 1
elif last_neutrino >= MAX_WAIT[NEUTRINO] - 1:
atom = NEUTRINO
last_neutrino = 1
elif last_minus >= MAX_WAIT[MINUS] - 1:
atom = MINUS
last_minus = 1
# (-) > (+)
last_plus = 1
elif last_plus >= MAX_WAIT[PLUS] - 1:
atom = PLUS
last_plus = 1
else:
# choose atom randomly
atom = get_rand_atom()
# increment time since last specials
if atom != PLUS and score_value >= SPAWN_WALL[PLUS]:
last_plus += 1
if atom != MINUS and score_value >= SPAWN_WALL[MINUS]:
last_minus += 1
if atom != DARK_PLUS and score_value >= SPAWN_WALL[DARK_PLUS]:
last_dark += 1
if atom != NEUTRINO and score_value >= SPAWN_WALL[NEUTRINO]:
last_neutrino += 1
return atom
def get_rand_atom():
"""Get a random atom to place"""
rnd = randint(0, ODDS_TOTAL - 1)
# if special atom
for i in xrange(len(SPECIAL_ODDS)):
if rnd < SPECIAL_ODDS[i]:
if score_value >= SPAWN_WALL[ SPECIALS[i] ]:
return SPECIALS[i]
else:
break
lowest = highest - SPAWN_RANGE
# atoms within spawn range + atoms below spawn range that are in play
return choice(range(max(1, lowest), highest + 1) +
list({atom for atom in L if MIN_ELEMENT <= atom < lowest}))
def user_insert_index(msg):
"""Get user input for index to insert an atom at"""
index = None
while index < -len(L) or index > len(L): # allow insert at end (same as 0)
index = int(raw_input(msg))
return index
def user_atom_index(msg):
"""Get user input for index to access an atom at"""
index = None
while index < -len(L) or index > len(L)-1:
index = int(raw_input(msg))
return index
def user_action(atom):
"""Perform user interaction"""
# display board
pretty_print(L)
s = atom_to_string(atom)
index = None
chain = -1
# (#)
if atom >= MIN_ELEMENT:
msg = "(%s) Enter index to insert atom at (0-%s):" % (s, len(L)-1)
index = user_insert_index(msg)
L.insert(index, atom)
# (+) or (x)
elif atom == PLUS or atom == DARK_PLUS:
msg = "(%s) Enter index to insert atom at (0-%s):" % (s, len(L)-1)
index = user_insert_index(msg)
L.insert(index, atom)
index, chain = reaction(index, dark=(atom == DARK_PLUS))
# (-)
elif atom == MINUS:
msg = "(%s) Enter index of atom to remove (0-%s):" % (s, len(L)-1)
index = user_atom_index(msg)
# remove atom
atom = L[index]
s = atom_to_string(atom)
del L[index]
# perform all possible reactions
index, chain = perform_all_reactions(index, chain)
if atom != PLUS:
# display board
pretty_print(L)
# change to (+) ?
msg = "\t(%s) Change to (%s)? Enter 1 for Yes, or 0 for No:" % (s, SPECIAL_STRING[PLUS])
yes = int(raw_input(msg))
if yes:
return user_action(PLUS)
else:
return user_action(atom)
# (?)
elif atom == NEUTRINO:
msg = "(%s) Enter index of atom to copy (0-%s):" % (s, len(L)-1)
index = user_atom_index(msg)
atom = L[index]
return user_action(atom)
# perform all possible reactions
index, chain = perform_all_reactions(index, chain)
return (index, chain)
def game_over():
global L, score_value
# add up points from atoms in play
while L:
score_value += get_value(L.pop())
print """\nGAME OVER
Atom: %s
Score: %s
Turns: %s
""" % (score_atom, score_value, turn)
def main():
global L, highest, turn, score_atom, score_value
# initialize ring randomly
L = []
for i in xrange(START_ATOMS):
L.append(choice(xrange(1, highest+1)))
score_atom = max(L)
# TODO: display help, options
# game loop
while len(L) < LIMIT:
# get atom for user to place
atom = get_next_atom()
# perform user action
user_action(atom)
# update highest atom reached
score_atom = max(score_atom, *L)
# next turn
turn += 1
if turn % 40 == 0:
highest += 1
# GAME OVER
game_over()
#main()
# TESTING
L = [7, 7, 0, 9, 0, 10, 0, 13, 0, 12, 0, 12, 0, 13, 14]
perform_all_reactions(*reaction(10))