wocli 0.2.0__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.
wocli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """wocli - 一个为发电而生的何意味终端小工具"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """wocli command modules."""
@@ -0,0 +1,51 @@
1
+ """wocli badapple - 播放 Bad Apple!! ASCII 动画"""
2
+ import sys
3
+ import os
4
+ import time
5
+
6
+ C_WHITE = "\033[97m"
7
+ C_RESET = "\033[0m"
8
+
9
+ FRAME_FILE = os.path.join(os.path.dirname(__file__), "frames.txt")
10
+
11
+
12
+ def load_frames():
13
+ """加载帧序列,返回帧列表"""
14
+ if not os.path.exists(FRAME_FILE):
15
+ print("\n frames.txt 未找到,请确保下载了 Bad Apple ASCII 帧序列并放到 commands/ 目录。")
16
+ print(" 下载地址: https://raw.githubusercontent.com/kevinjycui/bad-apple/main/frames.txt\n")
17
+ return None
18
+ with open(FRAME_FILE, "r", encoding="utf-8") as f:
19
+ data = f.read()
20
+ # 帧之间用 SPLIT 分隔,每一帧是一段文本
21
+ frames = data.split("\nSPLIT\n")
22
+ return frames
23
+
24
+
25
+ def run():
26
+ print("\n [ Bad Apple!! ]")
27
+ print(" 正在加载帧序列...")
28
+ frames = load_frames()
29
+ if not frames:
30
+ return
31
+ print(f" 共 {len(frames)} 帧,按 Enter 开始播放...")
32
+ input()
33
+
34
+ # 隐藏光标
35
+ sys.stdout.write("\033[?25l")
36
+ sys.stdout.flush()
37
+
38
+ try:
39
+ for frame in frames:
40
+ # 将光标移到终端左上角(不清屏,直接覆盖)
41
+ sys.stdout.write("\033[H")
42
+ sys.stdout.write(C_WHITE + frame + C_RESET)
43
+ sys.stdout.flush()
44
+ time.sleep(1 / 30) # 30fps,可根据机器性能调整
45
+ except KeyboardInterrupt:
46
+ pass
47
+ finally:
48
+ sys.stdout.write("\033[?25h") # 恢复光标
49
+ sys.stdout.write(C_RESET)
50
+ sys.stdout.flush()
51
+ print("\n 播放完毕。")
@@ -0,0 +1,124 @@
1
+ """wocli battery - 电池信息与健康度打分"""
2
+ import subprocess
3
+ import platform
4
+ import re
5
+
6
+
7
+ def get_battery_macos():
8
+ try:
9
+ result = subprocess.run(["pmset", "-g", "batt"], capture_output=True, text=True)
10
+ output = result.stdout
11
+ # 电量
12
+ pct_match = re.search(r"(\d{1,3})%", output)
13
+ percent = int(pct_match.group(1)) if pct_match else -1
14
+ # 充电状态
15
+ status = "未知"
16
+ if "charging" in output:
17
+ status = "充电中"
18
+ elif "discharging" in output:
19
+ status = "使用中"
20
+ elif "charged" in output:
21
+ status = "已充满"
22
+ # 健康度和循环次数
23
+ try:
24
+ health = subprocess.run(
25
+ ["system_profiler", "SPPowerDataType"],
26
+ capture_output=True, text=True
27
+ )
28
+ cycles_match = re.search(r"Cycle Count:\s*(\d+)", health.stdout)
29
+ cycles = int(cycles_match.group(1)) if cycles_match else -1
30
+ max_match = re.search(r"Maximum Capacity:\s*(\d+)%", health.stdout)
31
+ max_cap = int(max_match.group(1)) if max_match else 100
32
+ except:
33
+ cycles = -1
34
+ max_cap = 100
35
+ return percent, status, cycles, max_cap
36
+ except:
37
+ return -1, "未知", -1, 100
38
+
39
+
40
+ def get_battery_windows():
41
+ try:
42
+ result = subprocess.run(
43
+ ["wmic", "path", "Win32_Battery", "get", "EstimatedChargeRemaining,EstimatedRunTime,BatteryStatus"],
44
+ capture_output=True, text=True
45
+ )
46
+ lines = [l.strip() for l in result.stdout.split("\n") if l.strip()]
47
+ if len(lines) < 2:
48
+ return -1, "未知", -1, 100
49
+ parts = lines[1].split()
50
+ percent = int(parts[0]) if parts else -1
51
+ status_map = {"1": "使用中", "2": "充电中", "3": "已充满"}
52
+ status = status_map.get(parts[1], "未知") if len(parts) > 1 else "未知"
53
+ return percent, status, -1, 100
54
+ except:
55
+ return -1, "未知", -1, 100
56
+
57
+
58
+ def get_battery_linux():
59
+ try:
60
+ with open("/sys/class/power_supply/BAT0/capacity", "r") as f:
61
+ percent = int(f.read().strip())
62
+ with open("/sys/class/power_supply/BAT0/status", "r") as f:
63
+ raw = f.read().strip()
64
+ status_map = {"Discharging": "使用中", "Charging": "充电中", "Full": "已充满"}
65
+ status = status_map.get(raw, raw)
66
+ return percent, status, -1, 100
67
+ except:
68
+ return -1, "未知", -1, 100
69
+
70
+
71
+ def score_battery(percent, cycles, max_cap):
72
+ if percent < 0:
73
+ return 0, "无数据"
74
+ charge_score = percent
75
+ if cycles > 0:
76
+ cycle_score = max(0, 100 - cycles * 0.1)
77
+ else:
78
+ cycle_score = 100
79
+ health_score = max_cap
80
+ total = charge_score * 0.3 + cycle_score * 0.3 + health_score * 0.4
81
+ total = min(100, total)
82
+
83
+ if total >= 95:
84
+ grade = "S级 · 古希腊掌管电池的神"
85
+ elif total >= 85:
86
+ grade = "A级 · 还能战三年"
87
+ elif total >= 70:
88
+ grade = "B级 · 电池有点虚了"
89
+ elif total >= 50:
90
+ grade = "C级 · 已经离不开充电器"
91
+ else:
92
+ grade = "D级 · 电池这么老,零百加速只要三十分钟吧"
93
+
94
+ return round(total, 2), grade
95
+
96
+
97
+ def run():
98
+ system = platform.system()
99
+ if system == "Darwin":
100
+ percent, status, cycles, max_cap = get_battery_macos()
101
+ elif system == "Windows":
102
+ percent, status, cycles, max_cap = get_battery_windows()
103
+ else:
104
+ percent, status, cycles, max_cap = get_battery_linux()
105
+
106
+ if percent < 0:
107
+ print("\n 无法获取电池信息。\n")
108
+ return
109
+
110
+ score_val, grade = score_battery(percent, cycles, max_cap)
111
+ bar_len = 20
112
+ filled = int(bar_len * percent / 100)
113
+ bar = "=" * filled + "-" * (bar_len - filled)
114
+
115
+ print()
116
+ print(f" 当前电量:[{bar}] {percent}%")
117
+ print(f" 充电状态:{status}")
118
+ if cycles >= 0:
119
+ print(f" 循环次数:{cycles} 次")
120
+ if max_cap < 100:
121
+ print(f" 最大容量:{max_cap}%")
122
+ print(f" 健康评分:{score_val:.2f} / 100.00")
123
+ print(f" 评级:{grade}")
124
+ print()
wocli/commands/cow.py ADDED
@@ -0,0 +1,58 @@
1
+ """wocli cow - ASCII牛说你说的话."""
2
+
3
+ import sys
4
+
5
+
6
+ COW = r"""
7
+ \ ^__^
8
+ \ (oo)\_______
9
+ (__)\ )\/\
10
+ ||----w |
11
+ || ||
12
+ """
13
+
14
+
15
+ def make_bubble(text):
16
+ """生成对话气泡."""
17
+ lines = []
18
+ words = text.split()
19
+ line = ""
20
+ for word in words:
21
+ if len(line) + len(word) + 1 <= 36:
22
+ line += (" " + word) if line else word
23
+ else:
24
+ lines.append(line)
25
+ line = word
26
+ if line:
27
+ lines.append(line)
28
+
29
+ max_len = max(len(l) for l in lines) if lines else 0
30
+ top = " " + "_" * (max_len + 2)
31
+ bottom = " " + "-" * (max_len + 2)
32
+
33
+ bubble = [top]
34
+ if len(lines) == 1:
35
+ bubble.append(f" < {lines[0]} >")
36
+ else:
37
+ for i, l in enumerate(lines):
38
+ if i == 0:
39
+ bubble.append(f" / {l:<{max_len}} \\")
40
+ elif i == len(lines) - 1:
41
+ bubble.append(f" \\ {l:<{max_len}} /")
42
+ else:
43
+ bubble.append(f" | {l:<{max_len}} |")
44
+ bubble.append(bottom)
45
+ return "\n".join(bubble)
46
+
47
+
48
+ def run():
49
+ """运行 cow 命令."""
50
+ if len(sys.argv) > 1:
51
+ text = " ".join(sys.argv[1:])
52
+ else:
53
+ text = "哞————(无意味)"
54
+
55
+ print()
56
+ print(make_bubble(text))
57
+ print(COW)
58
+ print()
wocli/commands/dead.py ADDED
@@ -0,0 +1,41 @@
1
+ """wocli dead - 伪装终端死机"""
2
+ import sys
3
+ import time
4
+ import random
5
+
6
+
7
+ def run():
8
+ print()
9
+ print(" !! WARNING: Kernel panic detected !!")
10
+ time.sleep(1)
11
+
12
+ # 花屏
13
+ chars = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`"
14
+ colors = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96]
15
+ for _ in range(20):
16
+ line = " " + "".join(random.choice(chars) for _ in range(60))
17
+ color = random.choice(colors)
18
+ sys.stdout.write(f"\033[{color}m{line}\033[0m\n")
19
+ sys.stdout.flush()
20
+ time.sleep(0.05)
21
+
22
+ time.sleep(0.5)
23
+
24
+ # 蓝屏
25
+ sys.stdout.write("\033[44m\033[37m")
26
+ sys.stdout.write("\033[2J\033[H")
27
+ print()
28
+ print(" *** SYSTEM ERROR ***")
29
+ print()
30
+ print(" A fatal exception has occurred at 0xDEADBEEF")
31
+ print(" The system has been halted.")
32
+ print()
33
+ print(" Attempting to recover...")
34
+ sys.stdout.flush()
35
+
36
+ time.sleep(3)
37
+
38
+ # 恢复
39
+ sys.stdout.write("\033[0m\033[2J\033[H")
40
+ sys.stdout.write(" Just kidding. Your terminal is fine.\n\n")
41
+ sys.stdout.flush()
wocli/commands/dino.py ADDED
@@ -0,0 +1,229 @@
1
+ import sys, os, time, random, platform, signal
2
+
3
+ W, H = 70, 20
4
+ GROUND = H - 3
5
+ X0 = 10
6
+ C_ORANGE = "\033[38;5;214m"
7
+ C_GREEN = "\033[32m"
8
+ C_WHITE = "\033[97m"
9
+ C_RESET = "\033[0m"
10
+
11
+ CLAUDE_A = [" ▗ ▗ ", " ▘▘ ", " ▝▝ "]
12
+ CLAUDE_B = [" ▖ ▖ ", " ▝▝ ", " ▘▘ "]
13
+
14
+ # ---------- 跨平台终端控制 ----------
15
+ if platform.system() == "Windows":
16
+ import msvcrt
17
+ def setup_terminal(): pass
18
+ def restore_terminal(): pass
19
+ def get_key():
20
+ if msvcrt.kbhit():
21
+ ch = msvcrt.getch()
22
+ if ch == b'\xe0': return ''
23
+ try:
24
+ return ch.decode('utf-8', errors='ignore')
25
+ except:
26
+ return ''
27
+ return ''
28
+ else:
29
+ import termios, tty, fcntl
30
+ _old_term = None
31
+ _old_flags = None
32
+ _fd = sys.stdin.fileno()
33
+
34
+ def setup_terminal():
35
+ global _old_term, _old_flags
36
+ _old_term = termios.tcgetattr(_fd)
37
+ _old_flags = fcntl.fcntl(_fd, fcntl.F_GETFL)
38
+ tty.setraw(_fd)
39
+ fcntl.fcntl(_fd, fcntl.F_SETFL, _old_flags | os.O_NONBLOCK)
40
+
41
+ def restore_terminal():
42
+ if _old_term is not None:
43
+ termios.tcsetattr(_fd, termios.TCSADRAIN, _old_term)
44
+ if _old_flags is not None:
45
+ fcntl.fcntl(_fd, fcntl.F_SETFL, _old_flags)
46
+
47
+ def get_key():
48
+ try:
49
+ data = os.read(_fd, 1).decode('utf-8', errors='ignore')
50
+ return data
51
+ except BlockingIOError:
52
+ return ''
53
+
54
+ class Game:
55
+ def __init__(self):
56
+ self.score = 0
57
+ self.high = self._load_high()
58
+ self.ground_y = GROUND - len(CLAUDE_A)
59
+ self.y = float(self.ground_y)
60
+ self.vy = 0.0
61
+ self.objs = []
62
+ self.speed = 0.8
63
+ self.frame = 0
64
+ self.over = False
65
+ self._hide_cursor()
66
+ signal.signal(signal.SIGINT, self._stop)
67
+ signal.signal(signal.SIGTERM, self._stop)
68
+
69
+ def _stop(self, *args):
70
+ self.over = True
71
+
72
+ def _hide_cursor(self):
73
+ sys.stdout.write("\033[?25l")
74
+ sys.stdout.flush()
75
+
76
+ def _show_cursor(self):
77
+ sys.stdout.write("\033[?25h")
78
+ sys.stdout.flush()
79
+
80
+ def _load_high(self):
81
+ p = os.path.expanduser("~/.wocli_dino")
82
+ try:
83
+ with open(p) as f:
84
+ return int(f.read())
85
+ except:
86
+ return 0
87
+
88
+ def _save_high(self):
89
+ if self.score > self.high:
90
+ self.high = self.score
91
+ p = os.path.expanduser("~/.wocli_dino")
92
+ try:
93
+ with open(p, "w") as f:
94
+ f.write(str(self.score))
95
+ except:
96
+ pass
97
+
98
+ def jump(self):
99
+ if self.vy == 0 and self.y >= self.ground_y:
100
+ self.vy = -2.2
101
+
102
+ def update(self):
103
+ if self.over:
104
+ return
105
+ self.frame += 1
106
+ self.score += 1
107
+ if self.score % 600 == 0:
108
+ self.speed = min(2.0, self.speed * 1.05)
109
+
110
+ self.vy += 0.2
111
+ self.y += self.vy
112
+ if self.y >= self.ground_y:
113
+ self.y = self.ground_y
114
+ self.vy = 0
115
+
116
+ # 移除超出屏幕的障碍
117
+ self.objs = [o for o in self.objs if o[0] > -15]
118
+ for o in self.objs:
119
+ o[0] -= self.speed
120
+
121
+ # 生成障碍
122
+ if len(self.objs) < 3 and random.random() < 0.02:
123
+ last_x = max([o[0] for o in self.objs]) if self.objs else W
124
+ if last_x < W - 20:
125
+ if random.random() < 0.65:
126
+ h = random.randint(3, 4)
127
+ w = random.randint(5, 7)
128
+ cactus = []
129
+ for i in range(h):
130
+ line = ""
131
+ for j in range(w):
132
+ if i == 0 and j == w // 2: line += "┃"
133
+ elif i > 0 and (j == 0 or j == w - 1): line += "│"
134
+ elif i == h - 1: line += "━"
135
+ else: line += " " if random.random() < 0.7 else "╻"
136
+ cactus.append(line)
137
+ self.objs.append([W, w, h, GROUND - h, cactus, C_GREEN])
138
+ else:
139
+ bird_y = GROUND - random.randint(4, 7)
140
+ bird = [" ^^ ", " <^^> "]
141
+ self.objs.append([W, 6, 2, bird_y, bird, C_WHITE])
142
+
143
+ # 碰撞检测
144
+ claude_h = len(CLAUDE_A)
145
+ claude_w = max(len(r) for r in CLAUDE_A)
146
+ cx0, cy0 = X0, int(self.y)
147
+ for ox, ow, oh, oy, _, _ in self.objs:
148
+ if (cx0 + claude_w > ox and cx0 < ox + ow and
149
+ cy0 + claude_h > oy and cy0 < oy + oh):
150
+ self.over = True
151
+ self._save_high()
152
+ break
153
+
154
+ def render(self):
155
+ sys.stdout.write("\033[2J\033[H")
156
+ buf = [[" "] * W for _ in range(H)]
157
+ for x in range(W):
158
+ buf[GROUND][x] = "─"
159
+
160
+ frame_char = CLAUDE_A if (self.frame // 3) % 2 == 0 else CLAUDE_B
161
+ for dy, row in enumerate(frame_char):
162
+ y = int(self.y) + dy
163
+ if 0 <= y < H:
164
+ for dx, ch in enumerate(row):
165
+ x = X0 + dx
166
+ if 0 <= x < W and ch != " ":
167
+ buf[y][x] = f"{C_ORANGE}{ch}{C_RESET}"
168
+
169
+ for ox, ow, oh, oy, shape, col in self.objs:
170
+ for sy, row in enumerate(shape):
171
+ y = oy + sy
172
+ if 0 <= y < H:
173
+ for sx, ch in enumerate(row):
174
+ x = ox + sx
175
+ if 0 <= x < W and ch != " ":
176
+ buf[y][x] = f"{col}{ch}{C_RESET}"
177
+
178
+ for row in buf:
179
+ print(" " + "".join(row))
180
+
181
+ score_str = f"Score: {self.score:05d} High: {self.high:05d}"
182
+ hint = "Space:Jump Q:Quit" if not self.over else "GAME OVER R:Restart Q:Quit"
183
+ print(f" {C_ORANGE}{score_str}{C_RESET} {hint}")
184
+
185
+ def input_handle(self):
186
+ k = get_key()
187
+ while k:
188
+ if k == ' ':
189
+ self.jump()
190
+ elif k == 'q':
191
+ self.over = True
192
+ elif k == 'r' and self.over:
193
+ self._save_high()
194
+ self.__init__()
195
+ k = get_key()
196
+
197
+ def run(self):
198
+ try:
199
+ while not self.over:
200
+ self.input_handle()
201
+ self.update()
202
+ self.render()
203
+ time.sleep(0.04)
204
+ self.render()
205
+ time.sleep(1.5)
206
+ finally:
207
+ self._show_cursor()
208
+ self._save_high()
209
+
210
+
211
+ def main():
212
+ print(f"\n {C_ORANGE}╭─ Claude Run ──────────────────────────────╮{C_RESET}")
213
+ print(f" {C_ORANGE}│{C_RESET} 橙色小人跑酷,按任意键开始 {C_ORANGE}│{C_RESET}")
214
+ print(f" {C_ORANGE}╰──────────────────────────────────────────╯{C_RESET}")
215
+
216
+ setup_terminal()
217
+ try:
218
+ # 等待任意键
219
+ while True:
220
+ if get_key():
221
+ break
222
+ time.sleep(0.05)
223
+ g = Game()
224
+ g.run()
225
+ finally:
226
+ restore_terminal()
227
+
228
+ if __name__ == "__main__":
229
+ main()
@@ -0,0 +1,52 @@
1
+ """wocli glitch - 文字故障风效果."""
2
+
3
+ import sys
4
+ import random
5
+
6
+
7
+ def glitch_case(text):
8
+ return "".join(
9
+ c.upper() if random.random() > 0.5 else c.lower()
10
+ for c in text
11
+ )
12
+
13
+
14
+ def glitch_leet(text):
15
+ mapping = {
16
+ "a": "4", "A": "4",
17
+ "e": "3", "E": "3",
18
+ "i": "1", "I": "1",
19
+ "o": "0", "O": "0",
20
+ "s": "5", "S": "5",
21
+ "t": "7", "T": "7",
22
+ "l": "1", "L": "1",
23
+ }
24
+ return "".join(mapping.get(c, c) for c in text)
25
+
26
+
27
+ def glitch_zalgo(text):
28
+ up = "".join(random.choice("''^`") for _ in range(len(text)))
29
+ down = "".join(random.choice(",.;:") for _ in range(len(text)))
30
+ return up + "\n" + text + "\n" + down
31
+
32
+
33
+ def glitch_morse(text):
34
+ return " ".join(
35
+ "".join(random.choice([".", "-"]) for _ in range(random.randint(1, 4)))
36
+ for _ in text.split()
37
+ )
38
+
39
+
40
+ def run():
41
+ """运行 glitch 命令."""
42
+ if len(sys.argv) > 1:
43
+ text = " ".join(sys.argv[1:])
44
+ else:
45
+ text = "Hello World"
46
+
47
+ print()
48
+ print(f" Original : {text}")
49
+ print(f" Glitch 1 : {glitch_case(text)}")
50
+ print(f" Glitch 2 : {glitch_leet(text)}")
51
+ print(f" Glitch 3 : {glitch_zalgo(text)}")
52
+ print()
@@ -0,0 +1,40 @@
1
+ """wocli goodbye - 花式退出动画."""
2
+
3
+ import sys
4
+ import time
5
+ import random
6
+
7
+
8
+ MESSAGES = [
9
+ "明天又是充满 bug 的一天(目移)",
10
+ "记得 commit 和 push 哦",
11
+ "没人觉得 rust 很香吗",
12
+ "就当是为了我,对他使用force-push吧",
13
+ "Goodbye world!",
14
+ "本次编码结束,bug 数 +1。",
15
+ "代码能跑就行...",
16
+ "感谢使用awa",
17
+ "Ctrl+C 的人生不需要解释",
18
+ ]
19
+
20
+
21
+ def run():
22
+ """运行 goodbye 命令."""
23
+ msg = random.choice(MESSAGES)
24
+
25
+ print()
26
+ print(" System will shut down in 3 seconds...")
27
+ time.sleep(1)
28
+
29
+ width = 30
30
+ for i in range(width + 1):
31
+ bar = "=" * i + ">" + " " * (width - i)
32
+ pct = int(i / width * 100)
33
+ sys.stdout.write(f"\r [{bar}] {pct}%")
34
+ sys.stdout.flush()
35
+ time.sleep(0.08)
36
+
37
+ print()
38
+ print(f" {msg}")
39
+ print(" Bye!")
40
+ print()
wocli/commands/gpa.py ADDED
@@ -0,0 +1,84 @@
1
+ """wocli gpa - 加权GPA计算器,带毒舌点评."""
2
+
3
+
4
+ def grade_to_point(grade):
5
+ """百分制转绩点(4.0制)."""
6
+ if grade >= 90:
7
+ return 4.0
8
+ elif grade >= 85:
9
+ return 3.7
10
+ elif grade >= 82:
11
+ return 3.3
12
+ elif grade >= 78:
13
+ return 3.0
14
+ elif grade >= 75:
15
+ return 2.7
16
+ elif grade >= 72:
17
+ return 2.3
18
+ elif grade >= 68:
19
+ return 2.0
20
+ elif grade >= 64:
21
+ return 1.5
22
+ elif grade >= 60:
23
+ return 1.0
24
+ else:
25
+ return 0.0
26
+
27
+
28
+ def comment(gpa):
29
+ """根据GPA给出毒舌点评."""
30
+ if gpa >= 3.8:
31
+ return "你住在图书馆里。图书馆现在是你家。"
32
+ elif gpa >= 3.5:
33
+ return "再熬一点。提高一分干掉千人。"
34
+ elif gpa >= 3.0:
35
+ return "3分万岁。不难看就行。"
36
+ elif gpa >= 2.5:
37
+ return "正在卷GPA,亿万大学生必须加把劲。"
38
+ elif gpa >= 2.0:
39
+ return "两分够用了。Passing is passing。"
40
+ elif gpa >= 1.0:
41
+ return "至少老师记住你了,怎么记住的你别管。"
42
+ else:
43
+ return "回家吧孩子回家吧,你比较适合做一头猪。"
44
+
45
+
46
+ def run():
47
+ """运行 gpa 命令."""
48
+ print()
49
+ print(" [ GPA 计算器 ]")
50
+ print(" " + "-" * 40)
51
+
52
+ try:
53
+ n = int(input(" 课程数量: "))
54
+ except (ValueError, EOFError):
55
+ print("\n 输入无效。\n")
56
+ return
57
+
58
+ total_points = 0
59
+ total_credits = 0
60
+
61
+ for i in range(1, n + 1):
62
+ try:
63
+ credit = float(input(f" 课程{i} - 学分: "))
64
+ grade = float(input(f" 课程{i} - 成绩(0-100): "))
65
+ except (ValueError, EOFError):
66
+ print("\n 输入无效。\n")
67
+ return
68
+
69
+ point = grade_to_point(grade)
70
+ total_points += point * credit
71
+ total_credits += credit
72
+
73
+ if total_credits == 0:
74
+ print("\n 总学分不能为零。\n")
75
+ return
76
+
77
+ gpa = total_points / total_credits
78
+
79
+ print()
80
+ print(f" 加权 GPA:{gpa:.2f} / 4.0")
81
+ print(f" 总学分:{total_credits:.0f}")
82
+ print()
83
+ print(f" {comment(gpa)}")
84
+ print()