quarta-feira, novembro 29, 2017

Argghh, Just One More Go Mum: "Exploring Tetris On My Own"



"but...but...the Spectrum is for educational purposes Mum! Honest, look it comes with a game called Chess and Scrabble!"


Ping pong, Pacman, Defender, Elite, Sabre Wolf, Manic Miner, Jetset Willy, Tekken Sensible Soccer, Mario, Sonic, NHL, PES, Fifa, COD, Forza, Dirt, Bubble Burst, Candy Crush, Donald Trump, Mickey Mouse... Never affected me in anyway whatsoever, thinking back, I still haven't handed in my Technical Drawing homework due in 1985. "Argghh, just one more go mum."


I grew up with a commodore 64 and ZX Spectrum. I use to play games obsessively, especially Elite but it never impacted my social and family life. Apart from a bit of cop killing and drug, slave and alien artifact trading here and there I have grown up to be a well rounded member of society

I seem to be the only person in public spaces sitting staring into space, thinking without a device of some kind. Looking at other people using their phones, it seems that we're at the point where they're virtually programmed to reach for their phones as soon at every conceivable opportunity without even thinking about it. Often going through the same cycle, checking text, mail, news. Seems the whole concept of independent reflection has gone out the window.

In fairness sitting by yourself and staring into space in public was considered a bit odd even before computer games and smart phones. I feel the same way about people who can only run if they do so with headphones, that on some level they are saying 'Running is crap and I need this ear candy to make it bearable'.

It just depends if you have an addictive nature or not. Everyone's brains are wired differently. Our brains goe haywire with dopamine and serotonin whenever we receive a positive feedback. Doesn't matter if it's a blackjack, some under 20 team in Uzbekistan winning, landing a dragon punch in SFII, scoring a Tetris, looking at porn, reading or writing reviews... I'm just a born addict and will keep on doing that thing, usually addictive stuff.

Tetris. Ah. Those were the days...It's a beautiful game except when the pieces start falling faster. I always played with the sound off to reduce the stress but the pile-up . . . (deep breath) . . . .is always on the horizon . . . (deep breath) . . . .(deep breath) . . . .(deep breath) . . . .

What follows is something I concocted to while away the time when I was dealing with tossers I won't say where...

NB: Next step is porting the the following Tetris Engine to Android, which is already under way. The hard part is finished (e.g., collision detection, rotation matrices, and so forth).

Python Tetris Engine:


import simplegui
import random

BACKGROUND_COLOR = "White"
WIDTH = 700
HEIGHT = 600
BOARD_WIDTH = 10
BOARD_HEIGHT = 20
BOARD_UPPER_LEFT = (300, 75)
BLOCK_SIZE = 25
START_POSITION = (3, 0)
gameInPlay = False
score = 0
time = 0
vel_y = 0
next_piece = None
speed = 15

# global game objects
piece_O = None
piece_I = None
piece_T = None
piece_L = None
piece_J = None
piece_S = None
piece_Z = None
piece_B = None
board = None
pieces = None
pos = [10, 10]
current_piece = None

# test shape of one block
SHAPE_ONE_BLOCK = ["X"]
SHAPE_ONE_BLOCK.append("B")
SHAPE_ONE_BLOCK.append("Green")

SHAPE_O_0 = [["O", "X", "X", "O"],
             ["O", "X", "X", "O"],
             ["O", "O", "O", "O"],
             ["O", "O", "O", "O"]]
temp = []
temp.append(SHAPE_O_0)
temp.append(SHAPE_O_0)
temp.append(SHAPE_O_0)
temp.append(SHAPE_O_0)
SHAPE_O = []
SHAPE_O.append(temp)
SHAPE_O.append("O")
SHAPE_O.append("Yellow")

SHAPE_I_0 = [["O", "O", "O", "O"],
             ["X", "X", "X", "X"],
             ["O", "O", "O", "O"],
             ["O", "O", "O", "O"]]
SHAPE_I_1 = [["O", "X", "O", "O"],
             ["O", "X", "O", "O"],
             ["O", "X", "O", "O"],
             ["O", "X", "O", "O"]]
SHAPE_I_2 = [["O", "O", "O", "O"],
             ["O", "O", "O", "O"],
             ["X", "X", "X", "X"],
             ["O", "O", "O", "O"]]
SHAPE_I_3 = [["O", "O", "X", "O"],
             ["O", "O", "X", "O"],
             ["O", "O", "X", "O"],
             ["O", "O", "X", "O"]]
temp = []
temp.append(SHAPE_I_0)
temp.append(SHAPE_I_1)
temp.append(SHAPE_I_2)
temp.append(SHAPE_I_3)
SHAPE_I = []
SHAPE_I.append(temp)
SHAPE_I.append("I")
SHAPE_I.append("LightBlue") ##ADD8E6

SHAPE_T_0 = [["O", "X", "O"],
             ["X", "X", "X"],
             ["O", "O", "O"]]
SHAPE_T_90 = [["O", "X", "O"],
              ["X", "X", "O"],
              ["O", "X", "O"]]
SHAPE_T_180 = [["O", "O", "O"],
               ["X", "X", "X"],
               ["O", "X", "O"]]
SHAPE_T_270 = [["O", "X", "O"],
               ["O", "X", "X"],
               ["O", "X", "O"]]
temp = []
temp.append(SHAPE_T_0)
temp.append(SHAPE_T_90)
temp.append(SHAPE_T_180)
temp.append(SHAPE_T_270)
SHAPE_T = []
SHAPE_T.append(temp)
SHAPE_T.append("T")
SHAPE_T.append("Purple")

SHAPE_L_0 = [["O", "O", "X"],
             ["X", "X", "X"],
             ["O", "O", "O"]]
SHAPE_L_1 = [["X", "X", "O"],
             ["O", "X", "O"],
             ["O", "X", "O"]]
SHAPE_L_2 = [["O", "O", "O"],
             ["X", "X", "X"],
             ["X", "O", "O"]]
SHAPE_L_3 = [["O", "X", "O"],
             ["O", "X", "O"],
             ["O", "X", "X"]]
temp = []
temp.append(SHAPE_L_0)
temp.append(SHAPE_L_1)
temp.append(SHAPE_L_2)
temp.append(SHAPE_L_3)
SHAPE_L = []
SHAPE_L.append(temp)
SHAPE_L.append("L")
SHAPE_L.append("Orange")

SHAPE_J_0 = [["X", "O", "O"],
             ["X", "X", "X"],
             ["O", "O", "O"]]
SHAPE_J_1 = [["O", "X", "O"],
             ["O", "X", "O"],
             ["X", "X", "O"]]
SHAPE_J_2 = [["O", "O", "O"],
             ["X", "X", "X"],
             ["O", "O", "X"]]
SHAPE_J_3 = [["O", "X", "X"],
             ["O", "X", "O"],
             ["O", "X", "O"]]
temp = []
temp.append(SHAPE_J_0)
temp.append(SHAPE_J_1)
temp.append(SHAPE_J_2)
temp.append(SHAPE_J_3)
SHAPE_J = []
SHAPE_J.append(temp)
SHAPE_J.append("J")
SHAPE_J.append("DarkBlue") #00008B

SHAPE_S_0 = [["O", "X", "X"],
             ["X", "X", "O"],
             ["O", "O", "O"]]
SHAPE_S_1 = [["X", "O", "O"],
             ["X", "X", "O"],
             ["O", "X", "O"]]
SHAPE_S_2 = [["O", "O", "O"],
             ["O", "X", "X"],
             ["X", "X", "O"]]
SHAPE_S_3 = [["O", "X", "O"],
             ["O", "X", "X"],
             ["O", "O", "X"]]
temp = []
temp.append(SHAPE_S_0)
temp.append(SHAPE_S_1)
temp.append(SHAPE_S_2)
temp.append(SHAPE_S_3)
SHAPE_S = []
SHAPE_S.append(temp)
SHAPE_S.append("S")
SHAPE_S.append("Green")

SHAPE_Z_0 = [["X", "X", "O"],
             ["O", "X", "X"],
             ["O", "O", "O"]]
SHAPE_Z_1 = [["O", "X", "O"],
             ["X", "X", "O"],
             ["X", "O", "O"]]
SHAPE_Z_2 = [["O", "O", "O"],
             ["X", "X", "O"],
             ["O", "X", "X"]]
SHAPE_Z_3 = [["O", "O", "X"],
             ["O", "X", "X"],
             ["O", "X", "O"]]
temp = []
temp.append(SHAPE_Z_0)
temp.append(SHAPE_Z_1)
temp.append(SHAPE_Z_2)
temp.append(SHAPE_Z_3)
SHAPE_Z = []
SHAPE_Z.append(temp)
SHAPE_Z.append("Z")
SHAPE_Z.append("Red")

class Board():
 
    def __init__(self, upper_left_corner):
        self.upper_left = upper_left_corner
        self.upper_right = (upper_left_corner[0] + (BLOCK_SIZE * BOARD_WIDTH), self.upper_left[1])
        self.lower_right = (upper_left_corner[0] + (BLOCK_SIZE * BOARD_WIDTH), self.upper_left[1] + (BLOCK_SIZE * BOARD_HEIGHT))
        self.lower_left = (self.upper_left[0], self.upper_left[1] + (BLOCK_SIZE * BOARD_HEIGHT))

        # setup grid
        self.grid = []
        for i in range(BOARD_HEIGHT):
            row = [None] *  BOARD_WIDTH
            self.grid.append(row)
     
    def draw(self, canvas):
        for c in range(BOARD_WIDTH):
            for r in range(BOARD_HEIGHT):
                if self.grid[r][c] != None:
                    block = self.grid[r][c]
                    draw_block(canvas, (self.upper_left[0] + (BLOCK_SIZE * c), self.upper_left[1] + (BLOCK_SIZE * r)), 1, BACKGROUND_COLOR, block.get_color())
        canvas.draw_polygon((self.upper_left, self.upper_right, self.lower_right, self.lower_left), 1, "Black")

    def fill(self, pos, piece):
        self.grid[pos[0]][pos[1]] = piece
     
    def empty(self, pos):
        self.grid[pos[0]][pos[1]] = None
     
    def isEmpty(self, pos):
        return (self.grid[pos[0]][pos[1]] == None)
 
    def isRowFull(self, row):
        for col in range(BOARD_WIDTH):
            if self.grid[row][col] == None:
                return False
        return True
 
class Piece():
    def __init__(self, shape, pos = None):
        self.color = shape[2]
        self.type = shape[1]
        self.shapes = shape[0]
        self.shape_index = 0
        self.shape = self.shapes[0]
        self.width = len(self.shape[0])
        self.height = len(self.shape)
        if pos != None:
            self.x = pos[0]
            self.y = pos[1]
     
    def __str__(self):
        s = ""
        s += "Piece (shape = " + self.type + ", color = " + self.color + ")\n"
        s += " (width = " + str(self.width) + ", height = " + str(self.height) + ")\n"
        for row in self.shape:
            value = ""
            for col in row:
                value += col
            s += value + "\n"         
        return s
 
    def isValidRotation(self, direction):
        if direction == "CCW":
            if self.shape_index == len(self.shapes) - 1:
                idx = 0
            else:
                idx = self.shape_index + 1
        else:
            if self.shape_index == 0:
                idx = len(self.shapes) - 1
            else:
                idx = self.shape_index - 1
        temp_piece = Piece([[self.shapes[idx]], "temp", "Blue"], (self.x, self.y))

        canRotate = True
     
        #Check top side
        for row in range(len(temp_piece.shape)):
            for col in range(len(temp_piece.shape[row])):
                if temp_piece.shape[row][col] == "X":
                    if (temp_piece.y + row) < 0:
                        canRotate = False
                     
        #check down side
        for row in range(len(temp_piece.shape) - 1, -1, -1):
           for col in range(len(temp_piece.shape[row])):
                if temp_piece.shape[row][col] == "X":
                    if (temp_piece.y + row) >= BOARD_HEIGHT:
                        canRotate = False
                     
        #check right side
        for row in range(len(temp_piece.shape)):
            if "X" in temp_piece.shape[row]:
                reversed_row = list(temp_piece.shape[row])
                reversed_row.reverse()
                offset = reversed_row.index("X")
                if (temp_piece.x + temp_piece.width - offset) > BOARD_WIDTH:
                    canRotate = False
     
        #check left side
        for row in range(len(temp_piece.shape)):
            if "X" in temp_piece.shape[row]:
                offset = temp_piece.shape[row].index("X")
                if (temp_piece.x + offset) < 0:
                    canRotate = False
                 
        #check for overlaps
        for row in range(len(temp_piece.shape)):
            for col in range(len(temp_piece.shape[row])):
                if temp_piece.shape[row][col] == "X":
                    if temp_piece.x + col >= BOARD_WIDTH or temp_piece.y + row >= BOARD_HEIGHT:
                        canRotate = False
                    elif temp_piece.x + col < 0 or temp_piece.y + row < 0:
                        canRotate = False
                    elif not board.isEmpty((temp_piece.y + row, temp_piece.x + col)):
                        canRotate = False     
     
        return canRotate

    def rotate_ccw(self):
        if self.shape_index == len(self.shapes) - 1:
            self.shape_index = 0
        else:
            self.shape_index += 1
        self.shape = self.shapes[self.shape_index]
        self.width = len(self.shape[0])
        self.height = len(self.shape)
 
    def rotate_cw(self):
        if self.shape_index == 0:
            self.shape_index = len(self.shapes) - 1
        else:
            self.shape_index -= 1
        self.shape = self.shapes[self.shape_index]
        self.width = len(self.shape[0])
        self.height = len(self.shape)

    def get_color(self):
        return self.color
 
    def get_width(self):
        return self.width
 
    def get_height(self):
        return self.height
 
    def move_left(self):
        canMove = True
         
        for row in range(len(self.shape)):
            if "X" in self.shape[row]:
                offset = self.shape[row].index("X")
                if (self.x + offset) - 1 < 0:
                    canMove = False
                elif not board.isEmpty((self.y + row, self.x + offset - 1)):
                    canMove = False

        if canMove:
            self.x -= 1
         
    def move_right(self):
            canMove = True
         
            for row in range(len(self.shape)):
                if "X" in self.shape[row]:
                    reversed_row = list(self.shape[row])
                    reversed_row.reverse()
                    offset = reversed_row.index("X")
                    if (self.x + self.width - offset) + 1 > BOARD_WIDTH:
                        canMove = False
                    elif not board.isEmpty((self.y + row, self.x + self.width - offset)):
                        canMove = False

            if canMove:
                self.x += 1
     
    def move_up(self):
            canMove = True
         
            for row in range(len(self.shape)):
                for col in range(len(self.shape[row])):
                    if self.shape[row][col] == "X":
                        if (self.y + row - 1) < 0:
                            canMove = False
                        elif not board.isEmpty((self.y + row - 1, self.x + col)):
                            canMove = False

            if canMove:
                self.y -= 1       
 
    def move_down(self):
            canMove = True
         
            for row in range(len(self.shape) - 1, -1, -1):
                for col in range(len(self.shape[row])):
                    if self.shape[row][col] == "X":
                        if (self.y + row + 1) >= BOARD_HEIGHT:
                            canMove = False
                        elif not board.isEmpty((self.y + row + 1, self.x + col)):
                            canMove = False

            if canMove:
                self.y += 1
             
            return canMove
 
    def get_blocks(self):
        positions = []
        for row in range(len(self.shape)):
            for col in range(len(self.shape[row])):
                if self.shape[row][col] == "X":
                    positions.append((self.x + col, self.y + row))
        return positions
     
    def draw(self, canvas):
        for row in range(len(self.shape)):
            pos_y = BOARD_UPPER_LEFT[1] + (BLOCK_SIZE * self.y) + (BLOCK_SIZE * row)
            for col in range(len(self.shape[row])):
                pos_x = BOARD_UPPER_LEFT[0] + (BLOCK_SIZE * self.x) + (BLOCK_SIZE * col)
                if self.shape[row][col] == "X":
                    draw_block(canvas, (pos_x, pos_y), 1, BACKGROUND_COLOR, self.color)
                else:
                    draw_block(canvas, (pos_x, pos_y), 1, BACKGROUND_COLOR, BACKGROUND_COLOR)
 
    def get_position(self):
        return (self.x, self.y)
 
def draw_block(canvas, pos, line_width = 1, line_color = "Black", fill_color = None):
    block = [(pos[0], pos[1]), (pos[0] + BLOCK_SIZE, pos[1]), (pos[0] + BLOCK_SIZE, pos[1] + BLOCK_SIZE), (pos[0], pos[1] + BLOCK_SIZE)]
    canvas.draw_polygon(block, line_width, line_color, fill_color)

def draw(canvas):
    global time
    global gameInPlay
 
    time += 1
 
    if gameInPlay:
     
        if (time % speed == 0):
            piece_moved = current_piece.move_down()

            if current_piece.get_position() == START_POSITION:

                gameInPlay = False


            if (not piece_moved):
                lockdown_handler()
                create_new_piece_handler()

        for i in range(BOARD_HEIGHT):
            check_full_rows_handler()
         
        # draw next piece
        positions = next_piece.get_blocks()

        color = next_piece.get_color()
     
        pos[0] = BOARD_UPPER_LEFT[0] + (BOARD_WIDTH * BLOCK_SIZE)
        pos[1] = BOARD_UPPER_LEFT[1] + 60
     
        canvas.draw_text("Next Piece", [pos[0] + 20, pos[1] - 40], 20, "Black")

        for i in positions:
            draw_block(canvas, [pos[0] + (BLOCK_SIZE * i[0]) - 50, pos[1] + (BLOCK_SIZE * i[1])], 1, BACKGROUND_COLOR, color)
         
    current_piece.draw(canvas)
    board.draw(canvas)
 
    # Game Over Check
    if not gameInPlay:
        width = frame.get_canvas_textwidth("Game Over", 50)
        canvas.draw_text("Game Over", (BOARD_UPPER_LEFT[0] + (BOARD_WIDTH * 0.50 * BLOCK_SIZE) - (width * 0.50), BOARD_UPPER_LEFT[0] + (BOARD_HEIGHT * 0.5)), 50, "Black")
 
    # Instructions
    offset = 275
    text_score = "0000" + str(score)
    text_score = text_score[-4:]
    canvas.draw_text(text_score, (BOARD_UPPER_LEFT[0], BOARD_UPPER_LEFT[1] - 25), 50, "Blue")
    canvas.draw_text("left arrow = move piece left", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 20), 15, "Black")
    canvas.draw_text("right arrow = move piece right", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 40), 15, "Black")
    canvas.draw_text("down arrow = move piece down", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 60), 15, "Black")
    canvas.draw_text("'z' = turn piece counter clockwise", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 80), 15, "Black")
    canvas.draw_text("up arrow = turn piece counter clockwise", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 100), 15, "Black")
    canvas.draw_text("'x' = turn piece clockwise", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 120), 15, "Black")
    canvas.draw_text("'n' = new game", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 140), 15, "Black")
    canvas.draw_text("'space' = drops the piece", (BOARD_UPPER_LEFT[0] - offset, BOARD_UPPER_LEFT[1] + 160), 15, "Black")

def init():
    global board
    board = Board(BOARD_UPPER_LEFT)
 
    global piece_O
    piece_O = Piece(SHAPE_O)
 
    global piece_I
    piece_I = Piece(SHAPE_I)
 
    global piece_T
    piece_T = Piece(SHAPE_T)
 
    global piece_L
    piece_L = Piece(SHAPE_L)
 
    global piece_J
    piece_J = Piece(SHAPE_J)
 
    global piece_S
    piece_S = Piece(SHAPE_S)
 
    global piece_Z
    piece_Z = Piece(SHAPE_Z)
 
    global piece_B
    piece_B = Piece(SHAPE_ONE_BLOCK)
 
 
    global current_piece
    global pieces
 
    pieces = [SHAPE_O, SHAPE_I, SHAPE_T, SHAPE_L, SHAPE_J, SHAPE_S, SHAPE_Z]
 
    current_piece = Piece(random.choice(pieces), START_POSITION)
    global next_piece
    next_piece = Piece(random.choice(pieces), START_POSITION)
 
    global gameInPlay
    gameInPlay = True
 
    global score
    score = 0
 
    global speed
    speed = 15

def move_left_handler():
    current_piece.move_left()

def move_right_handler():
    current_piece.move_right()

def move_up_handler():
    current_piece.move_up()

def move_down_handler():
    current_piece.move_down()

def rotate_ccw_handler():
    if current_piece.isValidRotation("CCW"):
        current_piece.rotate_ccw()
 
def rotate_cw_handler():
    if current_piece.isValidRotation("CW"):
        current_piece.rotate_cw()
 
def keydown_handler(key):
    if key == simplegui.KEY_MAP["left"]:
        current_piece.move_left()
    if key == simplegui.KEY_MAP["right"]:
        current_piece.move_right()
    if key == simplegui.KEY_MAP["down"]:
        current_piece.move_down()
    if key == simplegui.KEY_MAP["up"]:
        if current_piece.isValidRotation("CCW"):
            current_piece.rotate_ccw()
    if key == simplegui.KEY_MAP["z"]:
        if current_piece.isValidRotation("CCW"):
            current_piece.rotate_ccw()
    if key == simplegui.KEY_MAP["x"]:
        if current_piece.isValidRotation("CW"):
            current_piece.rotate_cw()
    if key == simplegui.KEY_MAP["space"]:
        drop_piece_handler()
    if key == simplegui.KEY_MAP["n"]:
        new_game_handler()

def check_full_rows_handler():
    row_to_pop = None
    for row in range(BOARD_HEIGHT):
        if board.isRowFull(row):
            row_to_pop = row
    if row_to_pop != None:
        board.grid.pop(row_to_pop)
        row = [None] *  BOARD_WIDTH
        board.grid.insert(0, row)
        global score
        score += 1
        global speed
        if (score % 5) == 0:
            speed -= 2
 
def lockdown_handler():
    positions = current_piece.get_blocks()
    color = current_piece.get_color()
    for i in positions:
        board.fill([i[1], i[0]], Piece(['X', 'B', color]))

def drop_piece_handler():
    while (current_piece.move_down()):
        pass
    lockdown_handler()
    create_new_piece_handler()
         
def new_game_handler():
    init()

def create_new_piece_handler():
    global current_piece
    global next_piece
 
    current_piece = next_piece

    next_piece = Piece(random.choice(pieces), START_POSITION)

def check_ccw_handler():
    print current_piece.isValidRotateCCW()
             
frame = simplegui.create_frame("Antao", WIDTH, HEIGHT)
frame.set_canvas_background(BACKGROUND_COLOR)

frame.set_keydown_handler(keydown_handler)

frame.set_draw_handler(draw)

frame.start()

init()

2 comentários:

Book Stooge disse...

"Apart from a bit of cop killing and drug, slave and alien artifact trading here and there"

You are being too hard on yourself. Who doesn't do that thing once in a while, just to blow off steam?

I know I am guilty of the "check my phone constantly" syndrome. Part of it is that people leave me alone more if I'm fiddling with my phone than if I was reading a book. It boggles my mind how as a group of people we've all been trained a certain social way. It definitely makes something like Asimov's Psycho-History much more believable.

So why create your own tetris game? Just for the fun of it?

Manuel Antão disse...

I never understood why teaching even the basics of computing was so rare nowadays, if this results in any increase in students with this skill, it can only be a good thing. Even kettles now have devices in them that use this type of programming. If we can take the mystique out of it at a young age so much the better.

When I say "programming" I'm talking about real coding; not the HTML crap. HTML is not "code", it's mark-up. It's accessible because it's easy. It's also a dead skill because there are already so many tools that do it for you. Programming is fascinating and important but you can't learn it in a week.

You need to get your hands dirty with real code, before you know the trade-offs under the hood of higher level abstractions. Plus it's fun. Think of it like...learning a natural language, like German, rather than just learning that there are lots of different cultures in the world. Bring on a generation of programmers.

And yes, programming allows me to think about stuff in a clear way. I no longer do this as my day job, but back in the day, I was to do this for a living. Now I just do it for fun and to believe I still have what it takes...