#!/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()