#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib
import cairo
import math
import getpass
import os
import json
import random
STATS_FILE = os.path.expanduser("~/.cache/FindNumGame/stats.json")
def load_stats():
default = {"games": 0, "total_accuracy": 0.0}
if os.path.exists(STATS_FILE):
try:
with open(STATS_FILE, "r") as f:
data = json.load(f)
if "games" in data and "total_accuracy" in data:
return data
except:
pass
return default
def save_stats(stats):
os.makedirs(os.path.dirname(STATS_FILE), exist_ok=True)
with open(STATS_FILE, "w") as f:
json.dump(stats, f, indent=2)
def update_stats(accuracy):
stats = load_stats()
stats["games"] += 1
stats["total_accuracy"] += accuracy
save_stats(stats)
return stats
def get_average_accuracy(stats=None):
if stats is None:
stats = load_stats()
if stats["games"] == 0:
return 0.0
return stats["total_accuracy"] / stats["games"]
class WelcomeScreen(Gtk.Box):
def __init__(self, start_callback):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=20)
self.set_halign(Gtk.Align.CENTER)
self.set_valign(Gtk.Align.CENTER)
self.set_margin_top(40)
self.set_margin_bottom(40)
self.set_margin_start(40)
self.set_margin_end(40)
self.username = getpass.getuser()
self.text = f"Приветствую, {self.username}, не желаешь"
self.start_callback = start_callback
self.phase = 0.0
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
row.set_halign(Gtk.Align.CENTER)
self.drawing_area = Gtk.DrawingArea()
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
cr = cairo.Context(surface)
cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(34)
ext = cr.text_extents(self.text)
ascent, descent, _, _, _ = cr.font_extents()
text_width = int(ext.width + 7)
text_height = int(ascent + descent + 20)
self.drawing_area.set_size_request(text_width, text_height)
self.drawing_area.connect("draw", self.on_draw)
row.pack_start(self.drawing_area, False, False, 0)
self.start_button = Gtk.Button(label="Начать игру")
self.start_button.connect("clicked", lambda w: self.start_callback())
self.start_button.get_style_context().add_class("start-button")
self.start_button.set_relief(Gtk.ReliefStyle.NONE)
self.start_button.set_valign(Gtk.Align.CENTER)
row.pack_start(self.start_button, False, False, 0)
q = Gtk.Label()
q.set_markup("?")
q.set_valign(Gtk.Align.CENTER)
row.pack_start(q, False, False, 0)
self.pack_start(row, False, False, 0)
self.stats_label = Gtk.Label()
self.stats_label.set_halign(Gtk.Align.CENTER)
self.pack_start(self.stats_label, False, False, 10)
self.update_stats_display()
dev = Gtk.Label()
dev.set_markup("dev by jezzi • gtk3 • arch linux")
self.pack_start(dev, False, False, 5)
GLib.timeout_add(50, self.on_timeout)
def update_stats_display(self):
stats = load_stats()
avg = get_average_accuracy(stats)
self.stats_label.set_markup(
""
f"📊 Сыграно игр: {stats['games']} "
f"🎯 Средняя точность: {avg:.1f}%"
""
)
def on_draw(self, widget, cr):
cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(34)
ext = cr.text_extents(self.text)
ascent, descent, _, _, _ = cr.font_extents()
x = 0
y = ascent + 5
grad = cairo.LinearGradient(0, 0, ext.width, 0)
p = self.phase
grad.add_color_stop_rgb(0.0, 0.6 + 0.4 * math.sin(p), 0.2, 0.9)
grad.add_color_stop_rgb(0.5, 0.2, 0.5 + 0.4 * math.cos(p), 0.9)
grad.add_color_stop_rgb(1.0, 0.9, 0.3, 0.6 + 0.4 * math.sin(p))
cr.set_source(grad)
cr.move_to(x, y)
cr.show_text(self.text)
def on_timeout(self):
self.phase += 0.08
self.drawing_area.queue_draw()
return True
class MainApp(Gtk.Window):
def __init__(self):
super().__init__(title="Найди меня в числах")
self.set_default_size(900, 550)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_resizable(True)
self.phase = 0.0
GLib.timeout_add(50, self.on_game_timeout) # таймер для обновления phase
# CSS
css = b"""
window { background-color: #09090f; }
.start-button {
background-image: linear-gradient(
90deg,
#6d28d9,
#7c3aed,
#8b5cf6
);
color: white;
border: 1px solid #9f67ff;
border-radius: 18px;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
box-shadow:
0 0 8px rgba(124,58,237,0.4);
transition: 200ms ease;
}
.start-button:hover {
background-image: linear-gradient(
90deg,
#7c3aed,
#8b5cf6,
#a855f7
);
border-color: #c084fc;
box-shadow:
0 0 16px rgba(168,85,247,0.8),
0 0 32px rgba(168,85,247,0.4);
color: white;
}
.start-button:active {
background-image: linear-gradient(
90deg,
#5b21b6,
#6d28d9
);
}
.stats-label {
color: #a1a1aa;
font-size: 14px;
}
.title-part1 { color: #c084fc; font-size: 36px; font-weight: bold; }
.title-part2 { color: #60a5fa; font-size: 36px; font-weight: bold; }
.number { color: #e5e7eb; font-size: 80px; font-weight: bold; }
"""
provider = Gtk.CssProvider()
provider.load_from_data(css)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
self.stack = Gtk.Stack()
self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
self.stack.set_transition_duration(300)
self.add(self.stack)
self.welcome_screen = WelcomeScreen(self.on_start_game)
self.stack.add_named(self.welcome_screen, "welcome")
self.game_box = self.create_game_box()
self.stack.add_named(self.game_box, "game")
self.stack.set_visible_child_name("welcome")
self.update_stats_display()
def on_game_timeout(self):
self.phase += 0.08
if hasattr(self, "game_title_area"):
self.game_title_area.queue_draw()
return True
def update_stats_display(self):
self.welcome_screen.update_stats_display()
def on_start_game(self):
self.stack.set_visible_child_name("game")
self.reset_game_state()
self.drawing_area.queue_draw()
def create_game_box(self):
self.min_value = 0
self.max_value = random.randint(50, 10000)
self.target_number = random.randint(self.min_value, self.max_value)
self.user_click_x = None
self.correct_x = None
self.game_active = True
self.margin = 60
self.round_finished = False
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
main_box.set_margin_top(40)
main_box.set_margin_bottom(40)
main_box.set_margin_start(30)
main_box.set_margin_end(30)
self.game_title_area = Gtk.DrawingArea()
self.game_title_area.set_size_request(-1, 60)
self.game_title_area.connect("draw", self.on_game_title_draw)
main_box.pack_start(self.game_title_area, False, False, 0)
self.target_label = Gtk.Label()
self.target_label.set_text(str(self.target_number))
self.target_label.get_style_context().add_class("number")
main_box.pack_start(self.target_label, False, False, 10)
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_hexpand(True)
self.drawing_area.set_size_request(-1, 150)
self.drawing_area.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.drawing_area.connect("draw", self.on_draw)
self.drawing_area.connect("button-press-event", self.on_click)
main_box.pack_start(self.drawing_area, False, False, 0)
self.reset_button = Gtk.Button(label="Заново")
self.reset_button.set_visible(False)
self.reset_button.connect("clicked", self.on_reset)
main_box.pack_start(self.reset_button, False, False, 20)
return main_box
def reset_game_state(self):
self.max_value = random.randint(50, 10000)
self.target_number = random.randint(self.min_value, self.max_value)
self.user_click_x = None
self.correct_x = None
self.game_active = True
self.round_finished = False
self.target_label.set_text(str(self.target_number))
self.reset_button.set_visible(False)
def on_game_title_draw(self, widget, cr):
text = "Найди меня в числах"
cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(36)
ext = cr.text_extents(text)
W = widget.get_allocated_width()
H = widget.get_allocated_height()
x = (W - ext.width)/2
y = (H + ext.height)/2
grad = cairo.LinearGradient(0,0,ext.width,0)
p = self.phase
grad.add_color_stop_rgb(0, 0.6 + 0.4*math.sin(p), 0.2, 0.9)
grad.add_color_stop_rgb(0.5, 0.2, 0.5 + 0.4*math.cos(p), 0.9)
grad.add_color_stop_rgb(1, 0.9, 0.3, 0.6 + 0.4*math.sin(p))
cr.set_source(grad)
cr.move_to(x,y)
cr.show_text(text)
def calculate_correct_position(self, start_x, end_x):
normalized = (self.target_number - self.min_value) / (self.max_value - self.min_value)
return start_x + (end_x - start_x) * normalized
def on_draw(self, widget, cr):
width = widget.get_allocated_width()
height = widget.get_allocated_height()
y_line = height / 2
start_x = self.margin
end_x = width - self.margin
cr.set_source_rgb(0.5, 0.5, 0.6)
cr.set_line_width(3)
cr.move_to(start_x, y_line)
cr.line_to(end_x, y_line)
cr.stroke()
cr.set_source_rgb(0.6, 0.6, 0.7)
cr.arc(end_x, y_line, 6, 0, 2 * math.pi)
cr.fill()
cr.set_source_rgb(0.6, 0.6, 0.7)
cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(22)
text = str(self.max_value)
(x_advance, _) = cr.text_extents(text)[2:4]
text_x = end_x - x_advance / 2
text_y = y_line + 40
cr.move_to(text_x, text_y)
cr.show_text(text)
if self.user_click_x is not None and not self.game_active:
cr.set_source_rgb(0.95, 0.3, 0.45)
cr.set_line_width(3)
cr.move_to(self.user_click_x, y_line - 15)
cr.line_to(self.user_click_x, y_line + 15)
cr.stroke()
cr.set_source_rgb(0.95, 0.3, 0.45)
cr.set_font_size(12)
text = "YOU"
(x_adv, _) = cr.text_extents(text)[2:4]
cr.move_to(self.user_click_x - x_adv/2, y_line - 20)
cr.show_text(text)
if self.correct_x is not None and not self.game_active:
cr.set_source_rgb(0.3, 0.9, 0.5)
cr.set_line_width(3)
cr.move_to(self.correct_x, y_line - 15)
cr.line_to(self.correct_x, y_line + 15)
cr.stroke()
cr.set_source_rgb(0.3, 0.9, 0.5)
cr.set_font_size(12)
text = "ANSWER"
(x_adv, _) = cr.text_extents(text)[2:4]
cr.move_to(self.correct_x - x_adv/2, y_line - 20)
cr.show_text(text)
def on_click(self, widget, event):
if not self.game_active:
return
width = widget.get_allocated_width()
start_x = self.margin
end_x = width - self.margin
click_x = event.x
if click_x < start_x or click_x > end_x:
return
self.user_click_x = click_x
self.correct_x = self.calculate_correct_position(start_x, end_x)
distance = abs(click_x - self.correct_x)
max_distance = end_x - start_x
accuracy = max(0, 100 - (distance / max_distance * 100))
accuracy = round(accuracy, 1)
if not self.round_finished:
update_stats(accuracy)
self.round_finished = True
self.target_number = accuracy
self.target_label.set_text(str(self.target_number))
self.game_active = False
self.reset_button.set_visible(True)
widget.queue_draw()
def on_reset(self, widget):
self.reset_game_state()
self.drawing_area.queue_draw()
def main():
app = MainApp()
app.connect("destroy", Gtk.main_quit)
app.show_all()
Gtk.main()
if __name__ == "__main__":
main()