1-
21import random
32import hashlib
43from PyQt5 .QtWidgets import (
54 QWidget , QVBoxLayout , QLabel , QPushButton , QGraphicsScene , QGraphicsView ,
6- QHBoxLayout , QMessageBox
5+ QHBoxLayout , QMessageBox , QComboBox , QTableWidget , QTableWidgetItem , QTextEdit
76)
8- from PyQt5 .QtGui import QFont , QBrush , QColor , QPen
7+ from PyQt5 .QtGui import QFont , QBrush , QColor , QPen , QTextCursor
98from PyQt5 .QtCore import Qt , QPointF , QTimer
109
1110from ui .node_item import NodeItem
1211
13-
1412class BaseLevel (QWidget ):
15- def __init__ (self , level_name , parent_selector , max_rounds = 3 ):
13+ def __init__ (self , level_name , parent_selector , max_rounds = 3 , auto_mode = False , auto_rounds = 5 ):
1614 super ().__init__ ()
1715 self .level_name = level_name
1816 self .parent_selector = parent_selector
1917 self .max_rounds = max_rounds
18+ self .auto_mode = auto_mode
19+ self .auto_rounds = auto_rounds
2020 self .rounds = 0
2121 self .committed = False
2222
23+ self .valid_proofs = 0
24+ self .invalid_proofs = 0
25+ self .demo_phase = True
26+ self .auto_total_rounds = auto_rounds * 2
27+
2328 self .node_colors = {}
2429 self .commitments = {}
2530 self .nonces = {}
2631 self .nodes = {}
2732 self .edges = []
2833
2934 self .setWindowTitle (f"{ level_name } - ZKP Game" )
30- self .setGeometry (100 , 100 , 900 , 650 )
35+ self .setGeometry (100 , 100 , 900 , 700 )
3136 self .layout = QVBoxLayout ()
3237 self .setLayout (self .layout )
3338
34- self .narration = QLabel (f"👩\u200d 💼 Prover: Color nodes secretly to convince the Verifier!" )
35- self .narration .setFont (QFont ("Arial" , 12 ))
39+ self .narration = QLabel (f"👩💼 Prover: Color nodes secretly to convince the Verifier!" )
40+ self .narration .setFont (QFont ("Arial" , 14 , QFont .Bold ))
41+ self .narration .setStyleSheet ("color: white;" )
3642 self .layout .addWidget (self .narration )
3743
44+ self .log_box = QTextEdit ()
45+ self .log_box .setReadOnly (True )
46+ self .log_box .setStyleSheet ("background-color: #1c1c1c; color: white; font-weight: bold; font-size: 13px;" )
47+ self .log_box .setFont (QFont ("Consolas" , 12 ))
48+ self .log_box .setFixedHeight (150 )
49+ self .layout .addWidget (self .log_box )
50+
3851 self .scene = QGraphicsScene ()
3952 self .view = QGraphicsView (self .scene )
4053 self .layout .addWidget (self .view )
@@ -53,19 +66,14 @@ def __init__(self, level_name, parent_selector, max_rounds=3):
5366 self .layout .addLayout (self .buttons_layout )
5467
5568 self .show_education_modal ()
56-
57- def show_education_modal (self ):
58- QMessageBox .information (
59- self ,
60- "🔐 About Zero-Knowledge Rounds" ,
61- "Each round simulates one ZKP exchange.\n \n "
62- "You can only commit once per round, and reveal one edge.\n "
63- "The verifier learns nothing beyond this — try not to leak!\n \n "
64- "You’ll repeat this 3 times to earn trust."
65- )
66-
69+ if self .auto_mode :
70+ QTimer .singleShot (1000 , self .auto_run_verification )
6771 def update_narration (self , text ):
6872 self .narration .setText (text )
73+ def log (self , message ):
74+ self .log_box .append (f"➡️ { message } " )
75+ self .log_box .moveCursor (QTextCursor .End )
76+
6977
7078 def create_graph (self , node_positions , edge_list ):
7179 for i , pos in enumerate (node_positions ):
@@ -81,7 +89,20 @@ def create_graph(self, node_positions, edge_list):
8189
8290 self .view .setScene (self .scene )
8391
84- def commit_colors (self ):
92+ def randomly_color_nodes (self , allow_collision = False ):
93+ palette = ["#e74c3c" , "#2ecc71" , "#f1c40f" , "#3498db" ]
94+ self .node_colors .clear ()
95+ used = {}
96+ for node_id , node in self .nodes .items ():
97+ if not allow_collision :
98+ color = palette [node_id % len (palette )]
99+ else :
100+ color = random .choice (palette )
101+ self .node_colors [node_id ] = color
102+ node .setBrush (QBrush (QColor (color )))
103+ self .log (f"Node { node_id } colored { color } " )
104+
105+ def commit_colors (self , auto_trigger = False ):
85106 missing = [nid for nid in self .nodes if nid not in self .node_colors ]
86107 if missing :
87108 self .update_narration (f"⚠️ Please color all nodes: Missing { missing } " )
@@ -97,13 +118,16 @@ def commit_colors(self):
97118 self .nonces [node_id ] = nonce
98119 self .nodes [node_id ].locked = True
99120 self .nodes [node_id ].setBrush (QBrush (Qt .gray ))
121+ self .log (f"Node { node_id } locked with hidden commitment" )
100122
101123 self .committed = True
102- self .update_narration ("🔒 Commitments made. Verifier, choose an edge to challenge!" )
124+ if not auto_trigger :
125+ self .update_narration ("🔒 Commitments made. Verifier, choose an edge to challenge!" )
103126
104- def challenge_edge_once (self ):
127+ def challenge_edge_once (self , auto = False ):
105128 if not self .committed :
106- QMessageBox .warning (self , "⚠️ Commit First" , "You must commit before challenging." )
129+ if not auto :
130+ QMessageBox .warning (self , "⚠️ Commit First" , "You must commit before challenging." )
107131 return
108132
109133 edge = random .choice (self .edges )
@@ -113,22 +137,53 @@ def challenge_edge_once(self):
113137
114138 self .nodes [node1 ].setBrush (QBrush (QColor (color1 )))
115139 self .nodes [node2 ].setBrush (QBrush (QColor (color2 )))
140+ self .log (f"Verifier opens edge { edge } : Node { node1 } = { color1 } , Node { node2 } = { color2 } " )
116141
117142 if color1 == color2 :
143+ self .invalid_proofs += 1
118144 result = f"❌ Verifier: Edge { edge } has same colors. Proof fails!"
119145 self .update_narration (result )
120- QTimer .singleShot (1500 , self .reject_proof )
121- return
122146 else :
147+ self .valid_proofs += 1
123148 result = f"✅ Verifier: Edge { edge } looks good."
124- self .rounds += 1
149+ self .rounds += 1
125150
126- self .update_narration (result + f" Round { self .rounds } /{ self .max_rounds } " )
151+ self .update_narration (result + f" Round { self .rounds } /{ self .auto_total_rounds } " )
127152
128- if self .rounds < self .max_rounds :
129- QTimer .singleShot (1500 , self .prompt_next_round )
130- else :
131- QTimer .singleShot (1500 , self .finish_level )
153+ if not self .auto_mode :
154+ if self .rounds < self .max_rounds :
155+ QTimer .singleShot (1500 , self .prompt_next_round )
156+ else :
157+ QTimer .singleShot (1500 , self .finish_level )
158+
159+ def auto_run_verification (self ):
160+ if self .rounds >= self .auto_total_rounds :
161+ QTimer .singleShot (1000 , self .show_final_report )
162+ return
163+
164+ allow_collision = False if self .rounds < self .auto_rounds else True
165+ self .randomly_color_nodes (allow_collision = allow_collision )
166+ self .update_narration (f"🎨 Round { self .rounds + 1 } : Auto-coloring graph..." )
167+ QTimer .singleShot (1000 , self .auto_commit_step )
168+
169+ def auto_commit_step (self ):
170+ self .commit_colors (auto_trigger = True )
171+ QTimer .singleShot (1000 , self .auto_challenge_step )
172+
173+ def auto_challenge_step (self ):
174+ self .challenge_edge_once (auto = True )
175+ QTimer .singleShot (1500 , self .auto_run_verification )
176+
177+ def show_final_report (self ):
178+ total = self .valid_proofs + self .invalid_proofs
179+ success_rate = (self .valid_proofs / total ) * 100 if total > 0 else 0
180+
181+ msg = QMessageBox ()
182+ msg .setWindowTitle ("📊 Simulation Summary" )
183+ msg .setText (f"✅ Valid Proofs: { self .valid_proofs } \n ❌ Invalid Proofs: { self .invalid_proofs } \n \n "
184+ f"🎯 Success Probability: { success_rate :.2f} %" )
185+ msg .exec_ ()
186+ self .finish_level ()
132187
133188 def prompt_next_round (self ):
134189 QMessageBox .information (
@@ -139,10 +194,10 @@ def prompt_next_round(self):
139194 self .reset_game (preserve_round = True )
140195
141196 def finish_level (self ):
142- QMessageBox .information (self , "🎉 Success" , "Verifier: I’m convinced! You passed all rounds." )
143197 self .parent_selector .update_trust_points (points_earned = 3 )
144198 self .close ()
145199 self .parent_selector .show ()
200+
146201 def reject_proof (self ):
147202 QMessageBox .critical (self , "❌ Proof Rejected" , "Verifier: The proof failed. I cannot be convinced." )
148203 self .close ()
@@ -160,6 +215,9 @@ def reset_game(self, preserve_round=False):
160215
161216 if not preserve_round :
162217 self .rounds = 0
218+ self .valid_proofs = 0
219+ self .invalid_proofs = 0
163220 self .update_narration ("🔁 Game reset. Recolor and start again!" )
221+ self .log_box .clear ()
164222 else :
165- self .update_narration ("🎨 Prover: Please recolor the graph for the next round." )
223+ self .update_narration ("🎨 Prover: Please recolor the graph for the next round." )
0 commit comments