cetragm 0.1.2__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.

Potentially problematic release.


This version of cetragm might be problematic. Click here for more details.

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 cgm.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 cgm.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 cgm.tables import pieces, grades
6
+ from cgm.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 cgm.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