Compare commits

...

5 Commits

Author SHA1 Message Date
kdang
98f6f2716c I will finish the rest of the project at home 2026-01-16 09:36:38 -05:00
kdang
267fffa9b0 @chris My fruit offset does not work 2026-01-09 09:49:15 -05:00
Chris Proctor
d6adfef5a1 Add tetris example 2026-01-07 09:46:26 -05:00
kdang
298a5fc58c Merge branch 'main' of https://git.makingwithcode.org/kdang/project_game 2025-12-18 09:35:07 -05:00
kdang
f80e4dca48 @Chris, If you run the game.py file and dont move
for a few seconds you get a crash. We also cannot
figure out how to make the fruit fall. please take
a look at the crash and tell us how to make the
fruit fall. Thank you!-Conor
2025-12-17 09:39:29 -05:00
18 changed files with 365 additions and 61 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,18 @@
from random import randint
class CatcherPiece:
character = "-"
color = "white_on_indigo"
def __init__(self, position):
self.position = position
def play_turn(self, game):
fruit = game.get_agent_by_name("fruit")
if self.position == fruit.position:
game.state['Score'] += 1
class Catcher:
width = 7
width = 6
display = False
pieces = []
name = "catcher"
@@ -29,6 +36,14 @@ class Catcher:
if x + self.width < width:
self.position = (x+1, y)
self.update_piece_positions()
self.checkforfruitcollision(game)
def checkforfruitcollision(self, game):
for piece in self.pieces:
if piece.collision(game):
game.remove_agent(fruit)
game.state['Score'] += 1
def create_pieces(self, game):
x, y = self.position

View File

@@ -1,70 +1,56 @@
from random import randint
SHAPE_DEFINITIONS = [
[(0,0)],
[(0, 0), (1, 0), (0, 1), (1, 1)],
]
class FruitPiece:
character = "@"
color = "green_on_indigo"
display = True
def __init__(self, position):
self.position = position
class Fruit:
width = 2
height = 2
height = 1
display = False
pieces = []
name = "fruit"
character = "@"
color = "green_on_indigo"
def __init__(self, position):
def __init__(self, position, game, shape_offsets):
self.position = position
self.pieces = {}
for offset in shape_offsets:
self.create_shape(game, offset)
def play_turn(self, game):
if not self.pieces:
self.create_pieces(game)
if game.turn_number % 2 == 0:
if game.turn_number % 3 == 0:
x, y = self.position
if y == 24:
if y == 29:
game.remove_agent(self)
else:
catcher = game.get_agent_by_name('catcher')
new_position = (x, y + 1)
if new_position == catcher.position:
catcher.explode()
game.end()
else:
self.position = new_position
catcher = game.get_agent_by_name("catcher")
new_position = (x, y + 1)
def create_pieces(self, game):
def create_shape(self, game, offset):
x, y = self.position
self.pieces = []
for i in range(self.width):
piece = FruitPiece((x + i, y))
self.pieces.append(piece)
game.add_agent(piece)
for i in range(self.height):
piece = FruitPiece((x, y + i))
self.pieces.append(piece)
ox, oy = offset
piece = FruitPiece((x + ox, y + oy))
self.pieces[offset] = piece
game.add_agent(piece)
def update_piece_positions(self):
if game.turn_number % 2 == 0:
if game.turn_number % 3 == 0:
self.set_color()
x, y = self.position
if y == HEIGHT - 1:
if y == 29:
game.remove_agent(self)
else:
ship = game.get_agent_by_name('ship')
new_position = (x, y + 1)
if new_position == ship.position:
ship.explode()
game.end()
else:
self.position = new_position
class FruitSpawner:
display = False
def play_turn(self, game):
if self.should_spawn_fruit(game.turn_number):
asteroid = Fruit((randint(0, WIDTH - 1), 0))
game.add_agent(fruit)
def should_spawn_fruit(self, turn_number):
return randint(0, 1000) < turn_number
catcher = game.get_agent_by_name("catcher")
new_position = (x, y + 1)

17
game.py
View File

@@ -1,20 +1,17 @@
from random import randint
from retro.game import Game
from retro.graph import Graph
from catcher import Catcher
from fruit import Fruit
from manager import FruitManager
import json
g = Graph()
g.get_or_create_edge(26, 10, 26, 22)
agents = g.get_agents()
for agent in agents:
agent.color = "white_on_indigo"
WIDTH = 27
HEIGHT = 30
agents = [
Catcher((14, 24)),
Fruit((14, 2)),
Catcher((11, 29)),
FruitManager(),
]
state = {'Score': 0}
game = Game(agents, state, board_size=(35, 25), framerate=24, color="white_on_indigo")
game = Game(agents, state, board_size=(WIDTH, HEIGHT), framerate=24, color="white_on_indigo")
game.play()

18
manager.py Normal file
View File

@@ -0,0 +1,18 @@
from fruit import Fruit, SHAPE_DEFINITIONS
from random import choice, randint
from retro.errors import AgentNotFoundByName
class FruitManager:
display = False
def play_turn(self, game):
try:
game.get_agent_by_name("fruit")
except AgentNotFoundByName:
self.create_piece(game)
def create_piece(self, game):
x = randint(0, 26)
fruit = Fruit((x, 1), game, choice(SHAPE_DEFINITIONS))
game.add_agent(fruit)

2
nav.py
View File

@@ -2,7 +2,7 @@ from random import randint
from retro.game import Game
HEIGHT = 25
WIDTH = 2
WIDTH = 4
class Spaceship:
"""A player-controlled agent which moves left and right, dodging asteroids.

8
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
[[package]]
name = "ansicon"
@@ -50,14 +50,14 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "retro-games"
version = "1.1.1"
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.1-py3-none-any.whl", hash = "sha256:2b2eac8c2667c69f1dd90c083a0f58b948e6fdecb184720133b819fa78f8a57f"},
{file = "retro_games-1.1.1.tar.gz", hash = "sha256:67ce475191f78d13148028de17b97de099226c4c581d5cf811cd9dfc7f5bb420"},
{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]

View File

@@ -1,9 +1,9 @@
[project]
name = "project-game"
name = "fruit-catcher"
version = "0.1.0"
description = ""
authors = [
{name = "Chris Proctor",email = "chris@chrisproctor.net"}
{name = "Kayden Dang, Connor ",email = "chris@chrisproctor.net"}
]
license = {text = "MIT"}
readme = "README.md"
@@ -12,6 +12,14 @@ dependencies = [
"retro-games (>=1.1.1,<2.0.0)"
]
[project.scripts]
play = "game:play"
[tool.retro]
authors = "Kayden, Connor"
description = "Use your buttons to help the catcher catch the falling fruits. Don't drop too many fruits!"
instructions = "SCore as many points as possible before losing by using the two designated buttons to move left and right."
result_file = "result.json"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]

10
tetris/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
tetris/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
tetris/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
tetris/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
tetris/planning.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

91
tetris/poetry.lock generated Normal file
View 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
View 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
View 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