Skip to content

Instantly share code, notes, and snippets.

@abner-math
Last active October 4, 2022 14:54
Show Gist options
  • Save abner-math/892bf12a4a185721485f to your computer and use it in GitHub Desktop.
Save abner-math/892bf12a4a185721485f to your computer and use it in GitHub Desktop.
A clone of the class game Pacman made in Python 2.7
#!/usr/bin/python
# -*-coding=utf-8 -*-
#-----------------------------------------------------------------
# PyPac v1.2
# Created by: Abner Matheus
# E-mail: abner.math.c@gmail.com
# Github: http://github.com/picoledelimao
#-----------------------------------------------------------------
import time, os, platform, sys, select, math
from random import randint
from copy import copy
"""
Store a (x, y) coordinate of the terrain
"""
class Position:
def __init__(self, x, y):
self.x = x
self.y = y
"""
Representates the terrain of the map.
The terrain is a bidimensional matrix that is mapped through
a string passed
"""
class Terrain:
GHOST = (Position(11, 4), Position(10, 6), Position(11, 6), Position(12, 6))
PACMAN = Position(10, 8)
GHOST_CHAR = '^'
GHOST_WEAKEN_CHAR = '*'
PACMAN_CHAR = '@'
BULLET = '.'
GREATER_BULLET = '0'
def __init__(self):
self.terrain = map(lambda x: list(x), """
——————————————————
|○•••|••••••••|•••○|
|•——•|•——————•|•——•|
|•|••••••••••••••|•|
—|•|•——•———-——•——•|•|—
•••••••| |••••••
—|•|•——•——————•——•|•|—
|•|•••••• •••••••|•|
|•——•|•——————•|•——•|
|○•••|••••••••|•••○|
——————————————————
""".replace("•", Terrain.BULLET).replace("○", Terrain.GREATER_BULLET).replace("—", "=").split("\n"))
def is_blocked(self, position, direction):
try:
if position.x == 11 and position.y == 5 and direction == Direction.UPWARD: return False
char = self.terrain[position.y][position.x]
return char == "|" or char == "=" or char == "-"
except: return True
"""
Makes the pacman eats bullets and increment game score in process
"""
def eat(self, context, pacman):
position = self.terrain[pacman.position.y][pacman.position.x]
if position == Terrain.BULLET:
self.terrain[pacman.position.y][pacman.position.x] = ' '
context.context.score += 1
elif position == Terrain.GREATER_BULLET:
self.terrain[pacman.position.y][pacman.position.x] = ' '
context.context.score += 5
return True
return False
"""
Verify is the terrain has no 'bullets'
"""
def is_clear(self):
for line in self.terrain:
if '.' in line or '0' in line: return False
return True
@staticmethod
def clear_screen():
if platform.system() == "Windows": os.system("cls")
else: os.system("clear")
"""
Render the terrain on the screen
"""
def render(self, pacman, ghosts):
Terrain.clear_screen()
matrix = []
for i in range(len(self.terrain)):
matrix.append(list(self.terrain[i]))
matrix[pacman.position.y][pacman.position.x] = Terrain.PACMAN_CHAR
for ghost in ghosts:
if ghost.is_weaken():
matrix[ghost.position.y][ghost.position.x] = Terrain.GHOST_WEAKEN_CHAR
else:
matrix[ghost.position.y][ghost.position.x] = Terrain.GHOST_CHAR
result = ""
for i in range(len(matrix)):
result += "".join(matrix[i]) + "\n"
print(result)
"""
Enumerate the possible directions that a walkable can take
"""
class Direction:
FORWARD = 1
BACKWARD = 2
UPWARD = 3
DOWNWARD = 4
"""
Representates a walkable object in the map (ghost and pacman)
"""
class Walkable:
def __init__(self): pass
def turn(self, direction):
self.direction = direction
"""
Verify if given movement in a certain direction is possible
"""
def possible_movement(self, direction, terrain):
if direction == Direction.FORWARD:
return not terrain.is_blocked(Position(self.position.x + 1, self.position.y), direction)
elif direction == Direction.BACKWARD:
return not terrain.is_blocked(Position(self.position.x - 1, self.position.y), direction)
elif direction == Direction.UPWARD:
return not terrain.is_blocked(Position(self.position.x, self.position.y - 1), direction)
elif direction == Direction.DOWNWARD:
return not terrain.is_blocked(Position(self.position.x, self.position.y + 1), direction)
"""
Move the object toward its direction. Return false case the movement is not possible.
"""
def move(self, terrain):
if self.direction == Direction.FORWARD and self.position.x >= len(terrain.terrain[self.position.y]) - 1:
self.position.x = 0
elif self.direction == Direction.BACKWARD and self.position.x <= 0:
self.position.x = len(terrain.terrain[self.position.y]) - 1
elif self.possible_movement(self.direction, terrain):
if self.direction == Direction.FORWARD:
self.position.x += 1
elif self.direction == Direction.BACKWARD:
self.position.x -= 1
elif self.direction == Direction.UPWARD:
self.position.y -= 1
elif self.direction == Direction.DOWNWARD:
self.position.y += 1
else: return False
return True
"""
Representates the ghosts in the map.
A ghost is a walkable
"""
class Ghost(Walkable):
def __init__(self, number):
Walkable.__init__(self)
self.position = copy(Terrain.GHOST[number])
self.isWeaken = False
self.direction = Direction.UPWARD
"""
Verify is ghost is weaken (frightened)
"""
def is_weaken(self):
return self.isWeaken
"""
Weaken the ghost, making it 'eatable' by Pacman
"""
def weaken(self):
self.isWeaken = True
"""
Strengthen the ghost, making it dangerous to Pacman
"""
def strengthen(self):
self.isWeaken = False
"""
Representates the Pacman
"""
class Pacman(Walkable):
def __init__(self):
Walkable.__init__(self)
self.position = copy(Terrain.PACMAN)
self.direction = Direction.FORWARD
"""
Main class of the game.
It receives the input of the user and maps it to its state.
"""
class Game:
def __init__(self):
self.lives = 3
self.score = 0
self.level = 1
self.state = StateInitial(self)
"""
Main loop of the game
"""
def loop(self):
self.state.loop()
"""
Set a direction of Pacman. Only work in StatePlaying.
"""
def set_direction(self, direction):
self.state.set_direction(direction)
"""
Start a new game.
"""
def start_new_game(self):
self.state.start_new_game()
"""
Superclass of states of a game
"""
class GameState:
def __init__(self): pass
def loop(self): raise NotImplementedException()
def set_direction(self, direction): raise NotImplementedException()
def start_new_game(self): raise NotImplementedException()
"""
Initial screen of the game.
"""
class StateInitial(GameState):
def __init__(self, context):
GameState.__init__(self)
self.context = context
self.context.lives = 3
self.context.score = 0
self.context.level = 1
def loop(self):
message = """
____ __ __ ____ ____ __
| \| | || \ / | / ] ──▒▒▒▒▒────▄████▄─────
| o ) | || o ) o | / / ─▒─▄▒─▄▒──███▄█▀──────
| _/| ~ || _/| |/ / ─▒▒▒▒▒▒▒─▐████──█──█──
| | |___, || | | _ / \_ ─▒▒▒▒▒▒▒──█████▄──────
| | | || | | | \ | ─▒─▒─▒─▒───▀████▀─────
|__| |____/ |__| |__|__|\____|
GAME CONTROLS:
-----------------------------
PRESS 'N' TO START A NEW GAME.
'A', 'S', 'D', 'W' TO MOVE.
'ESC' TO EXIT GAME.
-----------------------------
CREATED BY:
-----------------------------
ABNER MATHEUS (abner.math.c@gmail.com)
"""
Terrain.clear_screen()
print(message.decode('utf-8'))
def set_direction(self, direction): pass
def start_new_game(self):
self.context.state = StatePlaying(self.context)
class StateGameOver(GameState):
def __init__(self, context):
GameState.__init__(self)
self.context = context
def loop(self):
Terrain.clear_screen()
print("""
____ ____ ___ ___ ___ ___ __ __ ___ ____
/ | / || | | / _] / \ | | | / _]| \
| __|| o || _ _ | / [_ | || | | / [_ | D )
| | || || \_/ || _] | O || | || _]| /
| |_ || _ || | || [_ | || : || [_ | \
| || | || | || | | | \ / | || . \
|___,_||__|__||___|___||_____| \___/ \_/ |_____||__|\_|
PRESS 'N' TO START A NEW GAME.""".decode('utf-8'))
def set_direction(self, direction): pass
def start_new_game(self):
self.context.state = StateInitial(self.context)
"""
Main state of the game.
Here is where the stuff happens.
"""
class StatePlaying(GameState):
def __init__(self, context):
GameState.__init__(self)
self.context = context
self.start_new_level()
"""
Get the angle between two vectors.
"""
def __get_angle(self, w1, w2):
return math.degrees(math.atan2((w2.position.y - w1.position.y), (w2.position.x - w1.position.x)))
"""
Convert a given angle in a direction.
"""
def __get_direction(self, angle):
if 0 <= angle < 45:
return Direction.FORWARD
elif 45 <= angle < 135:
return Direction.UPWARD
elif 135 <= angle < 225:
return Direction.BACKWARD
elif 225 <= angle < 315:
return Direction.DOWNWARD
elif 315 <= angle < 360:
return Direction.FORWARD
return Direction.FORWARD
"""
Verify is two vectors are equal
"""
def __equals(self, p1, p2):
return p1.x == p2.x and p1.y == p2.y
"""
Verify if two ghosts occupies the same tile.
"""
def __contains_others(self, movingGhost):
for ghost in self.ghosts:
if ghost != movingGhost:
if self.__equals(ghost.position, movingGhost.position): return True
return False
"""
Move the ghosts through the map
"""
def __move_ghosts(self):
for ghost in self.ghosts:
if (self.count_loop % 2 == 0 or self.count_loop % 3) and ghost.is_weaken(): continue
if self.count_loop % 2 == 0 and not ghost.is_weaken(): continue
if self.__equals(ghost.position, Terrain.GHOST[0]):
ghost.turn(Direction.FORWARD)
elif self.__equals(ghost.position, Terrain.GHOST[1]):
ghost.turn(Direction.FORWARD)
elif self.__equals(ghost.position, Terrain.GHOST[2]):
ghost.turn(Direction.UPWARD)
elif self.__equals(ghost.position, Terrain.GHOST[3]):
ghost.turn(Direction.BACKWARD)
else:
angle = self.__get_angle(ghost, self.pacman)
if ghost.is_weaken():
angle += 180
ghost.turn(self.__get_direction(angle))
old_position = copy(ghost.position)
if ghost.direction == Direction.FORWARD:
forbidden_direction = Direction.BACKWARD
elif ghost.direction == Direction.BACKWARD:
forbidden_direction = Direction.FORWARD
elif ghost.direction == Direction.UPWARD:
forbidden_direction = Direction.DOWNWARD
elif ghost.direction == Direction.DOWNWARD:
forbidden_direction = Direction.UPWARD
count = 0
while self.__equals(ghost.position, old_position) or self.__contains_others(ghost):
while True:
ghost.direction = randint(1, 4)
if (ghost.possible_movement(ghost.direction, self.terrain) and ghost.direction != forbidden_direction) or count > 100: break
count += 1
ghost.move(self.terrain)
"""
Makes the Pacman eat (or being eaten) the ghosts, depending its state.
"""
def __eat_ghosts(self):
for i in range(len(self.ghosts)):
if self.__equals(self.ghosts[i].position, self.pacman.position):
if self.ghosts[i].is_weaken():
self.ghosts[i].strengthen()
self.ghosts[i].position = copy(Terrain.GHOST[i])
else:
self.lose_live()
"""
Render the screen (terrain + info)
"""
def __render(self):
self.terrain.render(self.pacman, self.ghosts)
print("LIVES: %d SCORE: %d LEVEL: %d" % (self.context.lives, self.context.score, self.context.level))
def loop(self):
self.count_loop += 1
if self.terrain.is_clear():
print("""
CONGRATULATIONS!
A NEW LEVEL WILL START IN FEW SECONDS.""".decode('utf-8'))
time.sleep(1)
self.context.level += 1
self.start_new_level()
self.__move_ghosts()
self.pacman.move(self.terrain)
self.__eat_ghosts()
if self.countdown > 0: self.countdown -= 1
else:
for ghost in self.ghosts: ghost.strengthen()
if self.terrain.eat(self, self.pacman):
for ghost in self.ghosts: ghost.weaken()
self.countdown = 15
self.__render()
def set_direction(self, direction):
if self.pacman.possible_movement(direction, self.terrain):
self.pacman.turn(direction)
def start_new_game(self):
self.context.state = StateInitial(self.context)
"""
Start a new level. On a new level, lives and points are mantained.
"""
def start_new_level(self):
self.terrain = Terrain()
self.pacman = Pacman()
self.ghosts = []
for i in range(4): self.ghosts.append(Ghost(i))
self.count_loop = 0
self.countdown = 0
"""
Makes Pacman lose a life.
"""
def lose_live(self):
self.context.lives -= 1
if self.context.lives <= 0:
self.context.state = StateGameOver(self.context)
else:
self.__render()
print("""
YOU LOST A LIVE.""".decode('utf-8'))
time.sleep(2)
self.pacman = Pacman()
self.pacman = Pacman()
self.ghosts = []
for i in range(4): self.ghosts.append(Ghost(i))
self.count_loop = 0
self.countdown = 0
#---------------------------------
# IO MANAGER
#---------------------------------
def controller_windows():
import Tkinter
class Controller:
SPEED = 0.2
def __init__(self):
self.game = Game()
self.start_game()
def press_key(self, event):
key = event.keysym.lower()
if key == "escape": #ESC
return False
elif key == "n": #Enter
self.game.start_new_game()
elif key == "d": #Right arrow
self.game.set_direction(Direction.FORWARD)
elif key == "a": #Left arrow
self.game.set_direction(Direction.BACKWARD)
elif key == "w": #Up arrow
self.game.set_direction(Direction.UPWARD)
elif key == "s": #Down arrow
self.game.set_direction(Direction.DOWNWARD)
return True
def loop(self):
self.game.loop()
self.console.after(250, self.loop)
def start_game(self):
self.console = Tkinter.Tk()
self.console.bind_all('<Key>', self.press_key)
self.console.withdraw()
try:
self.console.after(250, self.loop)
self.console.mainloop()
except KeyboardInterrupt: pass
Controller()
def controller_unix():
import termios, tty, thread
class NonBlockingConsole(object):
def __enter__(self):
self.old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
return self
def __exit__(self, type, value, traceback):
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
def get_data(self):
if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
return sys.stdin.read(1)
return False
class Controller:
SPEED = 0.2
def __init__(self):
self.game = Game()
self.start_game()
def press_key(self, nbc):
key = str(nbc.get_data())
if key == '\x1b': #ESC
return False
elif key == 'n': #Enter
self.game.start_new_game()
elif key == 'd': #Right arrow
self.game.set_direction(Direction.FORWARD)
elif key == 'a': #Left arrow
self.game.set_direction(Direction.BACKWARD)
elif key == 'w': #Up arrow
self.game.set_direction(Direction.UPWARD)
elif key == 's': #Down arrow
self.game.set_direction(Direction.DOWNWARD)
return True
def loop(self, threadName):
while self.running:
time.sleep(Controller.SPEED)
self.game.loop()
def start_game(self):
self.running = True
thread.start_new_thread(self.loop, ("Thread-1",))
try:
with NonBlockingConsole() as nbc:
while self.press_key(nbc): pass
except KeyboardInterrupt: pass
self.running = False
Controller()
if __name__ == '__main__':
if platform.system() == "Windows":
controller_windows()
else:
controller_unix()
@mibi88
Copy link

mibi88 commented Jul 5, 2021

Wow ! This is a very good game !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment