tetris-terminal 0.0.1a3__tar.gz → 0.0.2a1__tar.gz
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.
- {tetris_terminal-0.0.1a3/src/tetris_terminal.egg-info → tetris_terminal-0.0.2a1}/PKG-INFO +22 -14
- tetris_terminal-0.0.2a1/README.md +44 -0
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/pyproject.toml +6 -4
- tetris_terminal-0.0.2a1/src/tetris/__init__.py +3 -0
- tetris_terminal-0.0.2a1/src/tetris/cli.py +25 -0
- tetris_terminal-0.0.2a1/src/tetris/tetris.py +609 -0
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1/src/tetris_terminal.egg-info}/PKG-INFO +22 -14
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/SOURCES.txt +1 -0
- tetris_terminal-0.0.2a1/src/tetris_terminal.egg-info/requires.txt +3 -0
- tetris_terminal-0.0.1a3/README.md +0 -37
- tetris_terminal-0.0.1a3/src/tetris/__init__.py +0 -0
- tetris_terminal-0.0.1a3/src/tetris/cli.py +0 -18
- tetris_terminal-0.0.1a3/src/tetris/tetris.py +0 -199
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/LICENSE +0 -0
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/setup.cfg +0 -0
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/dependency_links.txt +0 -0
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/entry_points.txt +0 -0
- {tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tetris-terminal
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2a1
|
|
4
4
|
Summary: A tetris game runs in the terminal
|
|
5
5
|
Author-email: jayzhu <jay.l.zhu@foxmail.com>
|
|
6
|
-
Project-URL: homepage, https://github.com/zlh124/
|
|
6
|
+
Project-URL: homepage, https://github.com/zlh124/tetris-terminal
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Environment :: Console :: Curses
|
|
9
9
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Terminals
|
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
+
Requires-Dist: windows-curses; sys_platform == "win32"
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|

|
|
@@ -33,14 +34,18 @@ A terminal-based Tetris game written in Python using the `curses` library.
|
|
|
33
34
|
[]()
|
|
34
35
|
|
|
35
36
|
### Features
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- Next
|
|
37
|
+
- Modern Tetris design following the [Tetris Design Guideline](https://dn720004.ca.archive.org/0/items/2009-tetris-variant-concepts_202201/2009%20Tetris%20Design%20Guideline.pdf)
|
|
38
|
+
- [x] Extended Placement
|
|
39
|
+
- [x] Next Piece Preview
|
|
40
|
+
- [x] SRS System
|
|
41
|
+
- [x] Piece Holding
|
|
42
|
+
- [ ] Scoring System
|
|
43
|
+
- [ ] Level System
|
|
39
44
|
|
|
40
45
|
### Platform Support
|
|
41
46
|
Based on Python's [`curses`](https://docs.python.org/3/library/curses.html) module:
|
|
42
47
|
- ✅ **Linux/macOS**: Works out of the box
|
|
43
|
-
-
|
|
48
|
+
- ✅️ **Windows**: With [`windows-curses`](https://github.com/zephyrproject-rtos/windows-curses)
|
|
44
49
|
|
|
45
50
|
### Installation & Usage
|
|
46
51
|
```bash
|
|
@@ -49,16 +54,19 @@ tetris
|
|
|
49
54
|
```
|
|
50
55
|
|
|
51
56
|
### Controls
|
|
52
|
-
| Key
|
|
53
|
-
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
| `
|
|
58
|
-
|
|
|
57
|
+
| Key | Action |
|
|
58
|
+
|------------|------------|
|
|
59
|
+
| `a`, `←` | Move left |
|
|
60
|
+
| `d`, `→` | Move right |
|
|
61
|
+
|`w`, `↑`,`x`| Rotate cw |
|
|
62
|
+
| `z` | Rotate ccw |
|
|
63
|
+
| `s`, `↓` | Soft drop |
|
|
64
|
+
| `space` | Hard drop |
|
|
65
|
+
| `c` | Hold |
|
|
66
|
+
| `q` | Quit game |
|
|
59
67
|
|
|
60
68
|
### License
|
|
61
69
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
62
70
|
|
|
63
71
|
### Acknowledgements
|
|
64
|
-
|
|
72
|
+
Idea from [tinytetris](https://github.com/taylorconor/tinytetris) (a C implementation).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+

|
|
2
|
+
# Tetris Terminal🎮
|
|
3
|
+
A terminal-based Tetris game written in Python using the `curses` library.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[]()
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- Modern Tetris design following the [Tetris Design Guideline](https://dn720004.ca.archive.org/0/items/2009-tetris-variant-concepts_202201/2009%20Tetris%20Design%20Guideline.pdf)
|
|
10
|
+
- [x] Extended Placement
|
|
11
|
+
- [x] Next Piece Preview
|
|
12
|
+
- [x] SRS System
|
|
13
|
+
- [x] Piece Holding
|
|
14
|
+
- [ ] Scoring System
|
|
15
|
+
- [ ] Level System
|
|
16
|
+
|
|
17
|
+
### Platform Support
|
|
18
|
+
Based on Python's [`curses`](https://docs.python.org/3/library/curses.html) module:
|
|
19
|
+
- ✅ **Linux/macOS**: Works out of the box
|
|
20
|
+
- ✅️ **Windows**: With [`windows-curses`](https://github.com/zephyrproject-rtos/windows-curses)
|
|
21
|
+
|
|
22
|
+
### Installation & Usage
|
|
23
|
+
```bash
|
|
24
|
+
pip install tetris-terminal
|
|
25
|
+
tetris
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Controls
|
|
29
|
+
| Key | Action |
|
|
30
|
+
|------------|------------|
|
|
31
|
+
| `a`, `←` | Move left |
|
|
32
|
+
| `d`, `→` | Move right |
|
|
33
|
+
|`w`, `↑`,`x`| Rotate cw |
|
|
34
|
+
| `z` | Rotate ccw |
|
|
35
|
+
| `s`, `↓` | Soft drop |
|
|
36
|
+
| `space` | Hard drop |
|
|
37
|
+
| `c` | Hold |
|
|
38
|
+
| `q` | Quit game |
|
|
39
|
+
|
|
40
|
+
### License
|
|
41
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
42
|
+
|
|
43
|
+
### Acknowledgements
|
|
44
|
+
Idea from [tinytetris](https://github.com/taylorconor/tinytetris) (a C implementation).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tetris-terminal"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.2-alpha1"
|
|
8
8
|
description = "A tetris game runs in the terminal"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -12,7 +12,9 @@ license = { file = "MIT" }
|
|
|
12
12
|
|
|
13
13
|
authors = [{ name = "jayzhu", email = "jay.l.zhu@foxmail.com" }]
|
|
14
14
|
|
|
15
|
-
dependencies = [
|
|
15
|
+
dependencies = [
|
|
16
|
+
"windows-curses; sys_platform == 'win32'",
|
|
17
|
+
]
|
|
16
18
|
|
|
17
19
|
classifiers = [
|
|
18
20
|
"Development Status :: 3 - Alpha",
|
|
@@ -21,7 +23,7 @@ classifiers = [
|
|
|
21
23
|
"License :: OSI Approved :: MIT License",
|
|
22
24
|
"Operating System :: POSIX :: Linux",
|
|
23
25
|
"Operating System :: MacOS :: MacOS X",
|
|
24
|
-
"Operating System :: Microsoft :: Windows",
|
|
26
|
+
"Operating System :: Microsoft :: Windows",
|
|
25
27
|
"Programming Language :: Python :: 3",
|
|
26
28
|
"Programming Language :: Python :: 3.8",
|
|
27
29
|
"Programming Language :: Python :: 3.9",
|
|
@@ -34,7 +36,7 @@ classifiers = [
|
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.urls]
|
|
37
|
-
homepage = "https://github.com/zlh124/
|
|
39
|
+
homepage = "https://github.com/zlh124/tetris-terminal"
|
|
38
40
|
|
|
39
41
|
[project.scripts]
|
|
40
42
|
tetris = "tetris.cli:main"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import curses
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from tetris import GAME_WINDOW_SIZE_HEIGHT, GAME_WINDOW_SIZE_WIDTH, Tetris
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def wrapper(stdscr: curses.window) -> int:
|
|
8
|
+
if curses.COLS < GAME_WINDOW_SIZE_WIDTH or curses.LINES < GAME_WINDOW_SIZE_HEIGHT:
|
|
9
|
+
return 1
|
|
10
|
+
Tetris(stdscr).main()
|
|
11
|
+
return 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> int:
|
|
15
|
+
if curses.wrapper(wrapper) == 1:
|
|
16
|
+
print(
|
|
17
|
+
f"ensure your terminal has at least {GAME_WINDOW_SIZE_HEIGHT} rows and {GAME_WINDOW_SIZE_WIDTH} columns."
|
|
18
|
+
)
|
|
19
|
+
print("To ensure the game runs smoothly.")
|
|
20
|
+
return 1
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
sys.exit(main())
|
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import curses
|
|
2
|
+
import random
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from collections import defaultdict, deque
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
EMPTY = 0
|
|
10
|
+
|
|
11
|
+
GAME_WINDOW_SIZE_HEIGHT = 22
|
|
12
|
+
GAME_WINDOW_SIZE_WIDTH = 50
|
|
13
|
+
|
|
14
|
+
# keymap
|
|
15
|
+
MOVE_LEFT = [curses.KEY_LEFT, ord("A"), ord("a")]
|
|
16
|
+
MOVE_RIGHT = [curses.KEY_RIGHT, ord("D"), ord("d")]
|
|
17
|
+
SOFT_DROP = [curses.KEY_DOWN, ord("s"), ord("S")]
|
|
18
|
+
ROTATE_CW = [curses.KEY_UP, ord("x"), ord("X"), ord("w"), ord("W")]
|
|
19
|
+
ROTATE_CCW = [ord("z"), ord("Z")]
|
|
20
|
+
HOLD = [ord("c"), ord("C")]
|
|
21
|
+
HARD_DROP = [ord(" ")]
|
|
22
|
+
EXIT = [ord("q"), ord("Q")]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rotate_points(
|
|
26
|
+
points: list[tuple[int, int]],
|
|
27
|
+
center: list[int | tuple[int, int]],
|
|
28
|
+
ccw: bool = False,
|
|
29
|
+
) -> list[tuple[int, int]]:
|
|
30
|
+
"""rotate the point 90 degree"""
|
|
31
|
+
if isinstance(center[0], (list, tuple)):
|
|
32
|
+
cr = (center[0][0] + center[0][1]) / 2.0
|
|
33
|
+
cc = (center[1][0] + center[1][1]) / 2.0 # type: ignore
|
|
34
|
+
else:
|
|
35
|
+
cr, cc = float(center[0]), float(center[1]) # type: ignore
|
|
36
|
+
|
|
37
|
+
rotated_points = []
|
|
38
|
+
|
|
39
|
+
for r, c in points:
|
|
40
|
+
rel_r = r - cr
|
|
41
|
+
rel_c = c - cc
|
|
42
|
+
new_rel_r = -rel_c if ccw else rel_c
|
|
43
|
+
new_rel_c = rel_r if ccw else -rel_r
|
|
44
|
+
new_r = int(new_rel_r + cr)
|
|
45
|
+
new_c = int(new_rel_c + cc)
|
|
46
|
+
|
|
47
|
+
rotated_points.append((new_r, new_c))
|
|
48
|
+
|
|
49
|
+
return rotated_points
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TetriminoShape(Enum):
|
|
53
|
+
Z = 1
|
|
54
|
+
S = 2
|
|
55
|
+
O = 3
|
|
56
|
+
J = 4
|
|
57
|
+
T = 5
|
|
58
|
+
I = 6
|
|
59
|
+
L = 7
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"TetriminoShape.{self.name}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Direction(Enum):
|
|
66
|
+
NORTH = 0
|
|
67
|
+
EAST = 1
|
|
68
|
+
SOUTH = 2
|
|
69
|
+
WEST = 3
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"Direction.{self.name}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
SHAPE_TABLE = {
|
|
76
|
+
TetriminoShape.I: [(0, 0), (0, 1), (0, 2), (0, 3)],
|
|
77
|
+
TetriminoShape.J: [(0, 0), (1, 0), (1, 1), (1, 2)],
|
|
78
|
+
TetriminoShape.L: [(0, 0), (0, 1), (0, 2), (-1, 2)],
|
|
79
|
+
TetriminoShape.O: [(0, 0), (0, 1), (1, 0), (1, 1)],
|
|
80
|
+
TetriminoShape.S: [(0, 0), (0, 1), (-1, 1), (-1, 2)],
|
|
81
|
+
TetriminoShape.T: [(0, 0), (0, 1), (-1, 1), (0, 2)],
|
|
82
|
+
TetriminoShape.Z: [(0, 0), (0, 1), (1, 1), (1, 2)],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ROTATE_AXIS = {
|
|
86
|
+
TetriminoShape.I: [(0, 1), (1, 2)],
|
|
87
|
+
TetriminoShape.J: [1, 1],
|
|
88
|
+
TetriminoShape.L: [0, 1],
|
|
89
|
+
TetriminoShape.O: [(0, 1), (0, 1)],
|
|
90
|
+
TetriminoShape.S: [0, 1],
|
|
91
|
+
TetriminoShape.T: [0, 1],
|
|
92
|
+
TetriminoShape.Z: [1, 1],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
GENERATE_POSITION = {
|
|
96
|
+
TetriminoShape.I: (19, 3),
|
|
97
|
+
TetriminoShape.J: (18, 3),
|
|
98
|
+
TetriminoShape.L: (19, 3),
|
|
99
|
+
TetriminoShape.O: (18, 4),
|
|
100
|
+
TetriminoShape.S: (19, 3),
|
|
101
|
+
TetriminoShape.T: (19, 3),
|
|
102
|
+
TetriminoShape.Z: (18, 3),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# SRS system
|
|
107
|
+
ROTATE_TABLE = defaultdict(lambda: defaultdict(dict))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
JLSTZ_WALL_KICK_OFFSET = {
|
|
111
|
+
(Direction.NORTH, Direction.EAST): [(0, 0), (0, -1), (-1, -1), (2, 0), (2, -1)],
|
|
112
|
+
(Direction.EAST, Direction.NORTH): [(0, 0), (0, 1), (1, 1), (-2, 0), (-2, 1)],
|
|
113
|
+
(Direction.EAST, Direction.SOUTH): [(0, 0), (0, 1), (1, 1), (-2, 0), (-2, 1)],
|
|
114
|
+
(Direction.SOUTH, Direction.EAST): [(0, 0), (0, -1), (-1, -1), (2, 0), (2, -1)],
|
|
115
|
+
(Direction.SOUTH, Direction.WEST): [(0, 0), (0, 1), (-1, 1), (2, 0), (2, 1)],
|
|
116
|
+
(Direction.WEST, Direction.SOUTH): [(0, 0), (0, -1), (1, -1), (-2, 0), (-2, -1)],
|
|
117
|
+
(Direction.WEST, Direction.NORTH): [(0, 0), (0, -1), (1, -1), (-2, 0), (-2, -1)],
|
|
118
|
+
(Direction.NORTH, Direction.WEST): [(0, 0), (0, 1), (-1, 1), (2, 0), (2, 1)],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
O_WALL_KICK_OFFSET = {
|
|
122
|
+
(Direction.NORTH, Direction.EAST): [(0, 0)],
|
|
123
|
+
(Direction.EAST, Direction.NORTH): [(0, 0)],
|
|
124
|
+
(Direction.EAST, Direction.SOUTH): [(0, 0)],
|
|
125
|
+
(Direction.SOUTH, Direction.EAST): [(0, 0)],
|
|
126
|
+
(Direction.SOUTH, Direction.WEST): [(0, 0)],
|
|
127
|
+
(Direction.WEST, Direction.SOUTH): [(0, 0)],
|
|
128
|
+
(Direction.WEST, Direction.NORTH): [(0, 0)],
|
|
129
|
+
(Direction.NORTH, Direction.WEST): [(0, 0)],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
I_WALL_KICK_OFFSET = {
|
|
133
|
+
(Direction.NORTH, Direction.EAST): [(0, 0), (0, -2), (0, 1), (1, -2), (-2, 1)],
|
|
134
|
+
(Direction.EAST, Direction.NORTH): [(0, 0), (0, 2), (0, -1), (-1, 2), (2, -1)],
|
|
135
|
+
(Direction.EAST, Direction.SOUTH): [(0, 0), (0, -1), (0, 2), (-2, -1), (1, 2)],
|
|
136
|
+
(Direction.SOUTH, Direction.EAST): [(0, 0), (0, 1), (0, -2), (2, 1), (-1, -2)],
|
|
137
|
+
(Direction.SOUTH, Direction.WEST): [(0, 0), (0, 2), (0, -1), (-1, 2), (2, -1)],
|
|
138
|
+
(Direction.WEST, Direction.SOUTH): [(0, 0), (0, -2), (0, 1), (1, -2), (-2, 1)],
|
|
139
|
+
(Direction.WEST, Direction.NORTH): [(0, 0), (0, 1), (0, -2), (2, 1), (-1, -2)],
|
|
140
|
+
(Direction.NORTH, Direction.WEST): [(0, 0), (0, -1), (0, 2), (-2, -1), (1, 2)],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# build the ROTATE_TABLE
|
|
144
|
+
for shape in list(TetriminoShape):
|
|
145
|
+
directions = list(Direction)
|
|
146
|
+
_cw = [
|
|
147
|
+
(directions[i], directions[(i + 1) % len(directions)], False)
|
|
148
|
+
for i in range(len(directions))
|
|
149
|
+
]
|
|
150
|
+
_ccw = [
|
|
151
|
+
(
|
|
152
|
+
directions[i],
|
|
153
|
+
directions[(len(directions) + (i - 1)) % len(directions)],
|
|
154
|
+
True,
|
|
155
|
+
)
|
|
156
|
+
for i in range(0, -len(directions), -1)
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
cur_pos = SHAPE_TABLE[shape][::]
|
|
160
|
+
for start, end, ccw in _cw + _ccw:
|
|
161
|
+
rotated = rotate_points(cur_pos, ROTATE_AXIS[shape], ccw)
|
|
162
|
+
diff = [(rx - x, ry - y) for (rx, ry), (x, y) in list(zip(rotated, cur_pos))]
|
|
163
|
+
cur_pos = rotated
|
|
164
|
+
|
|
165
|
+
ROTATE_TABLE[shape][(start, end)]["standard_rotate_diff"] = diff
|
|
166
|
+
|
|
167
|
+
if shape == TetriminoShape.I:
|
|
168
|
+
ROTATE_TABLE[shape][(start, end)]["offsets"] = I_WALL_KICK_OFFSET[
|
|
169
|
+
(start, end)
|
|
170
|
+
]
|
|
171
|
+
elif shape == TetriminoShape.O:
|
|
172
|
+
ROTATE_TABLE[shape][(start, end)]["offsets"] = O_WALL_KICK_OFFSET[
|
|
173
|
+
(start, end)
|
|
174
|
+
]
|
|
175
|
+
else:
|
|
176
|
+
ROTATE_TABLE[shape][(start, end)]["offsets"] = JLSTZ_WALL_KICK_OFFSET[
|
|
177
|
+
(start, end)
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Tetrimino:
|
|
182
|
+
|
|
183
|
+
## line0 0000000000 -
|
|
184
|
+
## ... |> buffer zone
|
|
185
|
+
## line19 0000000000 -
|
|
186
|
+
## line20 0000000000 -
|
|
187
|
+
## ... |> game zone
|
|
188
|
+
## line39 0000000000 -
|
|
189
|
+
## all the tetriminos are generated in the 18th and 19th line(buffer zone)
|
|
190
|
+
|
|
191
|
+
def __init__(self, shape: TetriminoShape) -> None:
|
|
192
|
+
self.shape = shape
|
|
193
|
+
self.no = shape.value
|
|
194
|
+
dx, dy = GENERATE_POSITION[shape]
|
|
195
|
+
self.bodies = [(x + dx, y + dy) for (x, y) in SHAPE_TABLE[shape]]
|
|
196
|
+
self.direction = Direction.NORTH
|
|
197
|
+
|
|
198
|
+
def __iter__(self):
|
|
199
|
+
for x, y in self.bodies:
|
|
200
|
+
yield x, y
|
|
201
|
+
|
|
202
|
+
def __getitem__(self, index: int) -> tuple[int, int]:
|
|
203
|
+
return self.bodies[index]
|
|
204
|
+
|
|
205
|
+
def __setitem__(self, index: int, value: tuple[int, int]) -> None:
|
|
206
|
+
self.bodies[index] = value
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Tetris:
|
|
210
|
+
score = 0
|
|
211
|
+
lines = 0
|
|
212
|
+
level = 1
|
|
213
|
+
|
|
214
|
+
fps = 60 # 1 / 60 s per frame
|
|
215
|
+
tick = 0.001 # calculate tick 1 ms
|
|
216
|
+
|
|
217
|
+
failed = False
|
|
218
|
+
|
|
219
|
+
cur_tetrimino = None
|
|
220
|
+
hold = None
|
|
221
|
+
|
|
222
|
+
frame_timer = 0
|
|
223
|
+
normal_fall_timer = 0
|
|
224
|
+
soft_drop_timer = 0
|
|
225
|
+
|
|
226
|
+
lock_down_timer = 0
|
|
227
|
+
lock_down_rotate_counter = 0
|
|
228
|
+
|
|
229
|
+
hold_once = False
|
|
230
|
+
reach_bottom = False
|
|
231
|
+
lowest = 0
|
|
232
|
+
|
|
233
|
+
board = [[0] * 10 for _ in range(40)]
|
|
234
|
+
bag: deque[Tetrimino] = deque(maxlen=14)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def fall_speed(self) -> float:
|
|
238
|
+
return (0.8 - ((self.level - 1) * 0.007)) ** (self.level - 1)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def soft_drop_speed(self) -> float:
|
|
242
|
+
return self.fall_speed / 20
|
|
243
|
+
|
|
244
|
+
def __init__(self, stdscr: curses.window) -> None:
|
|
245
|
+
self.stdscr = stdscr
|
|
246
|
+
|
|
247
|
+
def replenish_bag(self) -> None:
|
|
248
|
+
"""replenish the bag with 7 random tetriminos"""
|
|
249
|
+
tmp = [Tetrimino(shape) for shape in list(TetriminoShape)]
|
|
250
|
+
random.shuffle(tmp)
|
|
251
|
+
self.bag.extend(tmp)
|
|
252
|
+
|
|
253
|
+
def init_bag(self) -> None:
|
|
254
|
+
"""fill the bag"""
|
|
255
|
+
for _ in range(2):
|
|
256
|
+
self.replenish_bag()
|
|
257
|
+
|
|
258
|
+
def get_tetrimino(self) -> Tetrimino:
|
|
259
|
+
"""get a tetrimino from the bag"""
|
|
260
|
+
tetrimino = self.bag.popleft()
|
|
261
|
+
if len(self.bag) == 7:
|
|
262
|
+
self.replenish_bag()
|
|
263
|
+
# move down one cell immediate
|
|
264
|
+
return tetrimino
|
|
265
|
+
|
|
266
|
+
def get_current_lowest(self) -> int:
|
|
267
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
268
|
+
return max(x for x, _ in self.cur_tetrimino)
|
|
269
|
+
|
|
270
|
+
def generate_new_tetrimino(self) -> None:
|
|
271
|
+
self.cur_tetrimino = self.get_tetrimino()
|
|
272
|
+
if any(self.board[x][y] != EMPTY for x, y in self.cur_tetrimino):
|
|
273
|
+
self.failed = True
|
|
274
|
+
self.do_fall_immediate()
|
|
275
|
+
|
|
276
|
+
def line_clear(self) -> None:
|
|
277
|
+
for row in range(len(self.board) - 1, -1, -1):
|
|
278
|
+
while all(v != EMPTY for v in self.board[row]):
|
|
279
|
+
|
|
280
|
+
self.score += 1
|
|
281
|
+
self.lines += 1
|
|
282
|
+
|
|
283
|
+
for i in range(row - 1, -1, -1):
|
|
284
|
+
self.board[i + 1] = self.board[i]
|
|
285
|
+
self.board[0] = [0] * 10
|
|
286
|
+
|
|
287
|
+
def check_can_move_down(self) -> bool:
|
|
288
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
289
|
+
for x, y in self.cur_tetrimino:
|
|
290
|
+
if x + 1 >= 40:
|
|
291
|
+
return False
|
|
292
|
+
if (x + 1, y) in self.cur_tetrimino:
|
|
293
|
+
continue
|
|
294
|
+
if self.board[x + 1][y] != EMPTY:
|
|
295
|
+
return False
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
def check_can_move_left(self) -> bool:
|
|
299
|
+
if self.lock_down_rotate_counter >= 15:
|
|
300
|
+
return False
|
|
301
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
302
|
+
for x, y in self.cur_tetrimino:
|
|
303
|
+
if y - 1 < 0:
|
|
304
|
+
return False
|
|
305
|
+
if (x, y - 1) in self.cur_tetrimino:
|
|
306
|
+
continue
|
|
307
|
+
if self.board[x][y - 1] != EMPTY:
|
|
308
|
+
return False
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
def check_can_move_right(self) -> bool:
|
|
312
|
+
if self.lock_down_rotate_counter >= 15:
|
|
313
|
+
return False
|
|
314
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
315
|
+
for x, y in self.cur_tetrimino:
|
|
316
|
+
if y + 1 >= 10:
|
|
317
|
+
return False
|
|
318
|
+
if (x, y + 1) in self.cur_tetrimino:
|
|
319
|
+
continue
|
|
320
|
+
if self.board[x][y + 1] != EMPTY:
|
|
321
|
+
return False
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
def do_fall_immediate(self) -> bool:
|
|
325
|
+
if not self.check_can_move_down():
|
|
326
|
+
return False
|
|
327
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
328
|
+
# clean old pos
|
|
329
|
+
for x, y in self.cur_tetrimino:
|
|
330
|
+
self.board[x][y] = EMPTY
|
|
331
|
+
# move down
|
|
332
|
+
for i, (x, y) in enumerate(self.cur_tetrimino):
|
|
333
|
+
self.cur_tetrimino[i] = (x + 1, y)
|
|
334
|
+
# draw new pos
|
|
335
|
+
for x, y in self.cur_tetrimino:
|
|
336
|
+
self.board[x][y] = self.cur_tetrimino.no
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
def do_move_left(self) -> bool:
|
|
340
|
+
if not self.check_can_move_left():
|
|
341
|
+
return False
|
|
342
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
343
|
+
for x, y in self.cur_tetrimino:
|
|
344
|
+
self.board[x][y] = EMPTY
|
|
345
|
+
for i, (x, y) in enumerate(self.cur_tetrimino):
|
|
346
|
+
self.cur_tetrimino[i] = (x, y - 1)
|
|
347
|
+
for x, y in self.cur_tetrimino:
|
|
348
|
+
self.board[x][y] = self.cur_tetrimino.no
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
def do_move_right(self) -> bool:
|
|
352
|
+
if not self.check_can_move_right():
|
|
353
|
+
return False
|
|
354
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
355
|
+
for x, y in self.cur_tetrimino:
|
|
356
|
+
self.board[x][y] = EMPTY
|
|
357
|
+
for i, (x, y) in enumerate(self.cur_tetrimino):
|
|
358
|
+
self.cur_tetrimino[i] = (x, y + 1)
|
|
359
|
+
for x, y in self.cur_tetrimino:
|
|
360
|
+
self.board[x][y] = self.cur_tetrimino.no
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
def check_empty(self, points: list[tuple[int, int]]) -> bool:
|
|
364
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
365
|
+
m, n = len(self.board), len(self.board[0])
|
|
366
|
+
for x, y in points:
|
|
367
|
+
if (x, y) in self.cur_tetrimino.bodies:
|
|
368
|
+
continue
|
|
369
|
+
if not (0 <= x < m and 0 <= y < n) or self.board[x][y] != EMPTY:
|
|
370
|
+
return False
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
def do_rotate(self, cur_direction: Direction, next_direction: Direction):
|
|
374
|
+
if (
|
|
375
|
+
self.lock_down_rotate_counter >= 15
|
|
376
|
+
): # can only rotate 15 times when reach bottom
|
|
377
|
+
return
|
|
378
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
379
|
+
standard_rotate_diff, offsets = ROTATE_TABLE[self.cur_tetrimino.shape][
|
|
380
|
+
(cur_direction), (next_direction)
|
|
381
|
+
].values()
|
|
382
|
+
|
|
383
|
+
rotated = [
|
|
384
|
+
(x + dx, y + dy)
|
|
385
|
+
for (x, y), (dx, dy) in list(
|
|
386
|
+
zip(self.cur_tetrimino.bodies, standard_rotate_diff)
|
|
387
|
+
)
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
for dx, dy in offsets:
|
|
391
|
+
tmp = rotated[::]
|
|
392
|
+
for i, (x, y) in enumerate(rotated):
|
|
393
|
+
tmp[i] = x + dx, y + dy
|
|
394
|
+
|
|
395
|
+
if self.check_empty(tmp):
|
|
396
|
+
for x, y in self.cur_tetrimino.bodies:
|
|
397
|
+
self.board[x][y] = EMPTY
|
|
398
|
+
for x, y in tmp:
|
|
399
|
+
self.board[x][y] = self.cur_tetrimino.shape.value
|
|
400
|
+
self.cur_tetrimino.bodies = tmp
|
|
401
|
+
self.cur_tetrimino.direction = next_direction
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
def do_rotate_cw(self) -> None:
|
|
405
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
406
|
+
cur_direction = self.cur_tetrimino.direction
|
|
407
|
+
directions = list(Direction)
|
|
408
|
+
next_direction = directions[
|
|
409
|
+
(directions.index(cur_direction) + 1) % len(directions)
|
|
410
|
+
]
|
|
411
|
+
self.do_rotate(cur_direction, next_direction)
|
|
412
|
+
|
|
413
|
+
def do_rotate_ccw(self) -> None:
|
|
414
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
415
|
+
cur_direction = self.cur_tetrimino.direction
|
|
416
|
+
directions = list(Direction)
|
|
417
|
+
next_direction = directions[
|
|
418
|
+
(len(directions) + (directions.index(cur_direction) - 1)) % len(directions)
|
|
419
|
+
]
|
|
420
|
+
self.do_rotate(cur_direction, next_direction)
|
|
421
|
+
|
|
422
|
+
def normal_fall(self) -> None:
|
|
423
|
+
self.normal_fall_timer += self.tick
|
|
424
|
+
if self.normal_fall_timer < self.fall_speed:
|
|
425
|
+
return
|
|
426
|
+
self.normal_fall_timer = 0
|
|
427
|
+
if not self.do_fall_immediate():
|
|
428
|
+
self.lowest = self.get_current_lowest()
|
|
429
|
+
self.reach_bottom = True
|
|
430
|
+
|
|
431
|
+
def do_soft_drop(self) -> None:
|
|
432
|
+
# cancel normal fall
|
|
433
|
+
self.normal_fall_timer = 0
|
|
434
|
+
if not self.do_fall_immediate():
|
|
435
|
+
self.lowest = self.get_current_lowest()
|
|
436
|
+
self.reach_bottom = True
|
|
437
|
+
|
|
438
|
+
def do_hard_drop(self) -> None:
|
|
439
|
+
while self.do_fall_immediate():
|
|
440
|
+
pass
|
|
441
|
+
self.lock_down()
|
|
442
|
+
|
|
443
|
+
def do_hold(self) -> None:
|
|
444
|
+
if self.hold_once:
|
|
445
|
+
return
|
|
446
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
447
|
+
for x, y in self.cur_tetrimino:
|
|
448
|
+
self.board[x][y] = EMPTY
|
|
449
|
+
if self.hold is None:
|
|
450
|
+
self.hold = self.cur_tetrimino
|
|
451
|
+
self.generate_new_tetrimino()
|
|
452
|
+
else:
|
|
453
|
+
self.bag.appendleft(Tetrimino(self.hold.shape))
|
|
454
|
+
self.hold = self.cur_tetrimino
|
|
455
|
+
self.generate_new_tetrimino()
|
|
456
|
+
|
|
457
|
+
def draw_board(self) -> None:
|
|
458
|
+
self.frame_timer += self.tick
|
|
459
|
+
if self.frame_timer < 1 / self.fps:
|
|
460
|
+
return
|
|
461
|
+
self.frame_timer = 0
|
|
462
|
+
|
|
463
|
+
# draw border
|
|
464
|
+
self.stdscr.move(0, 0)
|
|
465
|
+
self.stdscr.addstr("┏")
|
|
466
|
+
self.stdscr.move(0, GAME_WINDOW_SIZE_WIDTH - 1)
|
|
467
|
+
self.stdscr.addstr("┓")
|
|
468
|
+
self.stdscr.move(GAME_WINDOW_SIZE_HEIGHT - 1, 0)
|
|
469
|
+
self.stdscr.addstr("┗")
|
|
470
|
+
self.stdscr.move(GAME_WINDOW_SIZE_HEIGHT - 1, GAME_WINDOW_SIZE_WIDTH - 1)
|
|
471
|
+
self.stdscr.addstr("┛")
|
|
472
|
+
|
|
473
|
+
for i in range(1, GAME_WINDOW_SIZE_WIDTH - 1):
|
|
474
|
+
self.stdscr.move(0, i)
|
|
475
|
+
self.stdscr.addstr("━")
|
|
476
|
+
self.stdscr.move(GAME_WINDOW_SIZE_HEIGHT - 1, i)
|
|
477
|
+
self.stdscr.addstr("━")
|
|
478
|
+
for i in range(1, GAME_WINDOW_SIZE_HEIGHT - 1):
|
|
479
|
+
self.stdscr.move(i, 0)
|
|
480
|
+
self.stdscr.addstr("┃")
|
|
481
|
+
self.stdscr.move(i, GAME_WINDOW_SIZE_WIDTH - 1)
|
|
482
|
+
self.stdscr.addstr("┃")
|
|
483
|
+
|
|
484
|
+
self.stdscr.move(0, 21)
|
|
485
|
+
self.stdscr.addstr("┳")
|
|
486
|
+
for i in range(1, 21):
|
|
487
|
+
self.stdscr.move(i, 21)
|
|
488
|
+
self.stdscr.addstr("┃")
|
|
489
|
+
self.stdscr.move(GAME_WINDOW_SIZE_HEIGHT - 1, 21)
|
|
490
|
+
self.stdscr.addstr("┻")
|
|
491
|
+
|
|
492
|
+
# title
|
|
493
|
+
self.stdscr.move(3, 28)
|
|
494
|
+
self.stdscr.addstr("━┳━┏━━━┳━┏━┓┳┏━╸")
|
|
495
|
+
self.stdscr.move(4, 28)
|
|
496
|
+
self.stdscr.addstr(" ┃ ┣━━ ┃ ┣┳┛┃┗━┓")
|
|
497
|
+
self.stdscr.move(5, 28)
|
|
498
|
+
self.stdscr.addstr(" ╹ ┗━━ ╹ ╹┗━┻━━┛")
|
|
499
|
+
|
|
500
|
+
# game info
|
|
501
|
+
self.stdscr.move(9, 27)
|
|
502
|
+
self.stdscr.addstr("Next : ")
|
|
503
|
+
for i in range(5):
|
|
504
|
+
self.stdscr.addstr(f"{self.bag[i].shape.name} ")
|
|
505
|
+
|
|
506
|
+
self.stdscr.move(11, 27)
|
|
507
|
+
self.stdscr.addstr(f"Score : {self.score}")
|
|
508
|
+
self.stdscr.move(13, 27)
|
|
509
|
+
self.stdscr.addstr(f"Lines : {self.lines}")
|
|
510
|
+
self.stdscr.move(15, 27)
|
|
511
|
+
self.stdscr.addstr(f"Level : {self.level}")
|
|
512
|
+
self.stdscr.move(17, 27)
|
|
513
|
+
self.stdscr.addstr(f"Hold : {self.hold.shape.name if self.hold else ""}")
|
|
514
|
+
# board
|
|
515
|
+
for i in range(20, 40):
|
|
516
|
+
self.stdscr.move(i - 19, 1)
|
|
517
|
+
for j in range(10):
|
|
518
|
+
self.stdscr.addstr(" ", curses.color_pair(self.board[i][j]))
|
|
519
|
+
|
|
520
|
+
self.stdscr.refresh()
|
|
521
|
+
|
|
522
|
+
def handle_input(self) -> None:
|
|
523
|
+
"""handle the input
|
|
524
|
+
Terminal input relies on the operating system's control
|
|
525
|
+
over the rate at which keyboard characters are entered.
|
|
526
|
+
it't hard to ctrl the long press and normal press
|
|
527
|
+
"""
|
|
528
|
+
c = self.stdscr.getch()
|
|
529
|
+
if c in EXIT:
|
|
530
|
+
self.failed = True
|
|
531
|
+
if c in MOVE_LEFT:
|
|
532
|
+
self.do_move_left()
|
|
533
|
+
if c in MOVE_RIGHT:
|
|
534
|
+
self.do_move_right()
|
|
535
|
+
if c in SOFT_DROP:
|
|
536
|
+
self.do_soft_drop()
|
|
537
|
+
if c in ROTATE_CW:
|
|
538
|
+
self.do_rotate_cw()
|
|
539
|
+
if c in ROTATE_CCW:
|
|
540
|
+
self.do_rotate_ccw()
|
|
541
|
+
if c in HARD_DROP:
|
|
542
|
+
self.do_hard_drop()
|
|
543
|
+
if c in HOLD:
|
|
544
|
+
self.do_hold()
|
|
545
|
+
|
|
546
|
+
def lock_down(self) -> None:
|
|
547
|
+
assert self.cur_tetrimino is not None, "cur_tetrimino is None"
|
|
548
|
+
# all cells in buff zone when lock down
|
|
549
|
+
if all(x < 20 for x, _ in self.cur_tetrimino):
|
|
550
|
+
self.failed = True
|
|
551
|
+
|
|
552
|
+
self.line_clear()
|
|
553
|
+
|
|
554
|
+
if self.lines >= self.level * (self.level + 1) / 2 * 10:
|
|
555
|
+
self.level += 1
|
|
556
|
+
|
|
557
|
+
self.generate_new_tetrimino()
|
|
558
|
+
|
|
559
|
+
self.reach_bottom = False
|
|
560
|
+
self.lock_down_timer = 0
|
|
561
|
+
self.lock_down_rotate_counter = 0
|
|
562
|
+
|
|
563
|
+
def handle_lock_down(self) -> None:
|
|
564
|
+
if not self.reach_bottom:
|
|
565
|
+
return
|
|
566
|
+
if self.lock_down_timer >= 0.5:
|
|
567
|
+
self.lock_down()
|
|
568
|
+
return
|
|
569
|
+
# no longer move down and has cells below, continue timer
|
|
570
|
+
if self.get_current_lowest() == self.lowest and not self.check_can_move_down():
|
|
571
|
+
self.lock_down_timer += self.tick
|
|
572
|
+
# reach new lowest, reset timer and counter
|
|
573
|
+
elif self.get_current_lowest() > self.lowest:
|
|
574
|
+
self.reach_button = False
|
|
575
|
+
self.lock_down_timer = 0
|
|
576
|
+
self.lock_down_rotate_counter = 0
|
|
577
|
+
|
|
578
|
+
def game_loop(self) -> None:
|
|
579
|
+
while not self.failed:
|
|
580
|
+
self.normal_fall()
|
|
581
|
+
self.draw_board()
|
|
582
|
+
self.handle_input()
|
|
583
|
+
self.handle_lock_down()
|
|
584
|
+
time.sleep(self.tick)
|
|
585
|
+
|
|
586
|
+
def init_color(self) -> None:
|
|
587
|
+
if curses.can_change_color():
|
|
588
|
+
curses.init_color(TetriminoShape.I.value, 0, 941, 941)
|
|
589
|
+
curses.init_color(TetriminoShape.O.value, 941, 941, 0)
|
|
590
|
+
curses.init_color(TetriminoShape.T.value, 627, 0, 941)
|
|
591
|
+
curses.init_color(TetriminoShape.L.value, 941, 627, 0)
|
|
592
|
+
curses.init_color(TetriminoShape.J.value, 0, 0, 941)
|
|
593
|
+
curses.init_color(TetriminoShape.S.value, 0, 941, 0)
|
|
594
|
+
curses.init_color(TetriminoShape.Z.value, 941, 0, 0)
|
|
595
|
+
curses.use_default_colors()
|
|
596
|
+
for tetrimino in list(TetriminoShape):
|
|
597
|
+
curses.init_pair(tetrimino.value, tetrimino.value, tetrimino.value)
|
|
598
|
+
|
|
599
|
+
def init_game(self) -> None:
|
|
600
|
+
self.init_bag()
|
|
601
|
+
self.generate_new_tetrimino()
|
|
602
|
+
self.init_color()
|
|
603
|
+
|
|
604
|
+
curses.curs_set(0)
|
|
605
|
+
self.stdscr.timeout(0)
|
|
606
|
+
|
|
607
|
+
def main(self) -> None:
|
|
608
|
+
self.init_game()
|
|
609
|
+
self.game_loop()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tetris-terminal
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2a1
|
|
4
4
|
Summary: A tetris game runs in the terminal
|
|
5
5
|
Author-email: jayzhu <jay.l.zhu@foxmail.com>
|
|
6
|
-
Project-URL: homepage, https://github.com/zlh124/
|
|
6
|
+
Project-URL: homepage, https://github.com/zlh124/tetris-terminal
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Environment :: Console :: Curses
|
|
9
9
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Terminals
|
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
+
Requires-Dist: windows-curses; sys_platform == "win32"
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|

|
|
@@ -33,14 +34,18 @@ A terminal-based Tetris game written in Python using the `curses` library.
|
|
|
33
34
|
[]()
|
|
34
35
|
|
|
35
36
|
### Features
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- Next
|
|
37
|
+
- Modern Tetris design following the [Tetris Design Guideline](https://dn720004.ca.archive.org/0/items/2009-tetris-variant-concepts_202201/2009%20Tetris%20Design%20Guideline.pdf)
|
|
38
|
+
- [x] Extended Placement
|
|
39
|
+
- [x] Next Piece Preview
|
|
40
|
+
- [x] SRS System
|
|
41
|
+
- [x] Piece Holding
|
|
42
|
+
- [ ] Scoring System
|
|
43
|
+
- [ ] Level System
|
|
39
44
|
|
|
40
45
|
### Platform Support
|
|
41
46
|
Based on Python's [`curses`](https://docs.python.org/3/library/curses.html) module:
|
|
42
47
|
- ✅ **Linux/macOS**: Works out of the box
|
|
43
|
-
-
|
|
48
|
+
- ✅️ **Windows**: With [`windows-curses`](https://github.com/zephyrproject-rtos/windows-curses)
|
|
44
49
|
|
|
45
50
|
### Installation & Usage
|
|
46
51
|
```bash
|
|
@@ -49,16 +54,19 @@ tetris
|
|
|
49
54
|
```
|
|
50
55
|
|
|
51
56
|
### Controls
|
|
52
|
-
| Key
|
|
53
|
-
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
| `
|
|
58
|
-
|
|
|
57
|
+
| Key | Action |
|
|
58
|
+
|------------|------------|
|
|
59
|
+
| `a`, `←` | Move left |
|
|
60
|
+
| `d`, `→` | Move right |
|
|
61
|
+
|`w`, `↑`,`x`| Rotate cw |
|
|
62
|
+
| `z` | Rotate ccw |
|
|
63
|
+
| `s`, `↓` | Soft drop |
|
|
64
|
+
| `space` | Hard drop |
|
|
65
|
+
| `c` | Hold |
|
|
66
|
+
| `q` | Quit game |
|
|
59
67
|
|
|
60
68
|
### License
|
|
61
69
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
62
70
|
|
|
63
71
|
### Acknowledgements
|
|
64
|
-
|
|
72
|
+
Idea from [tinytetris](https://github.com/taylorconor/tinytetris) (a C implementation).
|
{tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/SOURCES.txt
RENAMED
|
@@ -8,4 +8,5 @@ src/tetris_terminal.egg-info/PKG-INFO
|
|
|
8
8
|
src/tetris_terminal.egg-info/SOURCES.txt
|
|
9
9
|
src/tetris_terminal.egg-info/dependency_links.txt
|
|
10
10
|
src/tetris_terminal.egg-info/entry_points.txt
|
|
11
|
+
src/tetris_terminal.egg-info/requires.txt
|
|
11
12
|
src/tetris_terminal.egg-info/top_level.txt
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-

|
|
2
|
-
# Tetris Terminal🎮
|
|
3
|
-
A terminal-based Tetris game written in Python using the `curses` library.
|
|
4
|
-
|
|
5
|
-
[](LICENSE)
|
|
6
|
-
[]()
|
|
7
|
-
|
|
8
|
-
### Features
|
|
9
|
-
- Classic Tetris gameplay with 7 standard tetrominoes
|
|
10
|
-
- Real-time score
|
|
11
|
-
- Next piece preview
|
|
12
|
-
|
|
13
|
-
### Platform Support
|
|
14
|
-
Based on Python's [`curses`](https://docs.python.org/3/library/curses.html) module:
|
|
15
|
-
- ✅ **Linux/macOS**: Works out of the box
|
|
16
|
-
- ⚠️ **Windows**: Not supported yet
|
|
17
|
-
|
|
18
|
-
### Installation & Usage
|
|
19
|
-
```bash
|
|
20
|
-
pip install tetris-terminal
|
|
21
|
-
tetris
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Controls
|
|
25
|
-
| Key | Action |
|
|
26
|
-
|-----------|-----------------|
|
|
27
|
-
| `a` | Move left |
|
|
28
|
-
| `d` | Move right |
|
|
29
|
-
| `w` | Rotate piece |
|
|
30
|
-
| `s` | Hard drop |
|
|
31
|
-
| `q` | Quit game |
|
|
32
|
-
|
|
33
|
-
### License
|
|
34
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
35
|
-
|
|
36
|
-
### Acknowledgements
|
|
37
|
-
Game logic adapted from [tinytetris](https://github.com/taylorconor/tinytetris) (a C implementation).
|
|
File without changes
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import curses
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
from .tetris import Tetris
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def wrapper(stdscr: curses.window):
|
|
8
|
-
tetris = Tetris(stdscr)
|
|
9
|
-
tetris.main()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main() -> int:
|
|
13
|
-
curses.wrapper(wrapper)
|
|
14
|
-
return 0
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if __name__ == "__main__":
|
|
18
|
-
sys.exit(main())
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/python3
|
|
2
|
-
import curses
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from collections import deque
|
|
6
|
-
from copy import copy
|
|
7
|
-
from random import randint, shuffle
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Tetris:
|
|
11
|
-
x = 431424
|
|
12
|
-
y = 598356
|
|
13
|
-
r = 427089
|
|
14
|
-
c = 348480
|
|
15
|
-
p = 615696
|
|
16
|
-
|
|
17
|
-
px = 247872
|
|
18
|
-
py = 799248
|
|
19
|
-
pr = 0
|
|
20
|
-
|
|
21
|
-
tick = 0
|
|
22
|
-
|
|
23
|
-
board = [[0] * 10 for _ in range(20)]
|
|
24
|
-
|
|
25
|
-
piece_chars = ["Z", "S", "O", "J", "T", "I", "L"]
|
|
26
|
-
|
|
27
|
-
block = [
|
|
28
|
-
[x, y, x, y],
|
|
29
|
-
[r, p, r, p],
|
|
30
|
-
[c, c, c, c],
|
|
31
|
-
[599636, 431376, 598336, 432192],
|
|
32
|
-
[411985, 610832, 415808, 595540],
|
|
33
|
-
[px, py, px, py],
|
|
34
|
-
[614928, 399424, 615744, 428369],
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
def __init__(self, stdscr: curses.window):
|
|
38
|
-
self.stdscr = stdscr
|
|
39
|
-
|
|
40
|
-
queue = deque(maxlen=14)
|
|
41
|
-
|
|
42
|
-
score = 0
|
|
43
|
-
lock_until = 0
|
|
44
|
-
|
|
45
|
-
def now_ms(self) -> int:
|
|
46
|
-
return int(time.time() * 1000)
|
|
47
|
-
|
|
48
|
-
def reset_lock_delay(self) -> None:
|
|
49
|
-
self.lock_until = 0
|
|
50
|
-
|
|
51
|
-
def start_lock_delay(self) -> None:
|
|
52
|
-
if not self.lock_until:
|
|
53
|
-
self.lock_until = self.now_ms() + 500
|
|
54
|
-
|
|
55
|
-
def NUM(self, x: int, y: int) -> int:
|
|
56
|
-
return 3 & self.block[self.p][x] >> y
|
|
57
|
-
|
|
58
|
-
def fill_bag(self) -> None:
|
|
59
|
-
bag = list(range(7))
|
|
60
|
-
shuffle(bag)
|
|
61
|
-
while bag:
|
|
62
|
-
self.queue.append(bag.pop())
|
|
63
|
-
|
|
64
|
-
def init_queue(self) -> None:
|
|
65
|
-
self.fill_bag()
|
|
66
|
-
self.fill_bag()
|
|
67
|
-
|
|
68
|
-
def next_from_queue(self) -> int:
|
|
69
|
-
if len(self.queue) == 7:
|
|
70
|
-
self.fill_bag()
|
|
71
|
-
return self.queue.popleft()
|
|
72
|
-
|
|
73
|
-
def new_piece(self) -> None:
|
|
74
|
-
self.y = self.py = 0
|
|
75
|
-
self.p = self.next_from_queue()
|
|
76
|
-
self.r = self.pr = randint(0, 3)
|
|
77
|
-
self.x = self.px = randint(0, 9 - self.NUM(self.r, 16))
|
|
78
|
-
self.reset_lock_delay()
|
|
79
|
-
|
|
80
|
-
def frame(self, stdscr: curses.window) -> None:
|
|
81
|
-
stdscr.move(0, 3)
|
|
82
|
-
stdscr.addstr("Next: ")
|
|
83
|
-
for i in range(5):
|
|
84
|
-
stdscr.addstr(f"{self.piece_chars[self.queue[i]]} ")
|
|
85
|
-
|
|
86
|
-
for i in range(20):
|
|
87
|
-
stdscr.move(i + 1, 1)
|
|
88
|
-
for j in range(10):
|
|
89
|
-
if self.board[i][j]:
|
|
90
|
-
stdscr.attron(262176 | self.board[i][j] << 8)
|
|
91
|
-
stdscr.addstr(" ")
|
|
92
|
-
stdscr.attroff(262176 | self.board[i][j] << 8)
|
|
93
|
-
stdscr.move(21, 1)
|
|
94
|
-
stdscr.addstr(f"Score: {self.score}")
|
|
95
|
-
stdscr.refresh()
|
|
96
|
-
|
|
97
|
-
def set_piece(self, x: int, y: int, r: int, v: int) -> None:
|
|
98
|
-
for i in range(0, 8, 2):
|
|
99
|
-
self.board[self.NUM(r, i * 2) + y][self.NUM(r, (i * 2) + 2) + x] = v
|
|
100
|
-
|
|
101
|
-
def update_piece(self) -> None:
|
|
102
|
-
self.set_piece(self.px, self.py, self.pr, 0)
|
|
103
|
-
self.px, self.py, self.pr = self.x, self.y, self.r
|
|
104
|
-
self.set_piece(self.x, self.y, self.r, self.p + 1)
|
|
105
|
-
|
|
106
|
-
def remove_line(self) -> None:
|
|
107
|
-
for row in range(self.y, self.y + self.NUM(self.r, 18) + 1):
|
|
108
|
-
self.c = 1
|
|
109
|
-
for i in range(10):
|
|
110
|
-
self.c *= self.board[row][i]
|
|
111
|
-
if not self.c:
|
|
112
|
-
continue
|
|
113
|
-
for i in range(row - 1, 0, -1):
|
|
114
|
-
self.board[i + 1] = copy(self.board[i])
|
|
115
|
-
self.board[0] = [0] * 10
|
|
116
|
-
self.score += 1
|
|
117
|
-
|
|
118
|
-
def check_hit(self, x: int, y: int, r: int) -> int:
|
|
119
|
-
if y + self.NUM(r, 18) > 19:
|
|
120
|
-
return 1
|
|
121
|
-
self.set_piece(self.px, self.py, self.pr, 0)
|
|
122
|
-
self.c = 0
|
|
123
|
-
for i in range(0, 8, 2):
|
|
124
|
-
if self.board[y + self.NUM(r, i * 2)][x + self.NUM(r, (i * 2) + 2)]:
|
|
125
|
-
self.c += 1
|
|
126
|
-
self.set_piece(self.px, self.py, self.pr, self.p + 1)
|
|
127
|
-
return self.c
|
|
128
|
-
|
|
129
|
-
def do_tick(self) -> int:
|
|
130
|
-
self.tick += 1
|
|
131
|
-
if self.tick > 30:
|
|
132
|
-
self.tick = 0
|
|
133
|
-
if not self.check_hit(self.x, self.y + 1, self.r):
|
|
134
|
-
self.y += 1
|
|
135
|
-
self.update_piece()
|
|
136
|
-
self.reset_lock_delay()
|
|
137
|
-
else:
|
|
138
|
-
if not self.y:
|
|
139
|
-
return 0
|
|
140
|
-
self.start_lock_delay()
|
|
141
|
-
if self.now_ms() >= self.lock_until:
|
|
142
|
-
self.remove_line()
|
|
143
|
-
self.new_piece()
|
|
144
|
-
if self.lock_until and self.now_ms() >= self.lock_until:
|
|
145
|
-
if self.check_hit(self.x, self.y + 1, self.r):
|
|
146
|
-
self.remove_line()
|
|
147
|
-
self.new_piece()
|
|
148
|
-
else:
|
|
149
|
-
self.reset_lock_delay()
|
|
150
|
-
return 1
|
|
151
|
-
|
|
152
|
-
def runloop(self) -> None:
|
|
153
|
-
while self.do_tick():
|
|
154
|
-
time.sleep(0.01)
|
|
155
|
-
c = self.stdscr.getch()
|
|
156
|
-
if (
|
|
157
|
-
c == ord("a")
|
|
158
|
-
and self.x > 0
|
|
159
|
-
and not self.check_hit(self.x - 1, self.y, self.r)
|
|
160
|
-
):
|
|
161
|
-
self.x -= 1
|
|
162
|
-
if (
|
|
163
|
-
c == ord("d")
|
|
164
|
-
and self.x + self.NUM(self.r, 16) < 9
|
|
165
|
-
and not self.check_hit(self.x + 1, self.y, self.r)
|
|
166
|
-
):
|
|
167
|
-
self.x += 1
|
|
168
|
-
if c == ord("s"):
|
|
169
|
-
while not self.check_hit(self.x, self.y + 1, self.r):
|
|
170
|
-
self.y += 1
|
|
171
|
-
self.update_piece()
|
|
172
|
-
self.reset_lock_delay()
|
|
173
|
-
self.remove_line()
|
|
174
|
-
self.new_piece()
|
|
175
|
-
if c == ord("w"):
|
|
176
|
-
self.r += 1
|
|
177
|
-
self.r %= 4
|
|
178
|
-
while self.x + self.NUM(self.r, 16) > 9:
|
|
179
|
-
self.x -= 1
|
|
180
|
-
if self.check_hit(self.x, self.y, self.r):
|
|
181
|
-
self.x = self.px
|
|
182
|
-
self.r = self.pr
|
|
183
|
-
if c == ord("q"):
|
|
184
|
-
return
|
|
185
|
-
self.update_piece()
|
|
186
|
-
self.frame(self.stdscr)
|
|
187
|
-
|
|
188
|
-
def main(self) -> None:
|
|
189
|
-
curses.start_color()
|
|
190
|
-
self.init_queue()
|
|
191
|
-
for i in range(1, 8):
|
|
192
|
-
curses.init_pair(i, i, 0)
|
|
193
|
-
self.new_piece()
|
|
194
|
-
curses.resizeterm(22, 22)
|
|
195
|
-
curses.noecho()
|
|
196
|
-
curses.curs_set(0)
|
|
197
|
-
self.stdscr.timeout(0)
|
|
198
|
-
self.stdscr.box()
|
|
199
|
-
self.runloop()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{tetris_terminal-0.0.1a3 → tetris_terminal-0.0.2a1}/src/tetris_terminal.egg-info/top_level.txt
RENAMED
|
File without changes
|