generated from mwc/project_game
Add tetris example
This commit is contained in:
10
tetris/block.py
Normal file
10
tetris/block.py
Normal 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
tetris/game.py
Normal file
7
tetris/game.py
Normal 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
tetris/manager.py
Normal file
24
tetris/manager.py
Normal 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
tetris/piece.py
Normal file
100
tetris/piece.py
Normal 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
tetris/planning.jpg
Normal file
BIN
tetris/planning.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
91
tetris/poetry.lock
generated
Normal file
91
tetris/poetry.lock
generated
Normal file
@@ -0,0 +1,91 @@
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "ansicon"
|
||||
version = "1.89.0"
|
||||
description = "Python wrapper for loading Jason Hood's ANSICON"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
|
||||
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blessed"
|
||||
version = "1.20.0"
|
||||
description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities."
|
||||
optional = false
|
||||
python-versions = ">=2.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"},
|
||||
{file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""}
|
||||
six = ">=1.9.0"
|
||||
wcwidth = ">=0.1.4"
|
||||
|
||||
[[package]]
|
||||
name = "jinxed"
|
||||
version = "1.3.0"
|
||||
description = "Jinxed Terminal Library"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5"},
|
||||
{file = "jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
ansicon = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "retro-games"
|
||||
version = "1.1.3"
|
||||
description = "A simple framework for Terminal-based games"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "retro_games-1.1.3-py3-none-any.whl", hash = "sha256:4bdd27241b5cb3ee72e69a042d301ff58df2a2ade7e3c29400a538fa54e30148"},
|
||||
{file = "retro_games-1.1.3.tar.gz", hash = "sha256:4f91ff725e551820aa4e30c12c0264e2da41967ed34252122b7136bc2a8ed311"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blessed = ">=1.20.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
|
||||
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "03cc38c17964eb2c920ecf014cbfcf966c0c719418a127947b33382f086a0a6e"
|
||||
27
tetris/proposal.md
Normal file
27
tetris/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Game name
|
||||
|
||||
Tessera
|
||||
|
||||
## Vision
|
||||
|
||||
Tessera takes classic Tetris and adds a light puzzle layer: arranged clears that match colors grant small bonuses, rewarding planning without changing the tight, fast arcade feel. It's worth making because it blends a familiar, addictive core with a gentle twist that increases depth for skilled players while staying approachable.
|
||||
|
||||
## What the game will look like
|
||||
|
||||
A clean, modern pixel/flat aesthetic with bright, readable tetromino colors on a dark background; the HUD shows next pieces, hold slot, score, level, and subtle particle effects on clears.
|
||||
|
||||
## Player interaction
|
||||
|
||||
Players use keyboard controls to move, rotate, soft/hard drop, and hold pieces; menus use simple mouse/keyboard navigation. The game emphasizes quick decision-making and responsive controls with a visible ghost piece to aid placement.
|
||||
|
||||
## Core mechanics
|
||||
|
||||
1. Falling tetromino placement and rotation — move and rotate pieces to fit them into the well.
|
||||
2. Line clearing and gravity — completed rows clear and the above rows shift down, possibly chaining combos.
|
||||
3. Hold and next-queue — store one piece and preview upcoming pieces to plan ahead.
|
||||
4. Color-match bonus (optional) — clearing lines where most blocks share a color grants small score/energy bonuses to encourage pattern play.
|
||||
|
||||
## Milestone (first target)
|
||||
|
||||
Implement the core game engine: represent the board and pieces, support spawning, movement, rotation, collision detection, locking, and single-line clearing so you can run a minimal playable loop and unit tests for the engine.
|
||||
|
||||
21
tetris/pyproject.toml
Normal file
21
tetris/pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "project-game"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "Chris Proctor",email = "chris@chrisproctor.net"}
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<4.0"
|
||||
dependencies = [
|
||||
"retro-games (>=1.1.0,<2.0.0)"
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
package-mode = false
|
||||
Reference in New Issue
Block a user