From 4de5a530d578bda9ffc5d79156900c13b0553e02 Mon Sep 17 00:00:00 2001 From: "K. Lange" Date: Wed, 22 Feb 2023 10:47:02 +0900 Subject: [PATCH] mines: Port mines.py from toaru 1.2.x --- apps/mines.krk | 358 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 apps/mines.krk diff --git a/apps/mines.krk b/apps/mines.krk new file mode 100644 index 00000000..dddf0749 --- /dev/null +++ b/apps/mines.krk @@ -0,0 +1,358 @@ +#!/bin/kuroko +''' +Minesweeper game + +Originally written in Python and ported to Kuroko. +Visual design is based on the Gnome "Mines". +''' +import math +import random +from _yutani2 import (Font, rgb, decor_get_bounds, decor_render, decor_handle_event, decor_show_default_menu, + MenuBar, MenuEntry, MenuEntrySeparator, MenuList, MenuEntrySubmenu) +from yutani_mainloop import Window, AsyncMainloop + +let app_version = '2.0.0' +let mainloop = AsyncMainloop() + +def frgb(r,g,b): + '''RGB from float triplet''' + return rgb(int(255 * r),int(255 * g), int(255 * b)) + +def hsv_to_rgb(h,s,v): + '''HSV (radians and floats) to RGB (255/255/255) conversion.''' + let c = v * s + let hp = math.fmod(h, 2 * math.pi) + let x = c * (1.0 - abs(math.fmod(hp / 1.0472, 2) - 1.0)) + let m = v - c + let rp, gp, bp + if hp <= 1.0472: + rp = c; gp = x; bp = 0 + else if hp <= 2.0944: + rp = x; gp = c; bp = 0 + else if hp <= 3.1416: + rp = 0; gp = c; bp = x + else if hp <= 4.1888: + rp = 0; gp = x; bp = c + else if hp <= 5.2360: + rp = x; gp = 0; bp = c + else: + rp = c; gp = 0; bp = x + return rgb(int((rp + m) * 255), int((gp + m) * 255), int((bp + m) * 255)) + +def good_color(cnt): + '''Use neighbor count to pick a good color.''' + if cnt == 0: + return frgb(0.6,0.6,0.6) + let x = cnt / 8 + let h = 1.95 - x * 3.145 + return hsv_to_rgb(h,0.4,0.7) + +class MineButton: + def __init__(self, action, r, c, is_mine, neighbor_mines): + self.row, self.col = r, c + self.is_mine = is_mine + self.font = Font("sans-serif",13) + self.width = None + self.revealed = False + self.mines = neighbor_mines + self.flagged = False + self.hilight = 0 + self.action = action + self.text = "" + + def focus_enter(self): + self.hilight = 1 + + def focus_leave(self): + self.hilight = 0 + + def reveal(self): + if self.revealed: return + self.revealed = True + if self.is_mine: + self.text = "✸" # U+2738 + else if self.mines == 0: + self.text = "" + else: + self.text = str(self.mines) + + def set_flagged(self): + self.flagged = not self.flagged + + def draw(self, window, x, y, w, h): + self.font.size = int(h * 0.75) + if self.width != w: + self.x, self.y, self.width, self.height = x, y, w, h + let color = rgb(255,255,255) + if self.revealed and self.is_mine: + color = frgb(0.4,0.4,0.4) + else if self.revealed: + color = good_color(self.mines) + else if self.hilight == 1: + color = frgb(0.7,0.7,0.7) + else if self.hilight == 2: + color = frgb(0.3,0.3,0.3) + window.rect(x+1, y+1, w-2, h-2, color, radius=3) + let text = self.text + if not self.revealed and self.flagged: + text = '⚑' # U+2691 + if text: + self.font.draw_string(window,text,x+self.width//2-self.font.width(text)//2,int(y+self.height * 0.75)) + +def randrange(size): + return int(size * random.random()) + +class MinesWindow(Window): + + base_width = 400 + base_height = 440 + + def __init__(self): + let bounds = decor_get_bounds() + super().__init__(self.base_width + bounds['width'], self.base_height + bounds['height'], title="Mines", icon="mines", doublebuffer=True) + self.move(100,100) + self.button_width = {} + self.button_height = 0 + self.subtitleText = 'Hello, world.' + self.subtitleFont = Font('sans-serif.bold',17) + self.mb = MenuBar((("Game",'game'),("Help",'help'))) + + let _menu_Game = MenuList() + _menu_Game.insert(MenuEntrySubmenu('New Game', icon='new', action='new-game')) + _menu_Game.insert(MenuEntrySeparator()) + _menu_Game.insert(MenuEntry('Exit', lambda menu: self.close(), icon='exit')) + self.mb.insert('game',_menu_Game) + let _menu_Help = MenuList() + _menu_Help.insert(MenuEntry("About Mines", self.launch_about, icon='star')) + self.mb.insert('help',_menu_Help) + let _menu_New_Game = MenuList() + _menu_New_Game.insert(MenuEntry("9×9, 10 mines", lambda menu: self.basic_game())) + _menu_New_Game.insert(MenuEntry("16×16, 40 mines", lambda menu: self.new_game((16,40)))) + _menu_New_Game.insert(MenuEntry("20×20, 90 mines", lambda menu: self.new_game((20,90)))) + self.mb.insert('new-game',_menu_New_Game) + self.mb.callback = lambda x: self.draw() + + self.hover_widget = None + self.down_button = None + self.top_height = bounds['top_height'] + 24 + 40 + + self.modifiers = 0 + + self.basic_game() + + def launch_about(self, menu): + import os + os.system(f'about "About Mines" /usr/share/icons/48/mines.png "Mines {app_version}" "© 2017-2023 K. Lange\n-\nPart of ToaruOS, which is free software\nreleased under the NCSA/University of Illinois\nlicense.\n-\n%https://toaruos.org\n%https://github.com/klange/toaruos" &') + + def basic_game(self): + self.new_game((9,10)) + + def new_game(self, action): + + self.first_click = True + self.field_size, self.mine_count = action + self.subtitleText = f'There are {self.mine_count} mines.' + self.mines = [] + let i = 0 + while len(self.mines) < self.mine_count: + let x, y = randrange(self.field_size), randrange(self.field_size) + i += 1 + if (x, y) not in self.mines: + i = 0 + self.mines.append((x,y)) + if i > 50: + print("Board generation failed") + return + + def check_neighbors(r, c): + let n = [] + if r > 0: + if c > 0: + n.append((r-1,c-1)) + n.append((r-1,c)) + if c < self.field_size - 1: + n.append((r-1,c+1)) + if r < self.field_size - 1: + if c > 0: + n.append((r+1,c-1)) + n.append((r+1,c)) + if c < self.field_size - 1: + n.append((r+1,c+1)) + if c > 0: + n.append((r,c-1)) + if c < self.field_size - 1: + n.append((r,c+1)) + return n + + def check_neighbor_buttons(r,c): + return [self.buttons[x][y] for x, y in check_neighbors(r,c)] + + def mine_func(b): + let button = b + if self.first_click: + let i = 0 + while button.is_mine or button.mines: + if i > 30: + print("Failed to generate board") + return + self.new_game(action) + button = self.buttons[button.row][button.col] + i += 1 + self.first_click = False + if button.flagged: + return + if button.is_mine and not button.revealed: + self.subtitleText = "You lose." + for row in self.buttons: + for b in row: + b.reveal() + self.draw() + return + else: + if not button.revealed: + button.reveal() + if button.mines == 0: + let n = [x for x in check_neighbor_buttons(button.row,button.col) if not x.revealed] + while n: + b = n.pop() + b.reveal() + if b.mines == 0: + n.extend([x for x in check_neighbor_buttons(b.row,b.col) if not x.revealed and not x in n]) + self.check_win() + + self.buttons = [] + for row in range(self.field_size): + let r = [] + for col in range(self.field_size): + let is_mine = (row,col) in self.mines + let neighbor_mines = len([x for x in check_neighbors(row,col) if x in self.mines]) + r.append(MineButton(mine_func, row, col, is_mine, neighbor_mines)) + self.buttons.append(r) + + def check_win(self): + let buttons = [] + for row in self.buttons: + buttons.extend(row) + let n_flagged = len([x for x in buttons if x.flagged and not x.revealed]) + let n_revealed = len([x for x in buttons if x.revealed]) + if n_flagged == self.mine_count and n_revealed + n_flagged == self.field_size ** 2: + self.subtitleText = "You win." + for b in buttons: + b.reveal() + + def draw(self): + self.fill(rgb(204,204,204)) + let bounds = decor_get_bounds(self) + self.subtitleFont.draw_string(self,self.subtitleText, + (self.width - self.subtitleFont.width(self.subtitleText)) // 2, + bounds['top_height'] + 24 + 25) + let offset_x = bounds['left_width'] + let offset_y = self.top_height + self.button_height = (self.height - self.top_height - bounds['bottom_height']) // len(self.buttons) + let i = 0 + for row in self.buttons: + self.button_width[i] = (self.width - bounds['width']) // len(row) + for button in row: + if button: + button.draw(self,offset_x,offset_y,self.button_width[i],self.button_height) + offset_x += self.button_width[i] + offset_x = bounds['left_width'] + offset_y += self.button_height + i += 1 + self.mb.place(bounds['left_width'],bounds['top_height'],self.width-bounds['width'],self) + self.mb.render(self) + decor_render(self) + self.flip() + + def flag(self, button): + button.set_flagged() + self.check_win() + self.draw() + + def mouse_event(self, msg): + let decResponse = decor_handle_event(msg) + if decResponse == 2: + self.close() + return True + else if decResponse == 5: + decor_show_default_menu(self, self.x + msg.new_x, self.y + msg.new_y) + + let bounds = decor_get_bounds(self) + let x, y = msg.new_x, msg.new_y + let w, h = self.width, self.height + + if x >= 0 and x < w and y < self.top_height: + if self.hover_widget: + self.hover_widget.focus_leave() + self.hover_widget = None + self.draw() + self.mb.mouse_event(self, msg) + return + + let redraw = False + if self.down_button: + if msg.command == 2 or msg.command == 0: # RAISE or CLICK + if not (msg.buttons & 1): # BUTTON_LEFT + if x >= self.down_button.x and \ + x < self.down_button.x + self.down_button.width and \ + y >= self.down_button.y and \ + y < self.down_button.y + self.down_button.height: + self.down_button.focus_enter() + if self.modifiers & 1: # LEFT_CTRL + self.flag(self.down_button) + else: + self.down_button.action(self.down_button) + self.down_button = None + redraw = True + else: + self.down_button.focus_leave() + self.down_button = None + redraw = True + else: + # TOOD decors, menubar + if y >= self.top_height and y < h and x >= bounds['left_width'] and x < w: + let button + let xh = self.button_height * len(self.buttons) + let row = ((y - self.top_height) * len(self.buttons)) // xh + if row < len(self.buttons): + let xw = self.button_width[row] * len(self.buttons[row]) + let col = ((x - bounds['left_width']) * len(self.buttons[row])) // xw + if col < len(self.buttons[row]): + button = self.buttons[row][col] + else: + button = None + else: + button = None + if button is not self.hover_widget: + if button: + button.focus_enter() + redraw = True + if self.hover_widget: + self.hover_widget.focus_leave() + redraw = True + self.hover_widget = button + if msg.command == 3: # DOWN + if button: + button.hilight = 2 + self.down_button = button + redraw = True + else: + if self.hover_widget: + self.hover_widget.focus_leave() + redraw = True + self.hover_widget = None + if redraw: + self.draw() + + def keyboard_event(self, msg): + self.modifiers = msg.modifiers + + def close(self): + super().close() + mainloop.exit() + + +let window = MinesWindow() +window.draw() + +mainloop.run()