This project is based on the Milton Bradley toy called Simon.
The toy adds a note/color to a sequence, which the player must play back.
The original has 4 possible notes and colors.
According to the
Wiki, the originals are
A link in the comments helped me associate these with the correct numbers.
On that note, this
StackOverflow answer by jose helped a lot to use pygame.midi.
I wanted to make a Simon that was simple but true to the original.
Listed below are some features added to that end.
Levels of Difficulty
I have levels of difficulty that are considered a win when a sequence of a given length is repeated back. The levels 1-4 correspond to the
Wiki's list of 8, 14, 20, and 31. For historical value (i.e., I used it to test) there is a level 0 that asks for a user to play back a note. There is also an unending level. Selection of difficulty is the first screen the user sees.
Sequences Gradually Quicken
I noticed in LuckyPennyShop.com's YouTube video
Vintage Electronic Simon Game 1978 Milton Bradley Toys that the sequence not only got longer but played faster. This not only makes the game more difficult, but it also brings you to the new information faster. For this I used
a variation of
where 0 < c < 1 is a non-negative scaling constant and ell is the length of the list. (c = 1 made sequences play too quickly for long sequences.) A benefit to this function is that it decreases, but levels out asymptotically to zero. We can add a minimum m not to go faster than.
Correct Tile Blinks On Miss
I noticed that the ending tile in the video above blinks several times when the sequence is ended—correctly, if I followed the video right. I resort to an end screen. However, for incorrect tiles the tile that should have been pressed blinks 4 times. (I don't know if this happens in the original.)
Images of Gameplay
A board of unlit tiles looks like this:
Compare to this one, where the red button is lit:
The reason I have the number of tiles
n as a constant was because I hoped to expand this to even more tiles. (I have tried with 9.) However, on top of the ideal case of having more colors and sounds for the tiles, the brightening feature (the "bright" tuple in Button.draw()) ought to be changed: Right now it naively turns any nonzeros into 255 and leaves zeros alone. If we had a tile of (132, 210, 0) for example, its bright color is the same as our yellow's, which is given by (150,150,0).
When the game is over this end screen comes up:
The "sequence" box is mostly for the edification of those who chose infinite mode. Here we see the second number is 31, which means the player was on difficulty 4. The 7 is the size of the sequence that the user didn't play back correctly.
Clicking Replay takes the user back to the difficulty screen and Quit exits. There are several places where I call the end() functions. An improvement might be adding threads, one that keeps track of when the window's "x" button is clicked and another for inside the game.
Here's the code:
'''Simple Simon'''
import math, time, pygame, pygame.midi, random
pygame.midi.init()
pygame.font.init()
audio = pygame.midi.Output(0)
audio.set_instrument(4,1)
# midi chart: http://www.electronics.dit.ie/staff/tscarff/Music_
# technology/midi/midi_note_numbers_for_octaves.htm
BLACK = (255,255,255)
colors = [(0,150,0),(150,0,0),(150,150,0),(0,0,150)] #g,r,y ,b
sounds = [52,57,61,64] #e,a,c#,e_hi
hardness = {'0':1,'1':8, '2':14, '3':20, '4':31, '∞': -1}
size = 200
time_scale = .1
min_pause = .1
n = math.ceil(math.sqrt(len(colors)))
win_size = n*size
mid = round(n*size/2)
window = pygame.display.set_mode( (win_size, win_size) )
pygame.display.set_caption("Simple Simon")
class Pane:
def __init__(self,text,pos,font_size=40,color=BLACK,fun=None):
self.fun = fun
self.text = text
font = pygame.font.SysFont('Arial', font_size)
surf = font.render(text, True, color)
self.rect = surf.get_rect()
self.rect.center = pos
window.blit(surf, self.rect.topleft)
class Button:
def __init__(self,i):
self.x = size * (i%n)
self.y = size * (i//n)
self.color = colors[i]
self.sound = sounds[i]
self.rect = pygame.Rect( (self.x,self.y), (size,size) )
self.draw()
def draw(self, lit=False, wait=0):
bright = tuple(255 if i else 0 for i in self.color)
color = bright if lit else self.color
if lit:
audio.note_on(self.sound,127,1)
else:
audio.note_off(self.sound,127,1)
pygame.draw.rect(window,color,self.rect)
pygame.display.update()
time.sleep(wait)
def end():
audio.close()
pygame.midi.quit()
pygame.quit()
quit()
def choose_mode(ts=60,ms=40):
window.fill( (0,0,0) )
while True:
modes = enumerate(sorted(hardness.keys()))
title = Pane('Select hardness', (mid,ts/2), ts)
diffs = [Pane(x,(mid,ms*i+1.5*ts),ms,) for i,x in modes]
pygame.display.update()
for event in pygame.event.get():
if event.type == pygame.QUIT:
end()
if event.type == pygame.MOUSEBUTTONDOWN:
for diff in diffs:
if diff.rect.collidepoint(event.pos):
return diff.text
def simon():
player,wrong = False,False
sequence = []
correct = 0
mode = choose_mode()
tiles = [Button(i) for i in range(len(colors))]
while True:
# player's turn
if player:
for event in pygame.event.get():
if event.type == pygame.QUIT:
end()
if event.type == pygame.MOUSEBUTTONDOWN:
for tile in tiles:
if tile.rect.collidepoint(event.pos):
tile.draw(lit=True)
if tile == sequence[correct]:
correct += 1
else:
wrong = True
# letting go of mouse button redraws all tiles
elif event.type == pygame.MOUSEBUTTONUP:
[tile.draw() for tile in tiles]
# correct tile blinks on miss
if wrong:
right = sequence[correct]
for i in range(4):
right.draw(lit=True,wait=.1)
right.draw(wait=.1)
end_screen(len(sequence),mode,'Lost')
# player used correct sequence
elif correct == len(sequence):
correct = 0
player = False
# computer's turn
else:
chain = len(sequence)
pause = math.exp(-time_scale*chain)
# player wins, if not in infinite mode
if chain == hardness[mode]:
end_screen(chain, mode)
# pause before showing next sequence
time.sleep(min_pause + pause)
sequence.append(random.choice(tiles))
for tile in sequence:
tile.draw(lit=True,wait=pause)
tile.draw(wait=min_pause*pause)
# player can quit mid-sequence
for event in pygame.event.get():
if event.type == pygame.QUIT:
end()
player = True
pygame.display.update()
def end_screen(streak, mode, result='Won', fsize=40):
funs = [None, None, simon, end]
most = str(hardness[mode]) if mode.isalnum() else '∞'
texts = ['You %s!' % result,
'Sequence: %d/%s' % (streak,most),
'Replay', 'Quit']
data = enumerate(zip(texts,funs))
panes = [Pane(x[0],(mid,(i+1)*1.5*fsize),fsize,fun=x[1]) for i,x in data]
pygame.display.update()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
end()
if event.type == pygame.MOUSEBUTTONDOWN:
for pane in panes:
if pane.rect.collidepoint(event.pos):
if pane.fun:
pane.fun()
simon()