Started retro tetris game

Started developing a retro version of Tetris, following
the planning on the board of the classroom
(see planning.jpg).

Moved cursor work into cursor.
This commit is contained in:
Chris Proctor
2025-12-18 17:45:39 -05:00
parent b7aa246380
commit c8d1ddd58f
10 changed files with 145 additions and 4 deletions

10
block.py Normal file
View File

@@ -0,0 +1,10 @@
class Block:
"""A Block represents a single square on the Tetris board.
Blocks are part of a Piece while they are 'alive'.
"""
character = "X"
color = "blue"
alive = True
def __init__(self, position):
self.position = position

7
game.py Normal file
View File

@@ -0,0 +1,7 @@
from retro.game import Game
from manager import Manager
agents = [Manager()]
state = {'level': 1}
game = Game(agents, state, board_size=(20, 20), debug=True)
game.play()

24
manager.py Normal file
View File

@@ -0,0 +1,24 @@
from piece import Piece, PIECE_DEFINITIONS
from random import choice
from retro.errors import AgentNotFoundByName
class Manager:
"""The Manager takes care of stuff that isn't anyone else's responsibility:
- Create a Piece whenever none exists.
- Clear full rows of Blocks (and move other Blocks down).
- End the game when the Blocks pile up all the way.
"""
display = False
def play_turn(self, game):
try:
game.get_agent_by_name("piece")
except AgentNotFoundByName:
self.create_piece(game)
def create_piece(self, game):
width, height = game.board_size
piece = Piece((width//2, 2), game, choice(PIECE_DEFINITIONS))
game.add_agent(piece)

100
piece.py Normal file
View File

@@ -0,0 +1,100 @@
from block import Block
PIECE_DEFINITIONS = [
[(-1, 0), (0, 0), (1, 0), (2, 0)],
[(0, 0), (1, 0), (0, 1), (1, 1)],
]
class Piece:
"""A Piece is a group of blocks which are 'alive':
They fall and the player can rotate or move them.
A Piece is created with a position, the game, and a list of block_offsets,
each of which represents the location of one of the Piece's
Blocks relative to the Piece position.
"""
name = "piece"
display = False
def __init__(self, position, game, block_offsets):
self.position = position
self.blocks = {}
for offset in block_offsets:
self.create_block(game, offset)
def handle_keystroke(self, keystroke, game):
x, y = self.position
if keystroke.name == "KEY_LEFT":
new_position = (x - 1, y)
if self.can_move_to(new_position, game):
self.move_to(new_position)
elif keystroke.name == "KEY_RIGHT":
new_position = (x + 1, y)
if self.can_move_to(new_position, game):
self.move_to(new_position)
def play_turn(self, game):
if self.should_fall(game):
self.fall(game)
def should_fall(self, game):
"""Determines whether the piece should fall.
Currently, the Piece falls every third turn.
In the future, the Piece should fall slowly at first, and
then should fall faster at higher levels.
"""
return game.turn_number % 3 == 0
def fall(self, game):
x, y = self.position
falling_position = (x, y + 1)
if self.can_move_to(falling_position, game):
self.move_to(falling_position)
else:
self.destroy(game)
def can_move_to(self, new_position, game):
"""Checks whether the Piece can move to a new position.
For every one of the Piece's Blocks, finds where that block
would be after the move, and checks whether there are any dead agents
already there (live agents would be Blocks which are part of this Piece,
not a problem since they'll be moving too).
"""
x, y = new_position
agents_by_position = game.get_agents_by_position()
for offset in self.blocks.keys():
ox, oy = offset
new_block_position = (x+ox, y+oy)
if not game.on_board(new_block_position):
return False
for agent in agents_by_position[new_block_position]:
if not agent.alive:
return False
return True
def move_to(self, position):
"""Move to position and updates positions of Blocks.
"""
x, y = position
self.position = position
for offset, block in self.blocks.items():
ox, oy = offset
block.position = (x + ox, y + oy)
def create_block(self, game, offset):
x, y = self.position
ox, oy = offset
block = Block((x + ox, y + oy))
self.blocks[offset] = block
game.add_agent(block)
def destroy(self, game):
"""Causes the Piece to destroy itself.
All the Blocks are set to dead.
"""
for block in self.blocks.values():
block.alive = False
game.remove_agent(self)

BIN
planning.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

8
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]] [[package]]
name = "ansicon" name = "ansicon"
@@ -48,14 +48,14 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "retro-games" name = "retro-games"
version = "1.1.0" version = "1.1.3"
description = "A simple framework for Terminal-based games" description = "A simple framework for Terminal-based games"
optional = false optional = false
python-versions = "<4.0,>=3.10" python-versions = "<4.0,>=3.10"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "retro_games-1.1.0-py3-none-any.whl", hash = "sha256:c621117e4dd528b1e4870d897d00c4365566ab3ba965177e3996ed3c889dd9f8"}, {file = "retro_games-1.1.3-py3-none-any.whl", hash = "sha256:4bdd27241b5cb3ee72e69a042d301ff58df2a2ade7e3c29400a538fa54e30148"},
{file = "retro_games-1.1.0.tar.gz", hash = "sha256:2167b574f42fe1e739b7c9ec75e98a9b76df42e2166376b85559291b3dc58f82"}, {file = "retro_games-1.1.3.tar.gz", hash = "sha256:4f91ff725e551820aa4e30c12c0264e2da41967ed34252122b7136bc2a8ed311"},
] ]
[package.dependencies] [package.dependencies]