cetragm 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cetragm/__init__.py +16 -0
- cetragm/audio.py +1 -0
- cetragm/bag.py +24 -0
- cetragm/config.py +18 -0
- cetragm/controls.py +102 -0
- cetragm/draw.py +154 -0
- cetragm/game.py +101 -0
- cetragm/main.py +322 -0
- cetragm/player.py +78 -0
- cetragm/srs.py +1 -0
- cetragm/tables.py +416 -0
- cetragm/ui.txt +48 -0
- cetragm-0.1.3.dist-info/METADATA +71 -0
- cetragm-0.1.3.dist-info/RECORD +17 -0
- cetragm-0.1.3.dist-info/WHEEL +4 -0
- cetragm-0.1.3.dist-info/entry_points.txt +4 -0
- cetragm-0.1.3.dist-info/licenses/LICENSE +674 -0
cetragm/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Structure:
|
|
2
|
+
# main.py
|
|
3
|
+
# |- audio.py - sfx, music
|
|
4
|
+
# |- controls.py - das, arr
|
|
5
|
+
# |- draw.py - ui, stdout, terminal
|
|
6
|
+
# |- srs.py - wallkicks and rotation
|
|
7
|
+
# |
|
|
8
|
+
# |- game.py - scoring, logic, hold, etc
|
|
9
|
+
# |- pieces.py - define tetrominoes
|
|
10
|
+
# |- config.py - rebind controls, gravity, etc
|
|
11
|
+
# |
|
|
12
|
+
# \- /assets - for audio.py (simpleaudio)
|
|
13
|
+
# |- /sfx
|
|
14
|
+
# \- /music
|
|
15
|
+
|
|
16
|
+
pass
|
cetragm/audio.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Module for sound effects and persistent audio
|
cetragm/bag.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 7-bag randomizer
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from cetragm.tables import pieces
|
|
5
|
+
|
|
6
|
+
PIECE_BAG = list(pieces.keys())
|
|
7
|
+
|
|
8
|
+
class Bag:
|
|
9
|
+
def __init__(self, preview_size: int = 5):
|
|
10
|
+
self.preview_size = preview_size
|
|
11
|
+
self.current_bag = random.sample(PIECE_BAG, len(PIECE_BAG))
|
|
12
|
+
self.next_bag = random.sample(PIECE_BAG, len(PIECE_BAG))
|
|
13
|
+
|
|
14
|
+
def get_piece(self):
|
|
15
|
+
piece = self.current_bag.pop(0)
|
|
16
|
+
if not self.current_bag:
|
|
17
|
+
self.current_bag = self.next_bag
|
|
18
|
+
self.next_bag = random.sample(PIECE_BAG, len(PIECE_BAG))
|
|
19
|
+
return piece
|
|
20
|
+
|
|
21
|
+
def get_preview(self):
|
|
22
|
+
upcoming = self.current_bag + self.next_bag
|
|
23
|
+
return upcoming[:self.preview_size]
|
|
24
|
+
|
cetragm/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Module for configuration the user probably wouldn't see, keymaps and gravity etc
|
|
2
|
+
|
|
3
|
+
MAX_LOCK_RESETS = 15
|
|
4
|
+
ARE_FRAMES = 20
|
|
5
|
+
LINE_CLEAR_FRAMES = 20
|
|
6
|
+
LOCK_DELAY_FRAMES = 30
|
|
7
|
+
|
|
8
|
+
controls = {
|
|
9
|
+
"move_left": ["a", "j", "\x1b[D"], # left arrow
|
|
10
|
+
"move_right": ["d", "l", "\x1b[C"], # right
|
|
11
|
+
"soft_drop": ["s", "k", "\x1b[B"], # down
|
|
12
|
+
"hard_drop": ["w", "i"],
|
|
13
|
+
"rotate_cw": ["c", " ", "\x1b[A", "/"], # up
|
|
14
|
+
"rotate_ccw": ["z", "q", ","],
|
|
15
|
+
"rotate_180": ["\t", "x", "."], # tab
|
|
16
|
+
"hold": ["e", "v"],
|
|
17
|
+
"pause": ["\x1b", "p"],
|
|
18
|
+
}
|
cetragm/controls.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Module for binding and rebinding controls as well as taking input (threaded)
|
|
2
|
+
# DAS/ARR logic
|
|
3
|
+
|
|
4
|
+
from cetragm.config import controls
|
|
5
|
+
import termios
|
|
6
|
+
import tty
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
import queue
|
|
10
|
+
import select
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
class InputHandler:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.queue = queue.Queue()
|
|
16
|
+
self.stop_flag = threading.Event()
|
|
17
|
+
self.is_windows = os.name == "nt"
|
|
18
|
+
self.fd = None
|
|
19
|
+
self.old_settings = None
|
|
20
|
+
|
|
21
|
+
def start(self):
|
|
22
|
+
self.thread = threading.Thread(target=self._poll_loop, daemon=True)
|
|
23
|
+
self.thread.start()
|
|
24
|
+
|
|
25
|
+
def stop(self):
|
|
26
|
+
self.stop_flag.set()
|
|
27
|
+
if self.is_windows:
|
|
28
|
+
return
|
|
29
|
+
if self.fd is not None and self.old_settings is not None:
|
|
30
|
+
try:
|
|
31
|
+
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings)
|
|
32
|
+
except termios.error:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def _poll_loop(self):
|
|
36
|
+
if self.is_windows:
|
|
37
|
+
self._poll_windows()
|
|
38
|
+
else:
|
|
39
|
+
self._poll_linux()
|
|
40
|
+
|
|
41
|
+
def _poll_windows(self):
|
|
42
|
+
import msvcrt
|
|
43
|
+
while not self.stop_flag.is_set():
|
|
44
|
+
if msvcrt.kbhit():
|
|
45
|
+
ch = msvcrt.getwch()
|
|
46
|
+
self._process_key(ch)
|
|
47
|
+
else:
|
|
48
|
+
threading.Event().wait(0.01)
|
|
49
|
+
|
|
50
|
+
def _poll_linux(self):
|
|
51
|
+
try:
|
|
52
|
+
self.fd = os.open("/dev/tty", os.O_RDONLY)
|
|
53
|
+
except OSError:
|
|
54
|
+
self.fd = sys.stdin.fileno()
|
|
55
|
+
|
|
56
|
+
if not os.isatty(self.fd):
|
|
57
|
+
print("not a tty!! failing out", file=sys.stderr)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self.old_settings = termios.tcgetattr(self.fd)
|
|
61
|
+
tty.setcbreak(self.fd)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
while not self.stop_flag.is_set():
|
|
65
|
+
r, _, _ = select.select([self.fd], [], [], 0.02)
|
|
66
|
+
if not r:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
ch = os.read(self.fd, 1).decode(errors="ignore")
|
|
71
|
+
except OSError as e:
|
|
72
|
+
print("OSerror:", e, file=sys.stderr)
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if not ch:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if ch == "\x1b":
|
|
79
|
+
seq = ch
|
|
80
|
+
while select.select([self.fd], [], [], 0.001)[0]:
|
|
81
|
+
seq += os.read(self.fd, 1).decode(errors="ignore")
|
|
82
|
+
key = seq
|
|
83
|
+
else:
|
|
84
|
+
key = ch
|
|
85
|
+
self._process_key(key)
|
|
86
|
+
finally:
|
|
87
|
+
if self.old_settings is not None and os.isatty(self.fd):
|
|
88
|
+
try:
|
|
89
|
+
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings)
|
|
90
|
+
except termios.error:
|
|
91
|
+
pass
|
|
92
|
+
if self.fd not in (None, sys.stdin.fileno()):
|
|
93
|
+
try:
|
|
94
|
+
os.close(self.fd)
|
|
95
|
+
except OSError as e:
|
|
96
|
+
print(e, file=sys.stderr)
|
|
97
|
+
|
|
98
|
+
def _process_key(self, key):
|
|
99
|
+
for action, binds in controls.items():
|
|
100
|
+
if key in binds:
|
|
101
|
+
self.queue.put(action)
|
|
102
|
+
break
|
cetragm/draw.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# DRAW.PY
|
|
2
|
+
# Module for drawing to the screen (stdout tomfuckery time!!!)
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
from cetragm.tables import pieces, grades
|
|
6
|
+
from cetragm.game import collides
|
|
7
|
+
|
|
8
|
+
BLOCK = "██"
|
|
9
|
+
SHADOW = "▒▒"
|
|
10
|
+
EMPTY = "\x1b[48;2;20;20;20m\x1b[38;2;40;40;40m[]\x1b[49m\x1b[39m"
|
|
11
|
+
TOP_EMPTY = " "
|
|
12
|
+
|
|
13
|
+
termwidth = 0
|
|
14
|
+
termheight = 0
|
|
15
|
+
|
|
16
|
+
def validate_board(board): # check for dimensions in case game loop fucked me
|
|
17
|
+
if len(board) != 22:
|
|
18
|
+
raise ValueError(f"Board has {len(board)} rows")
|
|
19
|
+
widths = {len(r) for r in board}
|
|
20
|
+
if widths != {10}:
|
|
21
|
+
raise ValueError(f"Board has rows not 10 wide - {widths}")
|
|
22
|
+
|
|
23
|
+
def format_time(ms: int) -> str: # for TGM style timer at the bottom
|
|
24
|
+
minutes = (ms // 1000) // 60 # Shame i can't make it obnoxiously big
|
|
25
|
+
seconds = (ms // 1000) % 60 # or can i...? *mice on venus*
|
|
26
|
+
hundredths = (ms % 1000) // 10
|
|
27
|
+
return f"{minutes:02}:{seconds:02}:{hundredths:02}"
|
|
28
|
+
|
|
29
|
+
def color_block(piece_id: str | None): # in ["i", "z", "s", "l", "j", "t", "o"]
|
|
30
|
+
if (not piece_id) and (not piece_id.endswith("_sh")):
|
|
31
|
+
return EMPTY
|
|
32
|
+
if not piece_id.endswith("_sh"):
|
|
33
|
+
r, g, b = pieces[piece_id]["rgb"]
|
|
34
|
+
return f"\x1b[38;2;{r};{g};{b}m{BLOCK}\x1b[0m"
|
|
35
|
+
else:
|
|
36
|
+
return f"\x1b[38;2;130;130;130m{SHADOW}\x1b[0m"
|
|
37
|
+
|
|
38
|
+
# public
|
|
39
|
+
def draw_board(board, # from game.py. Board: 2d list. Entries: [0] or [1, "t"].
|
|
40
|
+
active_piece = None, # {"name": "t", "pos": (0, 0), "rotation": "0"}
|
|
41
|
+
score: int = 0, grade: str = "9", # score is used to calculate TGM grade (9 -> 1 -> S1 -> S9 -> Gm)
|
|
42
|
+
time_ms: int = 0, # BIG FLASHING TIMER
|
|
43
|
+
lines: int = 0, line_goal: int = 0, # for gravity level up with timer
|
|
44
|
+
hold: str | None = None, # hold = "t"
|
|
45
|
+
next_queue: list[str] | None = None # list of 5 pieces like ["t", "i", "z", "s", "o"] (7-bag)
|
|
46
|
+
):
|
|
47
|
+
validate_board(board)
|
|
48
|
+
height = len(board)
|
|
49
|
+
width = len(board[0])
|
|
50
|
+
|
|
51
|
+
# display on top of static board then check collision in game.py so it doesn't lag render loop
|
|
52
|
+
overlay = [[cell[:] for cell in row] for row in board] # already the full board, needs to be colored
|
|
53
|
+
|
|
54
|
+
# SHADOW PIECE
|
|
55
|
+
if active_piece:
|
|
56
|
+
pid = active_piece["name"]
|
|
57
|
+
shape = pieces[pid]["rotations"][active_piece["rotation"]]
|
|
58
|
+
px, py = active_piece["pos"]
|
|
59
|
+
|
|
60
|
+
drop_y = py
|
|
61
|
+
while True:
|
|
62
|
+
drop_y += 1
|
|
63
|
+
if collides({"name": pid, "rotation": active_piece["rotation"], "pos": [px, drop_y]}, board):
|
|
64
|
+
drop_y -= 1
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
for y, row in enumerate(shape):
|
|
68
|
+
for x, val in enumerate(row):
|
|
69
|
+
if val:
|
|
70
|
+
bx, by = px + x, drop_y + y
|
|
71
|
+
if 0 <= bx < width and 0 <= by < height:
|
|
72
|
+
overlay[by][bx] = [1, f"{pid}_sh"]
|
|
73
|
+
|
|
74
|
+
# ACTUAL PIECE
|
|
75
|
+
if active_piece:
|
|
76
|
+
pid = active_piece["name"]
|
|
77
|
+
shape = pieces[pid]["rotations"][active_piece["rotation"]]
|
|
78
|
+
px, py = active_piece["pos"]
|
|
79
|
+
for y, row in enumerate(shape):
|
|
80
|
+
for x, val in enumerate(row):
|
|
81
|
+
if val:
|
|
82
|
+
bx, by = px + x, py + y
|
|
83
|
+
if 0 <= bx < width and 0 <= by < height:
|
|
84
|
+
overlay[by][bx] = [1, pid]
|
|
85
|
+
|
|
86
|
+
left_lines = []
|
|
87
|
+
left_lines.append("│ HOLD ▼ │")
|
|
88
|
+
left_lines.append("╞════════╡")
|
|
89
|
+
if hold and hold in pieces:
|
|
90
|
+
for line in pieces[hold]["piece"]:
|
|
91
|
+
left_lines.append(f"│{line.center(8)}│") # should already be centered but why not do it again
|
|
92
|
+
else:
|
|
93
|
+
left_lines.extend(["│ ╲╱ │", "│ ╱╲ │"])
|
|
94
|
+
left_lines.append("╰────────┤")
|
|
95
|
+
for i in grades[grade]:
|
|
96
|
+
left_lines.append(i + "│")
|
|
97
|
+
|
|
98
|
+
right_lines = []
|
|
99
|
+
right_lines.append("│ NEXT ▼ │")
|
|
100
|
+
right_lines.append("╞════════╡")
|
|
101
|
+
|
|
102
|
+
if next_queue:
|
|
103
|
+
for num, name in enumerate(next_queue[:5]):
|
|
104
|
+
if name in pieces:
|
|
105
|
+
for line in pieces[name]["piece"]:
|
|
106
|
+
right_lines.append(f"│{line.center(8)}│")
|
|
107
|
+
else:
|
|
108
|
+
right_lines.extend(["│ ╲╱ │", "│ ╱╲ │"])
|
|
109
|
+
right_lines.append("├────────┤" if num != 4 else "├────────╯")
|
|
110
|
+
else:
|
|
111
|
+
for i in range(5):
|
|
112
|
+
right_lines.extend(["│ ╲╱ │", "│ ╱╲ │", "├────────┤" if i != 4 else "├────────╯"])
|
|
113
|
+
right_lines.append( "│ ")
|
|
114
|
+
right_lines.append( "│ LEVEL: ")
|
|
115
|
+
right_lines.append(f"│ \x1b[4m{str(lines):^3}\x1b[24m")
|
|
116
|
+
right_lines.append(f"│ {str(line_goal).ljust(9)}")
|
|
117
|
+
|
|
118
|
+
frame_lines = []
|
|
119
|
+
|
|
120
|
+
score = str(score) if len(str(score)) % 2 == 0 else ("0" + str(score))
|
|
121
|
+
wid = (width - (len(score) // 2)) - 1
|
|
122
|
+
frame_lines.append("╭────────┬" + "─" * (wid) + f"╢{score}╟" + "─" * (wid) + "┬────────╮")
|
|
123
|
+
|
|
124
|
+
for y in range(height):
|
|
125
|
+
row_buf = []
|
|
126
|
+
for x in range(width):
|
|
127
|
+
cell = overlay[y][x]
|
|
128
|
+
if y > 1:
|
|
129
|
+
row_buf.append(color_block(cell[1]) if cell[0] else EMPTY)
|
|
130
|
+
else:
|
|
131
|
+
row_buf.append(color_block(cell[1]) if cell[0] else TOP_EMPTY)
|
|
132
|
+
|
|
133
|
+
right = right_lines[y] if y < len(right_lines) else "│ "
|
|
134
|
+
left = left_lines[y] if y < len(left_lines) else " │"
|
|
135
|
+
frame_lines.append(left + "".join(row_buf) + right)
|
|
136
|
+
|
|
137
|
+
frame_lines.append( " ├" + "──" * width + "┤")
|
|
138
|
+
frame_lines.append(f" │ ▶ {format_time(time_ms):^14}◀ │")
|
|
139
|
+
frame_lines.append( " ╰" + "──" * width + "╯")
|
|
140
|
+
|
|
141
|
+
term = shutil.get_terminal_size()
|
|
142
|
+
frame_width = len(frame_lines[0])
|
|
143
|
+
pad_x = max((term.columns - frame_width) // 2, 0)
|
|
144
|
+
pad_y = max((term.lines - len(frame_lines)) // 2, 0)
|
|
145
|
+
|
|
146
|
+
global termwidth, termheight
|
|
147
|
+
if pad_x != termwidth or pad_y != termheight:
|
|
148
|
+
sys.stdout.write("\x1b[H\x1b[2J")
|
|
149
|
+
termwidth, termheight = pad_x, pad_y
|
|
150
|
+
|
|
151
|
+
centered = ("\n" * pad_y) + "\n".join(" " * pad_x + ln for ln in frame_lines)
|
|
152
|
+
|
|
153
|
+
sys.stdout.write("\x1b[H" + centered + "\n\n")
|
|
154
|
+
sys.stdout.flush()
|
cetragm/game.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# core logic like lock, gravity, and scoring
|
|
2
|
+
from cetragm.tables import pieces, ROT_180, ROT_CCW, ROT_CW
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
def get_cells(piece): # {"name": "i", "pos": (0, 0), "rotation": "0"}
|
|
6
|
+
# Gets the cells in a falling piece with its current rotation and coordinates on the board
|
|
7
|
+
shape = pieces[piece["name"]]["rotations"][piece["rotation"]]
|
|
8
|
+
px, py = piece["pos"]
|
|
9
|
+
cells = [(px + x, py + y)
|
|
10
|
+
for y, row in enumerate(shape)
|
|
11
|
+
for x, val in enumerate(row) if val]
|
|
12
|
+
return cells
|
|
13
|
+
|
|
14
|
+
def collides(piece, board):
|
|
15
|
+
# Check if the piece is colliding (will want to write a new one for srs.py)
|
|
16
|
+
for (x, y) in get_cells(piece):
|
|
17
|
+
if x < 0 or x >= len(board[0]) or y >= len(board):
|
|
18
|
+
return True
|
|
19
|
+
if y >= 0 and board[y][x][0]:
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def lock_piece(piece, board, player):
|
|
24
|
+
if collides(piece, board):
|
|
25
|
+
if piece["pos"][1] == 0:
|
|
26
|
+
return board, 0, True
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError("Tried to lock outside the board or lock piece over another piece (and it wasn't a loss)")
|
|
29
|
+
for i in get_cells(piece):
|
|
30
|
+
board[i[1]][i[0]] = [1, piece["name"]]
|
|
31
|
+
|
|
32
|
+
board, cleared = clear_lines(board)
|
|
33
|
+
board_empty = all(cell[0] for row in board for cell in row)
|
|
34
|
+
soft = player.soft # TODO: ADD SOFT, OR SOFT DROP CALC
|
|
35
|
+
|
|
36
|
+
score_gain, player.combo = get_score(player.level, cleared, player.combo, board_empty, soft)
|
|
37
|
+
player.score += score_gain
|
|
38
|
+
|
|
39
|
+
update_level(player, cleared)
|
|
40
|
+
|
|
41
|
+
return board, cleared, False
|
|
42
|
+
|
|
43
|
+
def clear_lines(board):
|
|
44
|
+
new_board = []
|
|
45
|
+
cleared = 0
|
|
46
|
+
|
|
47
|
+
for row in board:
|
|
48
|
+
if all(cell[0] for cell in row):
|
|
49
|
+
cleared += 1
|
|
50
|
+
else:
|
|
51
|
+
new_board.append(row)
|
|
52
|
+
|
|
53
|
+
for _ in range(cleared):
|
|
54
|
+
new_board.insert(0, [[0] for _ in range(len(board[0]))])
|
|
55
|
+
|
|
56
|
+
return new_board, cleared
|
|
57
|
+
|
|
58
|
+
def get_score(level, lines_cleared, combo, board_empty, soft):
|
|
59
|
+
if lines_cleared == 0:
|
|
60
|
+
return 0, 1 # 0 score, reset combo
|
|
61
|
+
|
|
62
|
+
combo = combo + (2 * lines_cleared) - 2 if combo > 1 else (2 * lines_cleared) - 1
|
|
63
|
+
if combo <1:
|
|
64
|
+
combo = 1
|
|
65
|
+
bravo = 4 if board_empty else 1
|
|
66
|
+
|
|
67
|
+
score = (math.ceil((level + lines_cleared) / 4) + soft) * lines_cleared * combo * bravo
|
|
68
|
+
return score, combo
|
|
69
|
+
|
|
70
|
+
def update_level(player, cleared):
|
|
71
|
+
if player.level >= 999:
|
|
72
|
+
player.line_goal = 999
|
|
73
|
+
player.level = 999
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
old_level = player.level
|
|
77
|
+
temp = old_level + 1 + int(cleared)
|
|
78
|
+
temp = 999 if temp > 999 else temp
|
|
79
|
+
|
|
80
|
+
old_sect = old_level // 100
|
|
81
|
+
new_sect = temp // 100
|
|
82
|
+
|
|
83
|
+
if new_sect > old_sect and cleared == 0:
|
|
84
|
+
temp = min(temp, (old_sect * 100 + 99))
|
|
85
|
+
|
|
86
|
+
player.level = min(temp, 999)
|
|
87
|
+
|
|
88
|
+
if player.level > 900:
|
|
89
|
+
player.line_goal = 999
|
|
90
|
+
else:
|
|
91
|
+
player.line_goal = ((player.level // 100) + 1) * 100
|
|
92
|
+
|
|
93
|
+
def rotate(current, direction):
|
|
94
|
+
if direction == +1:
|
|
95
|
+
return ROT_CW[current]
|
|
96
|
+
elif direction == -1:
|
|
97
|
+
return ROT_CCW[current]
|
|
98
|
+
elif direction == 2:
|
|
99
|
+
return ROT_180[current]
|
|
100
|
+
raise ValueError("direction should be -1, +1, or 2.")
|
|
101
|
+
return current
|