working game, needs some touch up

This commit is contained in:
2026-04-29 17:01:34 -04:00
commit 4117551198
5 changed files with 1075 additions and 0 deletions

802
shikaku.py Normal file
View File

@@ -0,0 +1,802 @@
#!/usr/bin/env python3
"""
Shikaku - Native NixOS puzzle game
Puzzle generator uses a guaranteed-solvable approach:
1. Partition the grid into rectangles
2. Place one clue number per rectangle
3. Player draws rectangles to match clues
"""
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gdk, GLib, Pango
import random
import sys
import time
from dataclasses import dataclass, field
from typing import Optional
# ─── Data structures ──────────────────────────────────────────────────────────
@dataclass
class Rect:
row: int
col: int
height: int
width: int
@property
def area(self):
return self.height * self.width
def cells(self):
for r in range(self.row, self.row + self.height):
for c in range(self.col, self.col + self.width):
yield (r, c)
@dataclass
class PlayerRect:
start_row: int
start_col: int
end_row: int
end_col: int
def normalized(self):
return PlayerRect(
min(self.start_row, self.end_row),
min(self.start_col, self.end_col),
max(self.start_row, self.end_row),
max(self.start_col, self.end_col),
)
def cells(self):
n = self.normalized()
for r in range(n.start_row, n.end_row + 1):
for c in range(n.start_col, n.end_col + 1):
yield (r, c)
def area(self):
n = self.normalized()
return (n.end_row - n.start_row + 1) * (n.end_col - n.start_col + 1)
def contains(self, row, col):
n = self.normalized()
return n.start_row <= row <= n.end_row and n.start_col <= col <= n.end_col
# ─── Puzzle generator ─────────────────────────────────────────────────────────
def generate_puzzle(size: int, seed: int = None) -> tuple[dict, list[Rect]]:
"""
Returns (clues, solution_rects).
clues: {(row, col): number} — the numbers shown on the grid
solution_rects: list of Rect — the correct partition
Algorithm:
- Repeatedly pick an uncovered cell and try to place a random rectangle
that covers it and only uncovered cells.
- Retry with a fresh random state if we get stuck.
"""
rng = random.Random(seed)
for _attempt in range(200):
grid = [[-1] * size for _ in range(size)] # -1 = uncovered
rects = []
failed = False
uncovered = [(r, c) for r in range(size) for c in range(size)]
rng.shuffle(uncovered)
while uncovered:
# pick first uncovered cell
start_r, start_c = uncovered[0]
if grid[start_r][start_c] != -1:
uncovered.pop(0)
continue
# gather candidate rectangles that:
# - include (start_r, start_c)
# - fit inside the grid
# - cover only uncovered cells
# - have area >= 2 (single-cell rectangles are not allowed)
candidates = []
for h in range(1, size - start_r + 1):
for w in range(1, size - start_c + 1):
if h == 1 and w == 1:
continue # minimum area is 2
# also try rectangles that start before the anchor
for ro in range(0, h):
for co in range(0, w):
r0 = start_r - ro
c0 = start_c - co
r1 = r0 + h - 1
c1 = c0 + w - 1
if r0 < 0 or c0 < 0 or r1 >= size or c1 >= size:
continue
if all(grid[r][c] == -1
for r in range(r0, r1 + 1)
for c in range(c0, c1 + 1)):
candidates.append(Rect(r0, c0, h, w))
if not candidates:
# This cell is isolated — no area-2+ rectangle fits.
# Absorb it into an orthogonally adjacent already-placed rectangle
# by expanding that rect to include this cell, if possible.
absorbed = False
neighbours = [
(start_r - 1, start_c),
(start_r + 1, start_c),
(start_r, start_c - 1),
(start_r, start_c + 1),
]
rng.shuffle(neighbours)
for nr, nc in neighbours:
if 0 <= nr < size and 0 <= nc < size and grid[nr][nc] != -1:
rid = grid[nr][nc]
old = rects[rid]
# Compute the bounding box of the existing rect + this cell
new_r0 = min(old.row, start_r)
new_c0 = min(old.col, start_c)
new_r1 = max(old.row + old.height - 1, start_r)
new_c1 = max(old.col + old.width - 1, start_c)
new_h = new_r1 - new_r0 + 1
new_w = new_c1 - new_c0 + 1
# Only absorb if the expanded bounding box covers only
# cells already owned by this rect or the isolated cell
expanded_cells = [
(r, c)
for r in range(new_r0, new_r1 + 1)
for c in range(new_c0, new_c1 + 1)
]
if all(grid[r][c] in (-1, rid) for r, c in expanded_cells):
# Re-assign all expanded cells to this rect
for r, c in expanded_cells:
grid[r][c] = rid
rects[rid] = Rect(new_r0, new_c0, new_h, new_w)
uncovered = [(r, c) for r, c in uncovered if grid[r][c] == -1]
absorbed = True
break
if not absorbed:
failed = True
break
continue # cell was absorbed into a neighbour, skip candidate placement
# Prefer smaller rectangles (keeps the puzzle interesting)
candidates.sort(key=lambda rect: (rect.area, rng.random()))
# pick from smaller end with some randomness
idx = min(int(rng.betavariate(1.2, 3.0) * len(candidates)), len(candidates) - 1)
chosen = candidates[idx]
rect_id = len(rects)
for r, c in chosen.cells():
grid[r][c] = rect_id
rects.append(chosen)
uncovered = [(r, c) for r, c in uncovered if grid[r][c] == -1]
if not failed and all(grid[r][c] != -1 for r in range(size) for c in range(size)):
# Place clue numbers — one per rectangle, at a random interior cell
clues = {}
for rect in rects:
cells = list(rect.cells())
cr, cc = rng.choice(cells)
clues[(cr, cc)] = rect.area
return clues, rects
raise RuntimeError("Puzzle generation failed after many attempts")
def verify_solution(size: int, player_rects: list[PlayerRect], clues: dict) -> tuple[bool, str]:
"""Check whether the player's rectangles constitute a valid solution."""
# Each cell must be covered exactly once
coverage = {}
for i, pr in enumerate(player_rects):
for cell in pr.cells():
if cell in coverage:
return False, f"Cells overlap between two of your rectangles"
coverage[cell] = i
total = size * size
if len(coverage) != total:
return False, f"Not all cells are covered ({len(coverage)}/{total})"
# Each rectangle must contain exactly one clue and match its number
for i, pr in enumerate(player_rects):
cells = set(pr.cells())
clues_inside = {cell: clues[cell] for cell in cells if cell in clues}
if len(clues_inside) == 0:
return False, "One of your rectangles contains no clue number"
if len(clues_inside) > 1:
return False, "One of your rectangles contains more than one clue number"
clue_val = next(iter(clues_inside.values()))
if pr.area() != clue_val:
return False, f"A rectangle contains {clue_val} but has {pr.area()} cells"
return True, "Solved!"
# ─── GTK Application ──────────────────────────────────────────────────────────
PADDING = 32
# Cell size and font scale down for larger grids so they fit comfortably on screen
CELL_SIZES = {
5: 56,
7: 52,
10: 46,
15: 36,
20: 28,
25: 24,
}
FONT_SIZES = {
5: 14,
7: 13,
10: 12,
15: 10,
20: 8,
25: 7,
}
def cell_size(grid_size: int) -> int:
return CELL_SIZES.get(grid_size, 40)
def font_size(grid_size: int) -> int:
return FONT_SIZES.get(grid_size, 10)
# Palette — deep ink & paper feel
BG_COLOR = (0.11, 0.10, 0.13, 1.0) # near-black background
PAPER_COLOR = (0.95, 0.93, 0.88, 1.0) # warm off-white grid
GRID_LINE = (0.55, 0.52, 0.48, 1.0) # warm grey lines
GRID_LINE_BOLD = (0.20, 0.18, 0.22, 1.0) # border
CLUE_COLOR = (0.10, 0.10, 0.12, 1.0) # near black text
RECT_COLORS = [
(0.36, 0.61, 0.84, 0.45), # blue
(0.42, 0.78, 0.60, 0.45), # green
(0.90, 0.55, 0.35, 0.45), # orange
(0.76, 0.45, 0.82, 0.45), # purple
(0.88, 0.76, 0.30, 0.45), # yellow
(0.45, 0.82, 0.85, 0.45), # cyan
(0.85, 0.42, 0.52, 0.45), # rose
(0.55, 0.72, 0.35, 0.45), # lime
]
RECT_BORDER = [
(0.20, 0.42, 0.70, 1.0),
(0.22, 0.60, 0.40, 1.0),
(0.75, 0.35, 0.12, 1.0),
(0.56, 0.25, 0.65, 1.0),
(0.70, 0.58, 0.08, 1.0),
(0.22, 0.62, 0.68, 1.0),
(0.68, 0.20, 0.32, 1.0),
(0.35, 0.54, 0.18, 1.0),
]
DRAG_COLOR = (0.50, 0.70, 0.95, 0.30)
DRAG_BORDER = (0.30, 0.55, 0.90, 0.90)
ERROR_COLOR = (0.90, 0.25, 0.25, 0.30)
ERROR_BORDER = (0.80, 0.10, 0.10, 0.90)
class ShikakuCanvas(Gtk.DrawingArea):
def __init__(self, game):
super().__init__()
self.game = game
self.set_draw_func(self._draw)
self.set_can_focus(True)
# Drag state
self._drag_start = None
self._drag_end = None
self._drag_preview: Optional[PlayerRect] = None
self._error_rects: set[int] = set()
# Callback fired after every board change — set by the window
self.on_board_changed = None
# Gestures
drag = Gtk.GestureDrag()
drag.connect("drag-begin", self._on_drag_begin)
drag.connect("drag-update", self._on_drag_update)
drag.connect("drag-end", self._on_drag_end)
self.add_controller(drag)
click = Gtk.GestureClick()
click.set_button(3) # right click
click.connect("pressed", self._on_right_click)
self.add_controller(click)
def _cell_at(self, x, y):
cs = cell_size(self.game.size)
col = int((x - PADDING) // cs)
row = int((y - PADDING) // cs)
size = self.game.size
if 0 <= row < size and 0 <= col < size:
return row, col
return None
def _on_drag_begin(self, gesture, x, y):
cell = self._cell_at(x, y)
if cell:
self._drag_start = cell
self._drag_end = cell
self._drag_preview = PlayerRect(*cell, *cell)
self.queue_draw()
def _on_drag_update(self, gesture, dx, dy):
if self._drag_start is None:
return
sx, sy = gesture.get_start_point()[1], gesture.get_start_point()[2]
cell = self._cell_at(sx + dx, sy + dy)
if cell:
self._drag_end = cell
sr, sc = self._drag_start
er, ec = cell
self._drag_preview = PlayerRect(sr, sc, er, ec)
self.queue_draw()
def _on_drag_end(self, gesture, dx, dy):
if self._drag_start and self._drag_end:
sr, sc = self._drag_start
er, ec = self._drag_end
pr = PlayerRect(sr, sc, er, ec)
self.game.place_rect(pr)
self._drag_start = None
self._drag_end = None
self._drag_preview = None
self.queue_draw()
if self.on_board_changed:
self.on_board_changed()
def _on_right_click(self, gesture, n, x, y):
cell = self._cell_at(x, y)
if cell:
self.game.remove_rect_at(*cell)
self.queue_draw()
if self.on_board_changed:
self.on_board_changed()
def mark_errors(self, error_indices: set[int]):
self._error_rects = error_indices
self.queue_draw()
def clear_errors(self):
self._error_rects = set()
self.queue_draw()
def _draw(self, area, cr, width, height):
size = self.game.size
clues = self.game.clues
player_rects = self.game.player_rects
cs = cell_size(size)
grid_w = size * cs
# Background
cr.set_source_rgba(*BG_COLOR)
cr.paint()
# Grid paper
cr.set_source_rgba(*PAPER_COLOR)
cr.rectangle(PADDING, PADDING, grid_w, grid_w)
cr.fill()
# Draw placed rectangles
for i, pr in enumerate(player_rects):
color_idx = i % len(RECT_COLORS)
n = pr.normalized()
rx = PADDING + n.start_col * cs
ry = PADDING + n.start_row * cs
rw = (n.end_col - n.start_col + 1) * cs
rh = (n.end_row - n.start_row + 1) * cs
if i in self._error_rects:
cr.set_source_rgba(*ERROR_COLOR)
else:
cr.set_source_rgba(*RECT_COLORS[color_idx])
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
cr.fill()
if i in self._error_rects:
cr.set_source_rgba(*ERROR_BORDER)
else:
cr.set_source_rgba(*RECT_BORDER[color_idx])
cr.set_line_width(2.5)
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
cr.stroke()
# Drag preview
if self._drag_preview:
n = self._drag_preview.normalized()
rx = PADDING + n.start_col * cs
ry = PADDING + n.start_row * cs
rw = (n.end_col - n.start_col + 1) * cs
rh = (n.end_row - n.start_row + 1) * cs
cr.set_source_rgba(*DRAG_COLOR)
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
cr.fill()
cr.set_source_rgba(*DRAG_BORDER)
cr.set_line_width(2.0)
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
cr.stroke()
# Grid lines
cr.set_line_width(0.8)
cr.set_source_rgba(*GRID_LINE)
for i in range(size + 1):
x = PADDING + i * cs
cr.move_to(x, PADDING)
cr.line_to(x, PADDING + grid_w)
cr.stroke()
cr.move_to(PADDING, x)
cr.line_to(PADDING + grid_w, x)
cr.stroke()
# Border
cr.set_line_width(3.0)
cr.set_source_rgba(*GRID_LINE_BOLD)
cr.rectangle(PADDING, PADDING, grid_w, grid_w)
cr.stroke()
# Clue numbers
layout = self.create_pango_layout("")
fs = font_size(size)
layout.set_font_description(Pango.FontDescription(f"Iosevka, Monospace Bold {fs}"))
for (r, c), num in clues.items():
layout.set_text(str(num), -1)
_, ext = layout.get_pixel_extents()
tx = PADDING + c * cs + cs // 2 - ext.width // 2
ty = PADDING + r * cs + cs // 2 - ext.height // 2
cr.set_source_rgba(*CLUE_COLOR)
cr.move_to(tx, ty)
from gi.repository import PangoCairo
PangoCairo.show_layout(cr, layout)
def _rounded_rect(cr, x, y, w, h, r):
cr.new_sub_path()
cr.arc(x + r, y + r, r, 3.14159, 1.5 * 3.14159)
cr.arc(x + w - r, y + r, r, 1.5 * 3.14159, 0)
cr.arc(x + w - r, y + h - r, r, 0, 0.5 * 3.14159)
cr.arc(x + r, y + h - r, r, 0.5 * 3.14159, 3.14159)
cr.close_path()
# ─── Game state ───────────────────────────────────────────────────────────────
class ShikakuGame:
def __init__(self, size: int):
self.size = size
self.clues: dict = {}
self.solution: list[Rect] = []
self.player_rects: list[PlayerRect] = []
self.start_time: float = 0
self.elapsed: float = 0
self.solved = False
self.new_puzzle()
def new_puzzle(self, seed=None):
self.clues, self.solution = generate_puzzle(self.size, seed or random.randint(0, 2**31))
self.player_rects = []
self.start_time = time.time()
self.elapsed = 0
self.solved = False
def place_rect(self, pr: PlayerRect):
"""Add a rectangle, removing any existing ones that overlap."""
new_cells = set(pr.cells())
self.player_rects = [r for r in self.player_rects
if not set(r.cells()) & new_cells]
self.player_rects.append(pr)
def remove_rect_at(self, row, col):
self.player_rects = [r for r in self.player_rects
if (row, col) not in set(r.cells())]
def check_solution(self) -> tuple[bool, str, set[int]]:
ok, msg = verify_solution(self.size, self.player_rects, self.clues)
error_indices = set()
if not ok:
# Mark rectangles that are invalid
for i, pr in enumerate(self.player_rects):
cells = set(pr.cells())
clues_inside = {cell: self.clues[cell] for cell in cells if cell in self.clues}
if len(clues_inside) != 1 or pr.area() != next(iter(clues_inside.values()), -1):
error_indices.add(i)
else:
self.solved = True
self.elapsed = time.time() - self.start_time
return ok, msg, error_indices
# ─── Main window ──────────────────────────────────────────────────────────────
class ShikakuWindow(Gtk.ApplicationWindow):
def __init__(self, app):
super().__init__(application=app, title="Shikaku")
self.set_resizable(True)
self._current_size = 5
self.game = ShikakuGame(self._current_size)
self._build_ui()
self._update_canvas_size()
self._start_timer()
def _build_ui(self):
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
self.set_child(outer)
# ── Top bar ──
topbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
topbar.set_margin_top(14)
topbar.set_margin_bottom(10)
topbar.set_margin_start(20)
topbar.set_margin_end(20)
outer.append(topbar)
title = Gtk.Label(label="SHIKAKU")
title.add_css_class("title-label")
topbar.append(title)
spacer = Gtk.Box()
spacer.set_hexpand(True)
topbar.append(spacer)
self._timer_label = Gtk.Label(label="0:00")
self._timer_label.add_css_class("timer-label")
topbar.append(self._timer_label)
# ── Size buttons ──
size_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
size_bar.set_halign(Gtk.Align.CENTER)
size_bar.set_margin_bottom(10)
outer.append(size_bar)
sizes_label = Gtk.Label(label="Grid:")
sizes_label.add_css_class("sizes-label")
size_bar.append(sizes_label)
self._size_buttons = {}
for sz in [5, 7, 10, 15, 20, 25]:
btn = Gtk.Button(label=f"{sz}×{sz}")
btn.add_css_class("size-btn")
btn.connect("clicked", self._on_size_clicked, sz)
size_bar.append(btn)
self._size_buttons[sz] = btn
self._update_size_buttons()
# ── Canvas inside a scrolled window for large grids ──
scroll = Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scroll.set_hexpand(True)
scroll.set_vexpand(True)
self.canvas = ShikakuCanvas(self.game)
self.canvas.set_halign(Gtk.Align.CENTER)
self.canvas.set_valign(Gtk.Align.CENTER)
self.canvas.on_board_changed = self._trigger_auto_check
scroll.set_child(self.canvas)
outer.append(scroll)
# ── Bottom bar ──
botbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
botbar.set_halign(Gtk.Align.CENTER)
botbar.set_margin_top(14)
botbar.set_margin_bottom(18)
outer.append(botbar)
new_btn = Gtk.Button(label="New Puzzle")
new_btn.add_css_class("action-btn")
new_btn.connect("clicked", self._on_new)
botbar.append(new_btn)
check_btn = Gtk.Button(label="Check")
check_btn.add_css_class("action-btn")
check_btn.connect("clicked", self._on_check)
botbar.append(check_btn)
clear_btn = Gtk.Button(label="Clear")
clear_btn.add_css_class("action-btn-ghost")
clear_btn.connect("clicked", self._on_clear)
botbar.append(clear_btn)
self._status_label = Gtk.Label(label="Draw rectangles around each number — right-click to erase")
self._status_label.add_css_class("status-label")
self._status_label.set_margin_top(4)
self._status_label.set_margin_bottom(8)
outer.append(self._status_label)
# ── CSS ──
css = Gtk.CssProvider()
css.load_from_string(self._css())
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def _css(self):
return """
window {
background-color: #1c1a21;
}
.title-label {
font-family: "Iosevka", "Monospace";
font-size: 22px;
font-weight: 900;
letter-spacing: 6px;
color: #e8e4dc;
}
.timer-label {
font-family: "Iosevka", "Monospace";
font-size: 18px;
color: #8a8070;
font-weight: 600;
letter-spacing: 2px;
}
.sizes-label {
font-family: "Iosevka", "Monospace";
font-size: 12px;
color: #6a6060;
letter-spacing: 2px;
}
.size-btn {
font-family: "Iosevka", "Monospace";
font-size: 12px;
padding: 4px 12px;
border-radius: 4px;
background: #2a2730;
color: #9a9090;
border: 1px solid #3a3540;
}
.size-btn:hover {
background: #3a3540;
color: #d0c8c0;
}
.size-btn-active {
background: #4a4060;
color: #e8e0d8;
border: 1px solid #7a6090;
}
.action-btn {
font-family: "Iosevka", "Monospace";
font-size: 13px;
font-weight: 700;
letter-spacing: 1px;
padding: 8px 22px;
border-radius: 5px;
background: #5a4880;
color: #e8e0ff;
border: none;
}
.action-btn:hover {
background: #7060a0;
}
.action-btn-ghost {
font-family: "Iosevka", "Monospace";
font-size: 13px;
letter-spacing: 1px;
padding: 8px 22px;
border-radius: 5px;
background: transparent;
color: #6a6070;
border: 1px solid #3a3540;
}
.action-btn-ghost:hover {
color: #a09090;
border-color: #5a5060;
}
.status-label {
font-family: "Iosevka", "Monospace";
font-size: 11px;
color: #5a5560;
letter-spacing: 1px;
}
.status-ok {
color: #60c080;
}
.status-err {
color: #c06060;
}
"""
def _update_size_buttons(self):
for sz, btn in self._size_buttons.items():
btn.remove_css_class("size-btn-active")
if sz == self._current_size:
btn.add_css_class("size-btn-active")
def _update_canvas_size(self):
sz = self.game.size
cs = cell_size(sz)
total = sz * cs + 2 * PADDING
self.canvas.set_content_width(total)
self.canvas.set_content_height(total)
def _on_size_clicked(self, btn, size):
self._current_size = size
self.game = ShikakuGame(size)
self.canvas.game = self.game
self.canvas.on_board_changed = self._trigger_auto_check
self.canvas._error_rects = set()
self._update_canvas_size()
self._update_size_buttons()
self._status_label.remove_css_class("status-ok")
self._status_label.remove_css_class("status-err")
self._status_label.set_text("Draw rectangles around each number — right-click to erase")
self.canvas.queue_draw()
def _on_new(self, btn):
self.game.new_puzzle()
self.canvas._error_rects = set()
self._status_label.remove_css_class("status-ok")
self._status_label.remove_css_class("status-err")
self._status_label.set_text("Draw rectangles around each number — right-click to erase")
self.canvas.queue_draw()
def _trigger_auto_check(self):
"""Silently check after every move; announce only on a correct solution."""
if self.game.solved:
return
ok, msg, errors = self.game.check_solution()
if ok:
self.canvas.clear_errors()
mins = int(self.game.elapsed) // 60
secs = int(self.game.elapsed) % 60
self._status_label.remove_css_class("status-err")
self._status_label.add_css_class("status-ok")
self._status_label.set_text(f"✓ Solved in {mins}:{secs:02d}!")
else:
# Don't highlight errors on auto-check — only the manual Check button does that
self.canvas.clear_errors()
def _on_check(self, btn):
ok, msg, errors = self.game.check_solution()
self.canvas.mark_errors(errors)
self._status_label.remove_css_class("status-ok")
self._status_label.remove_css_class("status-err")
if ok:
mins = int(self.game.elapsed) // 60
secs = int(self.game.elapsed) % 60
self._status_label.set_text(f"✓ Solved in {mins}:{secs:02d}!")
self._status_label.add_css_class("status-ok")
else:
self._status_label.set_text(f"{msg}")
self._status_label.add_css_class("status-err")
def _on_clear(self, btn):
self.game.player_rects = []
self.canvas.clear_errors()
self._status_label.remove_css_class("status-ok")
self._status_label.remove_css_class("status-err")
self._status_label.set_text("Cleared")
self.canvas.queue_draw()
def _start_timer(self):
GLib.timeout_add(1000, self._tick)
def _tick(self):
if not self.game.solved:
elapsed = int(time.time() - self.game.start_time)
m = elapsed // 60
s = elapsed % 60
self._timer_label.set_text(f"{m}:{s:02d}")
return True # keep ticking
# ─── Application entry ────────────────────────────────────────────────────────
class ShikakuApp(Gtk.Application):
def __init__(self):
super().__init__(application_id="com.shikaku.puzzle")
def do_activate(self):
win = ShikakuWindow(self)
win.present()
def main():
app = ShikakuApp()
sys.exit(app.run(sys.argv))
if __name__ == "__main__":
main()