Files
ctf/2025/lake/misc/wordler/tmp.py
2025-11-28 23:48:04 +01:00

216 lines
7.7 KiB
Python

import re
import os
from collections import Counter, defaultdict
import itertools
def parse_ansi_feedback(raw_bytes):
"""
Parses the raw bytes containing ANSI codes to extract letters and their colors.
Returns a list of word_segments.
Each segment is a list of tuples: (color_code, char)
"""
try:
decoded = raw_bytes.decode('utf-8')
except UnicodeDecodeError:
decoded = raw_bytes.decode('utf-8', errors='ignore')
print("--- Raw Decoded String ---")
print(decoded)
print("--------------------------")
# Split into word chunks based on the underscore
raw_segments = decoded.strip().split('_')
parsed_segments = []
# Regex to find ANSI code and the character immediately following
# Matches: \x1b[93mU (captures '93' and 'U')
ansi_pattern = re.compile(r'\x1b\[(\d+)m([A-Z])')
for raw_seg in raw_segments:
matches = ansi_pattern.findall(raw_seg)
if matches:
# matches is a list of ('93', 'U'), ('90', 'V'), etc.
parsed_segments.append(matches)
return parsed_segments
def analyze_global_constraints(all_segments):
"""
Derives global letter counts from all segments combined based on the rule:
- Green/Yellow count contributes to the total.
- Presence of Grey implies the count is EXACTLY (Green + Yellow).
- Absence of Grey implies the count is AT LEAST (Green + Yellow).
"""
# Flatten all entries to count global occurrences in the guess
all_entries = [item for seg in all_segments for item in seg]
char_data = defaultdict(lambda: {'92': 0, '93': 0, '90': 0})
for color, char in all_entries:
char_data[char.lower()][color] += 1
global_constraints = {}
for char, counts in char_data.items():
greens = counts['92']
yellows = counts['93']
greys = counts['90']
required = greens + yellows
if greys > 0:
# We hit the limit (Grey exists), so the total count in solution is exactly 'required'
global_constraints[char] = {'min': required, 'max': required}
else:
# No Greys seen, so there could be more. Count is at least 'required'
global_constraints[char] = {'min': required, 'max': float('inf')}
return global_constraints
def generate_local_constraints(segment_data):
"""
Generates positional constraints for a single word slot.
"""
constraints = {
'length': len(segment_data),
'fixed': {}, # index: char (Green)
'not_at_pos': [], # list of (index, char) (Yellow or Grey)
}
for i, (color, char) in enumerate(segment_data):
char = char.lower()
if color == '92': # Green
constraints['fixed'][i] = char
elif color == '93': # Yellow
constraints['not_at_pos'].append((i, char))
elif color == '90': # Grey
# Grey means "not a match at this position".
constraints['not_at_pos'].append((i, char))
return constraints
def get_candidates_for_slots(wordlist_path, segments, global_constraints):
"""
1. Filters wordlist for each slot based on local constraints.
2. Prunes words containing letters that are globally forbidden (max=0).
"""
if not os.path.exists(wordlist_path):
print(f"Error: {wordlist_path} not found.")
return []
try:
with open(wordlist_path, 'r', encoding='utf-8') as f:
all_words = [w.strip().lower() for w in f if w.strip()]
except Exception as e:
print(f"Error reading wordlist: {e}")
return []
# Identify globally forbidden characters (max count is 0)
forbidden_chars = {c for c, limits in global_constraints.items() if limits['max'] == 0}
all_slots_candidates = []
for i, seg in enumerate(segments):
local = generate_local_constraints(seg)
candidates = []
print(f"\nScanning candidates for Slot {i+1} (Length {local['length']})...")
for w in all_words:
# 1. Length
if len(w) != local['length']: continue
# 2. Global "Zero Tolerance" check
if any(c in forbidden_chars for c in w): continue
# 3. Green (Fixed Position)
if any(w[idx] != char for idx, char in local['fixed'].items()): continue
# 4. Not At Position (Yellow/Grey)
if any(w[idx] == char for idx, char in local['not_at_pos']): continue
candidates.append(w)
print(f"-> {len(candidates)} candidates found.")
all_slots_candidates.append(candidates)
return all_slots_candidates
def solve_combination(candidate_lists, global_constraints):
"""
Finds a combination of one word from each list such that the total letter counts
satisfy the global constraints.
"""
print("\n=== Solving for Valid Combination ===")
# We use a recursive backtracking approach or itertools.product.
# Given the likely search space, product might be heavy if candidates are many.
# However, Python generators handle it decently if solutions appear early.
# Pre-check: if any list is empty, fail early
if any(not lst for lst in candidate_lists):
print("Error: One or more slots have no candidates.")
return None
# Limit total combinations to avoid hanging if constraints are loose
max_checks = 2000000
checks = 0
import time
start_time = time.time()
for combination in itertools.product(*candidate_lists):
checks += 1
if checks % 100000 == 0:
print(f"Checked {checks} combinations...")
if checks > max_checks:
print("Time limit exceeded searching for combinations.")
break
# Combine all letters
combined_text = "".join(combination)
counts = Counter(combined_text)
valid = True
# Check against global constraints
for char, limits in global_constraints.items():
c_count = counts[char]
if c_count < limits['min']:
valid = False; break
if limits['max'] is not None and c_count != limits['max']:
valid = False; break
if valid:
print(f"Solution Found in {time.time() - start_time:.2f}s!")
return "_".join(combination)
print("No valid combination found matching all constraints.")
return None
# --- Main Execution ---
# The specific ANSI byte string provided
raw_ansi_output = b'\x1b[93mU\x1b[0m\x1b[93mN\x1b[0m\x1b[93mI\x1b[0m\x1b[90mV\x1b[0m\x1b[92mE\x1b[0m\x1b[93mR\x1b[0m\x1b[93mS\x1b[0m\x1b[90mI\x1b[0m\x1b[93mT\x1b[0m\x1b[90mY\x1b[0m_\x1b[90mC\x1b[0m\x1b[93mO\x1b[0m\x1b[93mN\x1b[0m\x1b[93mT\x1b[0m\x1b[90mA\x1b[0m\x1b[90mC\x1b[0m\x1b[90mT\x1b[0m_\x1b[90mI\x1b[0m\x1b[93mN\x1b[0m\x1b[90mT\x1b[0m\x1b[92mE\x1b[0m\x1b[90mR\x1b[0m\x1b[93mN\x1b[0m\x1b[90mA\x1b[0m\x1b[90mT\x1b[0m\x1b[90mI\x1b[0m\x1b[93mO\x1b[0m\x1b[90mN\x1b[0m\x1b[90mA\x1b[0m\x1b[93mL\x1b[0m\n'
# 1. Parse
segments = parse_ansi_feedback(raw_ansi_output)
# 2. Analyze Global Constraints
global_cons = analyze_global_constraints(segments)
print("\nGlobal Constraints (Total Counts in Solution):")
for c, lim in global_cons.items():
mx = lim['max'] if lim['max'] is not None else "Inf"
print(f" {c.upper()}: {lim['min']} - {mx}")
# 3. Get Candidates per Slot
candidate_lists = get_candidates_for_slots('wordlist.txt', segments, global_cons)
# 4. Find valid combination
final_guess = solve_combination(candidate_lists, global_cons)
if final_guess:
print("\n--------------------------------")
print(f"FORMATTED GUESS: {final_guess}")
print("--------------------------------")