mulgogi 0.1.0__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.
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
mulgogi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: mulgogi
3
+ Version: 0.1.0
4
+ Summary: A fishing game in your terminal
5
+ Project-URL: Homepage, https://github.com/justart-dev/mulgogi
6
+ Project-URL: Repository, https://github.com/justart-dev/mulgogi
7
+ Project-URL: Issues, https://github.com/justart-dev/mulgogi/issues
8
+ Author-email: justart-dev <justart-dev@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ Keywords: cli,fishing,game,terminal,tui
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Games/Entertainment
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: textual>=0.70.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # mulgogi 🎣
26
+
27
+ A fishing game in your terminal.
28
+
29
+ ```bash
30
+ pip install mulgogi
31
+ mulgogi
32
+ ```
33
+
34
+ ## Controls
35
+
36
+ - `←` `→`: adjust rod angle (cosmetic for now)
37
+ - `Space`: cast / hook / reel
38
+ - `q`: quit
39
+
40
+ ## Development
41
+
42
+ ```bash
43
+ git clone https://github.com/justart-dev/mulgogi.git
44
+ cd mulgogi
45
+ python -m venv .venv
46
+ source .venv/bin/activate
47
+ pip install -e .
48
+ mulgogi
49
+ ```
50
+
51
+ ## Homebrew (macOS/Linux)
52
+
53
+ ```bash
54
+ brew tap justart-dev/mulgogi
55
+ brew install mulgogi
56
+ mulgogi
57
+ ```
@@ -0,0 +1,33 @@
1
+ # mulgogi 🎣
2
+
3
+ A fishing game in your terminal.
4
+
5
+ ```bash
6
+ pip install mulgogi
7
+ mulgogi
8
+ ```
9
+
10
+ ## Controls
11
+
12
+ - `←` `→`: adjust rod angle (cosmetic for now)
13
+ - `Space`: cast / hook / reel
14
+ - `q`: quit
15
+
16
+ ## Development
17
+
18
+ ```bash
19
+ git clone https://github.com/justart-dev/mulgogi.git
20
+ cd mulgogi
21
+ python -m venv .venv
22
+ source .venv/bin/activate
23
+ pip install -e .
24
+ mulgogi
25
+ ```
26
+
27
+ ## Homebrew (macOS/Linux)
28
+
29
+ ```bash
30
+ brew tap justart-dev/mulgogi
31
+ brew install mulgogi
32
+ mulgogi
33
+ ```
File without changes
@@ -0,0 +1,3 @@
1
+ from mulgogi.main import main
2
+
3
+ main()
@@ -0,0 +1,289 @@
1
+ import asyncio
2
+ import random
3
+ import time
4
+ import argparse
5
+ from dataclasses import dataclass
6
+
7
+ from textual.app import App, ComposeResult
8
+ from textual.containers import Center, Middle, Vertical
9
+ from textual.reactive import reactive
10
+ from textual.widgets import Footer, Header, Static
11
+
12
+
13
+ @dataclass
14
+ class Fish:
15
+ name: str
16
+ ascii: str
17
+ min_weight: float
18
+ max_weight: float
19
+ difficulty: int # 1-5
20
+
21
+
22
+ FISH_POOL = [
23
+ Fish("붕어", " <\\><(_)>\n<))))><", 0.3, 1.2, 1),
24
+ Fish("잉어", " __\n__/ o\\__\n \____/", 1.0, 4.0, 2),
25
+ Fish("송어", " _ __\n>(___(_)>\n ~~~~~~~", 0.8, 2.5, 2),
26
+ Fish("메기", " __\n ( \\___\n \_____)>", 1.5, 6.0, 3),
27
+ Fish("황금잉어", " \*/\n*<(o o)>*\n /| |\\", 2.0, 5.0, 4),
28
+ ]
29
+
30
+
31
+ class SplashWidget(Static):
32
+ """물결 애니메이션 위젯"""
33
+
34
+ frame = reactive(0)
35
+
36
+ def __init__(self, **kwargs):
37
+ super().__init__("", **kwargs)
38
+ self._timer = None
39
+ self._running = False
40
+
41
+ def on_mount(self):
42
+ self._running = True
43
+ self._timer = self.set_interval(0.15, self._tick)
44
+
45
+ def _tick(self):
46
+ if not self._running:
47
+ return
48
+ self.frame += 1
49
+ waves = ["~", "-", "≈", "∿", "~"]
50
+ lines = []
51
+ for row in range(5):
52
+ line = ""
53
+ for col in range(40):
54
+ idx = (self.frame + row * 3 + col) % len(waves)
55
+ line += waves[idx]
56
+ lines.append(line)
57
+ self.update("\n".join(lines))
58
+
59
+ def stop(self):
60
+ self._running = False
61
+ if self._timer:
62
+ self._timer.stop()
63
+
64
+
65
+ class AngleWidget(Static):
66
+ """낚싯대 각도 표시 (장식용)"""
67
+
68
+ angle = reactive(45)
69
+
70
+ def render(self):
71
+ rad = self.angle * 3.14159 / 180
72
+ length = 10
73
+ dx = int(round(length * 0.7))
74
+ dy = int(round(length * 0.7))
75
+ # 각도를 라인으로 그리기
76
+ x, y = 15, 5
77
+ end_x = x + int(round(length * 0.7))
78
+ end_y = y - int(round(length * 0.7))
79
+ grid = [[" " for _ in range(40)] for _ in range(8)]
80
+ # 대각선
81
+ for i in range(length):
82
+ px = x + int(round(i * 0.7))
83
+ py = y - int(round(i * 0.7))
84
+ if 0 <= px < 40 and 0 <= py < 8:
85
+ grid[py][px] = "/" if i < length - 1 else "@"
86
+ # 물고기 잡는 사람
87
+ grid[5][12] = "O"
88
+ lines = ["".join(row) for row in grid]
89
+ lines_joined = "\n".join(lines)
90
+ return f"{lines_joined}\n\n각도: {self.angle}° (장식용)"
91
+
92
+
93
+ class ReelWidget(Static):
94
+ """릴 미니게임 게이지"""
95
+
96
+ position = reactive(0.0)
97
+ direction = reactive(1)
98
+
99
+ def render(self):
100
+ bar_width = 30
101
+ pos = int(self.position * bar_width)
102
+ pos = max(0, min(bar_width - 1, pos))
103
+ bar = ["-"] * bar_width
104
+ bar[pos] = "O"
105
+ return f"타이밍! Space를 눌러 멈추세요\n[{''.join(bar)}]"
106
+
107
+
108
+ class MulgogiApp(App):
109
+ """mulgogi 메인 앱"""
110
+
111
+ CSS = """
112
+ Screen { align: center middle; }
113
+ #main { width: 80; height: auto; border: solid green; padding: 1 2; }
114
+ Static { text-align: center; }
115
+ """
116
+
117
+ BINDINGS = [
118
+ ("q", "quit", "종료"),
119
+ ]
120
+
121
+ def __init__(self):
122
+ super().__init__()
123
+ self.state = "menu"
124
+ self.angle = 45
125
+ self.bite_timer = None
126
+ self.reel_timer = None
127
+ self.reel_pos = 0.0
128
+ self.reel_dir = 1
129
+ self.reel_speed = 0.06
130
+ self.catch_result = None
131
+
132
+ def compose(self) -> ComposeResult:
133
+ yield Header(show_clock=True)
134
+ with Vertical(id="main"):
135
+ yield Static("mulgogi 🎣", id="title")
136
+ yield Static("방향키로 각도 조절, Space로 캐스트\nq를 누륵 종료", id="info")
137
+ yield AngleWidget(id="angle")
138
+ yield SplashWidget(id="splash")
139
+ yield ReelWidget(id="reel")
140
+ yield Static("", id="result")
141
+ yield Footer()
142
+
143
+ def on_mount(self):
144
+ self.query_one("#splash", SplashWidget).display = False
145
+ self.query_one("#reel", ReelWidget).display = False
146
+ self.query_one("#result", Static).update(
147
+ "[b]mulgogi[/b]\n터미널에서 즐기는 낚시 게임\n\n"
148
+ "[green]Space[/green]: 캐스트 / 입질 / 릴\n"
149
+ "[green]← →[/green]: 각도 조절 (장식용)\n"
150
+ "[green]q[/green]: 종료"
151
+ )
152
+
153
+ def on_key(self, event):
154
+ key = event.key
155
+
156
+ if key == "q":
157
+ self.exit()
158
+ return
159
+
160
+ if self.state == "menu":
161
+ if key == "left":
162
+ self.angle = max(10, self.angle - 5)
163
+ self.query_one("#angle", AngleWidget).angle = self.angle
164
+ elif key == "right":
165
+ self.angle = min(80, self.angle + 5)
166
+ self.query_one("#angle", AngleWidget).angle = self.angle
167
+ elif key == "space":
168
+ self.start_cast()
169
+
170
+ elif self.state == "waiting":
171
+ if key == "space":
172
+ # 입질 전에는 무시하거나 패널티
173
+ pass
174
+
175
+ elif self.state == "biting":
176
+ if key == "space":
177
+ self.start_reel()
178
+
179
+ elif self.state == "reeling":
180
+ if key == "space":
181
+ self.stop_reel()
182
+
183
+ elif self.state == "result":
184
+ if key == "space":
185
+ self.reset_to_menu()
186
+
187
+ def start_cast(self):
188
+ self.state = "waiting"
189
+ self.query_one("#info", Static).update("물고기가 물기를 기다리는 중...")
190
+ self.query_one("#angle", AngleWidget).display = False
191
+ self.query_one("#splash", SplashWidget).display = True
192
+ self.query_one("#result", Static).update("")
193
+ # 랜덤 시간 후 입질
194
+ delay = random.uniform(2.0, 5.0)
195
+ self.bite_timer = self.set_timer(delay, self.trigger_bite)
196
+
197
+ def trigger_bite(self):
198
+ if self.state != "waiting":
199
+ return
200
+ self.state = "biting"
201
+ self.query_one("#info", Static).update("[bold red]!! 입질 !!\nSpace를 눌러 릴을 걸어라![/]")
202
+ # 1.5초 안에 못 누른 놓치기
203
+ self.bite_timer = self.set_timer(1.5, self.miss_bite)
204
+
205
+ def miss_bite(self):
206
+ if self.state != "biting":
207
+ return
208
+ self.state = "result"
209
+ self.query_one("#splash", SplashWidget).stop()
210
+ self.query_one("#splash", SplashWidget).display = False
211
+ self.query_one("#info", Static).update("입질을 놓쳤다...")
212
+ self.query_one("#result", Static).update("미끼만 날아갔습니다.\nSpace를 눌러 계속")
213
+
214
+ def start_reel(self):
215
+ if self.bite_timer:
216
+ self.bite_timer.stop()
217
+ self.state = "reeling"
218
+ self.query_one("#info", Static).update("[bold cyan]릴을 감는 중... Space로 멈춰![/]")
219
+ self.reel_pos = 0.0
220
+ self.reel_dir = 1
221
+ self.reel_speed = random.uniform(0.04, 0.10)
222
+ self.reel_timer = self.set_interval(0.05, self.update_reel)
223
+
224
+ def update_reel(self):
225
+ self.reel_pos += self.reel_dir * self.reel_speed
226
+ if self.reel_pos >= 1.0:
227
+ self.reel_pos = 1.0
228
+ self.reel_dir = -1
229
+ elif self.reel_pos <= 0.0:
230
+ self.reel_pos = 0.0
231
+ self.reel_dir = 1
232
+ widget = self.query_one("#reel", ReelWidget)
233
+ widget.position = self.reel_pos
234
+ widget.display = True
235
+
236
+ def stop_reel(self):
237
+ if self.reel_timer:
238
+ self.reel_timer.stop()
239
+ self.query_one("#reel", ReelWidget).display = False
240
+ self.query_one("#splash", SplashWidget).stop()
241
+ self.query_one("#splash", SplashWidget).display = False
242
+
243
+ # 중앙 근처(0.4~0.6)면 성공
244
+ if 0.4 <= self.reel_pos <= 0.6:
245
+ self.catch_fish()
246
+ else:
247
+ self.lose_fish()
248
+
249
+ def catch_fish(self):
250
+ self.state = "result"
251
+ fish = random.choice(FISH_POOL)
252
+ weight = round(random.uniform(fish.min_weight, fish.max_weight), 2)
253
+ self.query_one("#info", Static).update(f"[bold green]{fish.name}을(를) 잡았다![/]")
254
+ self.query_one("#result", Static).update(
255
+ f"{fish.ascii}\n\n"
256
+ f"무게: {weight}kg\n"
257
+ f"난이도: {'★' * fish.difficulty}{'☆' * (5 - fish.difficulty)}\n\n"
258
+ f"Space를 눌러 계속"
259
+ )
260
+
261
+ def lose_fish(self):
262
+ self.state = "result"
263
+ self.query_one("#info", Static).update("[bold red]물고기가 도망갔다...[/]")
264
+ self.query_one("#result", Static).update("타이밍이 안 맞았습니다.\nSpace를 눌러 계속")
265
+
266
+ def reset_to_menu(self):
267
+ self.state = "menu"
268
+ self.query_one("#angle", AngleWidget).display = True
269
+ self.query_one("#splash", SplashWidget).display = False
270
+ self.query_one("#reel", ReelWidget).display = False
271
+ self.query_one("#info", Static).update("방향키로 각도 조절, Space로 캐스트\nq를 누륵 종료")
272
+ self.query_one("#result", Static).update(
273
+ "[b]mulgogi[/b]\n터미널에서 즐기는 낚시 게임\n\n"
274
+ "[green]Space[/green]: 캐스트 / 입질 / 릴\n"
275
+ "[green]← →[/green]: 각도 조절 (장식용)\n"
276
+ "[green]q[/green]: 종료"
277
+ )
278
+
279
+
280
+ def main():
281
+ parser = argparse.ArgumentParser(prog="mulgogi", description="A fishing game in your terminal")
282
+ parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
283
+ parser.parse_args()
284
+ app = MulgogiApp()
285
+ app.run()
286
+
287
+
288
+ if __name__ == "__main__":
289
+ main()
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mulgogi"
7
+ version = "0.1.0"
8
+ description = "A fishing game in your terminal"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ {name = "justart-dev", email = "justart-dev@users.noreply.github.com"}
14
+ ]
15
+ keywords = ["cli", "game", "fishing", "terminal", "tui"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Games/Entertainment",
27
+ ]
28
+ dependencies = [
29
+ "textual>=0.70.0",
30
+ ]
31
+
32
+ [project.scripts]
33
+ mulgogi = "mulgogi.main:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/justart-dev/mulgogi"
37
+ Repository = "https://github.com/justart-dev/mulgogi"
38
+ Issues = "https://github.com/justart-dev/mulgogi/issues"
@@ -0,0 +1 @@
1
+ textual>=0.70.0
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PyPI에 mulgogi를 올린 후 실행하여 Homebrew formula의 resource 블록을 생성합니다.
4
+
5
+ pip install homebrew-pypi-poet
6
+ python scripts/generate-homebrew-formula.py
7
+
8
+ 출력된 내용을 homebrew-mulgogi/Formula/mulgogi.rb의 resource 영역에 붙여넣으세요.
9
+ """
10
+
11
+ import subprocess
12
+ import sys
13
+
14
+
15
+ def main():
16
+ try:
17
+ output = subprocess.check_output(["poet", "mulgogi"], text=True)
18
+ except FileNotFoundError:
19
+ print("homebrew-pypi-poet가 설치되지 않았습니다.", file=sys.stderr)
20
+ print(" pip install homebrew-pypi-poet", file=sys.stderr)
21
+ sys.exit(1)
22
+ except subprocess.CalledProcessError as e:
23
+ print(f"실패: {e}", file=sys.stderr)
24
+ sys.exit(1)
25
+
26
+ print(output)
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()