generated from mwc/project_game
Compare commits
5 Commits
1631bef4c0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98f6f2716c | ||
|
|
267fffa9b0 | ||
|
|
d6adfef5a1 | ||
|
|
298a5fc58c | ||
|
|
f80e4dca48 |
Binary file not shown.
Binary file not shown.
BIN
__pycache__/manager.cpython-311.pyc
Normal file
BIN
__pycache__/manager.cpython-311.pyc
Normal file
Binary file not shown.
17
catcher.py
17
catcher.py
@@ -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
|
||||
|
||||
66
fruit.py
66
fruit.py
@@ -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
17
game.py
@@ -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
18
manager.py
Normal 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
2
nav.py
@@ -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
8
poetry.lock
generated
@@ -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]
|
||||
|
||||
@@ -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
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