From 17b314407fd4ec9ce1f585b61ba6d0b860a72add Mon Sep 17 00:00:00 2001 From: Tommmy Date: Wed, 14 May 2025 08:19:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20RPG=5FSample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一个纯终端的RPG游戏基本样例 包含键盘、鼠标交互等 Signed-off-by: Tommmy --- RPG_Sample/config.py | 16 +++++++ RPG_Sample/inventory.py | 80 +++++++++++++++++++++++++++++++++++ RPG_Sample/main.py | 31 ++++++++++++++ RPG_Sample/menus.py | 77 +++++++++++++++++++++++++++++++++ RPG_Sample/world.py | 94 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 RPG_Sample/config.py create mode 100644 RPG_Sample/inventory.py create mode 100644 RPG_Sample/main.py create mode 100644 RPG_Sample/menus.py create mode 100644 RPG_Sample/world.py diff --git a/RPG_Sample/config.py b/RPG_Sample/config.py new file mode 100644 index 00000000..d5ed0260 --- /dev/null +++ b/RPG_Sample/config.py @@ -0,0 +1,16 @@ +# config.py + +import curses + +difficulty = "Normal" +player = {"x": 0, "y": 0} +camera = {"x": 0, "y": 0} +objects = set() + +def init_colors(): + # 初始化配色:前景色, 背景黑 + curses.start_color() + curses.init_pair(1, curses.COLOR_YELLOW, curses.COLOR_BLACK) # 玩家 + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # 物品 + curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLACK) # 菜单文字 + curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK) # 标题/提示 diff --git a/RPG_Sample/inventory.py b/RPG_Sample/inventory.py new file mode 100644 index 00000000..f9800208 --- /dev/null +++ b/RPG_Sample/inventory.py @@ -0,0 +1,80 @@ +# inventory.py + +import curses + +class Inventory: + def __init__(self, cols=5, rows=4): + self.cols = cols + self.rows = rows + self.slots = {i: None for i in range(cols * rows)} + for i, ch in enumerate(["S", "H", "P"]): + if i < cols * rows: + self.slots[i] = ch + self.held = None + self.held_pos = (0, 0) + + def draw(self, stdscr, scr_w, scr_h): + side = int(min(scr_w, scr_h) * 0.6) + cell = (side - 2) // max(self.cols, self.rows) + win_w = cell * self.cols + 2 + win_h = cell * self.rows + 2 + sx = (scr_w - win_w) // 2 + sy = (scr_h - win_h) // 2 + + win = curses.newwin(win_h, win_w, sy, sx) + win.bkgd(' ', curses.color_pair(3)) + win.box() + + for idx in range(self.cols * self.rows): + r, c = divmod(idx, self.cols) + y0 = 1 + r * cell + x0 = 1 + c * cell + # 绘格子 + for x in range(x0, x0+cell): + win.addch(y0, x, curses.ACS_HLINE) + win.addch(y0+cell-1, x, curses.ACS_HLINE) + for y in range(y0, y0+cell): + win.addch(y, x0, curses.ACS_VLINE) + win.addch(y, x0+cell-1, curses.ACS_VLINE) + win.addch(y0, x0, curses.ACS_ULCORNER) + win.addch(y0, x0+cell-1, curses.ACS_URCORNER) + win.addch(y0+cell-1, x0, curses.ACS_LLCORNER) + win.addch(y0+cell-1, x0+cell-1, curses.ACS_LRCORNER) + # 画字母 + ch = self.slots.get(idx) + if ch: + win.addch(y0 + cell//2, x0 + cell//2, ch, curses.color_pair(2)) + + win.refresh() + + if self.held: + my, mx = self.held_pos + if 0 <= my < scr_h and 0 <= mx < scr_w: + stdscr.addch(my, mx, self.held, curses.color_pair(2)) + stdscr.refresh() + + return (sy, sx, win_h, win_w) + + def click(self, my, mx, area): + sy, sx, h, w = area + if not (sy <= my < sy+h and sx <= mx < sx+w): + return + rel_y, rel_x = my-sy-1, mx-sx-1 + cell_h = (h-2)//self.rows + cell_w = (w-2)//self.cols + r = rel_y // cell_h + c = rel_x // cell_w + if 0 <= r < self.rows and 0 <= c < self.cols: + idx = r*self.cols + c + if self.held: + if self.slots.get(idx) is None: + self.slots[idx] = self.held + self.held = None + else: + ch = self.slots.get(idx) + if ch: + self.held = ch + self.slots[idx] = None + + def move(self, my, mx): + self.held_pos = (my, mx) diff --git a/RPG_Sample/main.py b/RPG_Sample/main.py new file mode 100644 index 00000000..36af4a36 --- /dev/null +++ b/RPG_Sample/main.py @@ -0,0 +1,31 @@ +# main.py + +import curses +from menus import main_menu, start_game_menu, settings_menu, credits_menu +from world import game_loop, init_objects +from config import init_colors, player, camera + +def main(stdscr): + curses.curs_set(0) + init_colors() + + h,w=stdscr.getmaxyx() + height, width = h-1, w + + player["x"],player["y"]=0,0 + camera["x"],camera["y"] = player["x"]-width//2, player["y"]-height//2 + + init_objects(width, height) + + while True: + choice=main_menu(stdscr) + if choice==-1: break + elif choice==0: + mode=start_game_menu(stdscr) + if mode in (0,1): game_loop(stdscr, width, height) + elif choice==1: settings_menu(stdscr) + elif choice==2: credits_menu(stdscr) + elif choice==3: break + +if __name__=="__main__": + curses.wrapper(main) \ No newline at end of file diff --git a/RPG_Sample/menus.py b/RPG_Sample/menus.py new file mode 100644 index 00000000..1356f945 --- /dev/null +++ b/RPG_Sample/menus.py @@ -0,0 +1,77 @@ +# menus.py + +import curses +from config import difficulty + +def main_menu(stdscr): + options = ["Start Game","Settings","Credits","Exit"] + sel=0 + while True: + stdscr.clear() + h,w=stdscr.getmaxyx() + if h<20 or w<50: + stdscr.addstr(h//2,w//2-10,"Terminal too small!") + stdscr.refresh(); stdscr.getch(); return -1 + # ASCII 标题 + draw_title(stdscr,h,w) + for i,opt in enumerate(options): + x=w//2-len(opt)//2-2; y=h//2+i + if i==sel: + stdscr.addstr(y,x,f"> {opt} <",curses.color_pair(2)) + else: + stdscr.addstr(y,x,f" {opt} ",curses.color_pair(3)) + k=stdscr.getch() + if k in (ord('w'),curses.KEY_UP): sel=(sel-1)%len(options) + elif k in (ord('s'),curses.KEY_DOWN): sel=(sel+1)%len(options) + elif k==ord('\n'): return sel + +def draw_title(stdscr,h,w): + title=r""" + _______ _______ _______ +( ____ \( ___ )( ____ \ +| ( \/| ( ) || ( \/ +| | | | | || (__ +| | | | | || __) +| | | | | || ( +| (____/\| (___) || (____/\ +(_______/(_______)(_______/ + """ + for i,line in enumerate(title.splitlines()): + stdscr.addstr(h//2-10+i, w//2-len(line)//2, line, curses.color_pair(4)) + +def start_game_menu(stdscr): + return _generic(stdscr,["Infinite World","Maze","Back"]) + +def settings_menu(stdscr): + global difficulty + opts=["Easy","Normal","Hard"]; idx=opts.index(difficulty) + while True: + stdscr.clear(); h,w=stdscr.getmaxyx() + stdscr.addstr(h//2-2,w//2-7,"Select Difficulty",curses.color_pair(4)) + for i,d in enumerate(opts): + x=w//2-len(d)//2; y=h//2+i + if i==idx: stdscr.addstr(y,x,f"> {d} <",curses.color_pair(2)) + else: stdscr.addstr(y,x,f" {d} ",curses.color_pair(3)) + k=stdscr.getch() + if k in (ord('w'),curses.KEY_UP): idx=(idx-1)%3 + elif k in (ord('s'),curses.KEY_DOWN): idx=(idx+1)%3 + elif k==ord('\n'): difficulty=opts[idx]; return + +def credits_menu(stdscr): + stdscr.clear(); h,w=stdscr.getmaxyx() + msg="Made by 陈坤阳" + stdscr.addstr(h//2,w//2-len(msg)//2,msg,curses.color_pair(4)) + stdscr.refresh(); stdscr.getch() + +def _generic(stdscr,opts): + sel=0 + while True: + stdscr.clear(); h,w=stdscr.getmaxyx() + for i,o in enumerate(opts): + x=w//2-len(o)//2; y=h//2+i + if i==sel: stdscr.addstr(y,x,f"> {o} <",curses.color_pair(2)) + else: stdscr.addstr(y,x,f" {o} ",curses.color_pair(3)) + k=stdscr.getch() + if k in (ord('w'),curses.KEY_UP): sel=(sel-1)%len(opts) + elif k in (ord('s'),curses.KEY_DOWN): sel=(sel+1)%len(opts) + elif k==ord('\n'): return sel diff --git a/RPG_Sample/world.py b/RPG_Sample/world.py new file mode 100644 index 00000000..be1dff74 --- /dev/null +++ b/RPG_Sample/world.py @@ -0,0 +1,94 @@ +# world.py + +import random +import curses +from config import objects, player, camera +from inventory import Inventory + +inventory = Inventory(cols=5, rows=4) +show_inv = False +inv_area = None + +def init_objects(width, height): + objects.clear() + for _ in range(99999): + x = random.randint(-2000, 2000) + y = random.randint(-2000, 2000) + objects.add((x, y)) + camx, camy = camera["x"], camera["y"] + for _ in range(200): + x = random.randint(camx, camx+width-1) + y = random.randint(camy, camy+height-1) + objects.add((x, y)) + +def draw_map(stdscr, width, height): + stdscr.clear() + for y in range(height): + for x in range(width): + wx, wy = camera["x"]+x, camera["y"]+y + if (wx,wy)==(player["x"],player["y"]): + stdscr.addstr(y,x,"@",curses.color_pair(1)) + elif (wx,wy) in objects: + stdscr.addstr(y,x,"*",curses.color_pair(2)) + stdscr.refresh() + +def game_loop(stdscr, width, height): + global show_inv, inv_area + + stdscr.keypad(True) + curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) + curses.mouseinterval(0) + curses.putp(b"\033[?1003h") + + stdscr.nodelay(True) + stdscr.timeout(30) + + try: + while True: + draw_map(stdscr, width, height) + if show_inv: + inv_area = inventory.draw(stdscr, width, height) + + key = stdscr.getch() + if key == ord('e'): + show_inv = not show_inv + stdscr.clear() + continue + + if show_inv and key == curses.KEY_MOUSE: + _, mx, my, _, bstate = curses.getmouse() + inventory.move(my, mx) + if bstate & curses.BUTTON1_PRESSED: + inventory.click(my, mx, inv_area) + continue + + # ←↑→↓ 控制 + if key == curses.KEY_UP: + player["y"] -= 1 + elif key == curses.KEY_DOWN: + player["y"] += 1 + elif key == curses.KEY_LEFT: + player["x"] -= 1 + elif key == curses.KEY_RIGHT: + player["x"] += 1 + elif key == 27: + break + + if player["x"]-camera["x"]<20: camera["x"]-=1 + elif player["x"]-camera["x"]>width-21: camera["x"]+=1 + if player["y"]-camera["y"]<7: camera["y"]-=1 + elif player["y"]-camera["y"]>height-8: camera["y"]+=1 + + if random.random()<0.01: + ox = random.randint(player["x"]-20,player["x"]+20) + oy = random.randint(player["y"]-20,player["y"]+20) + objects.add((ox,oy)) + + pos = (player["x"],player["y"]) + if pos in objects: + objects.remove(pos) + stdscr.addstr(height,0,"You picked up an item!",curses.color_pair(4)) + stdscr.refresh() + curses.napms(300) + finally: + curses.putp(b"\033[?1003l")