Logo
Overview
[CLUB LEAGUE] worlde writeup

[CLUB LEAGUE] worlde writeup

October 11, 2025
4 min read

Analysis

사전 외 단어 입력 시 Invalied word 메시지 출력

출력에서 ANSI 색 코드:

  • 33(G): 글자와 위치 모두 일치
  • 33(Y): 글자는 존재하지만 위치 다름
  • ETC/W: 글자 없음

Solver

추론 로직 설계

  • 대량 후보: 문자의 빈도 기반 점수화 (엔트로피 유사 휴리스틱)
  • 중간 후보: minimax 방식으로 최적 단어 추측
  • 소규모 후보: 재귀적 forced_guess searching
import socket
import re
import time
from collections import Counter
from typing import List, Set, Optional, Tuple
from pathlib import Path
WORD_LIST_PATH = Path(__file__).with_name('wordle_words.txt')
if not WORD_LIST_PATH.exists():
raise FileNotFoundError('wordle_words.txt not found; download the Wordle list first')
with open(WORD_LIST_PATH) as f:
WORDS = [line.strip() for line in f if line.strip()]
ALLOWED_WORDS = WORDS[:]
COLOR_RE = re.compile(r'\x1b\[(\d+(?:;\d+)*)m([a-z])\x1b\[0m')
def evaluate(secret: str, guess: str) -> str:
"""Return Wordle pattern string for guess against secret using 'g', 'y', 'b'."""
result = ['b'] * 5
counter = Counter()
for i in range(5):
s = secret[i]
g = guess[i]
if g == s:
result[i] = 'g'
else:
counter[s] += 1
for i in range(5):
if result[i] == 'g':
continue
g = guess[i]
if counter[g] > 0:
result[i] = 'y'
counter[g] -= 1
return ''.join(result)
def filter_candidates(candidates: List[str], guess: str, pattern: str) -> List[str]:
return [word for word in candidates if evaluate(word, guess) == pattern]
def score_candidates(candidates: List[str]) -> dict:
freq_total = Counter()
freq_pos = [Counter() for _ in range(5)]
for word in candidates:
unique_letters = set(word)
freq_total.update(unique_letters)
for idx, ch in enumerate(word):
freq_pos[idx][ch] += 1
return {
'total': freq_total,
'pos': freq_pos,
}
def forced_guess(candidates: List[str], allowed: List[str], remaining: int) -> Optional[str]:
if len(candidates) <= 1:
return candidates[0] if candidates else None
if remaining <= 1:
return None
cache: dict[tuple[Tuple[str, ...], int], Optional[str]] = {}
allowed_pool = list(dict.fromkeys(candidates + allowed[:max(20, len(candidates))]))
def helper(cands: Tuple[str, ...], rem: int) -> Optional[str]:
if len(cands) <= 1:
return cands[0] if cands else None
if rem <= 1:
return None
key = (cands, rem)
if key in cache:
return cache[key]
for word in allowed_pool:
partitions: dict[str, List[str]] = {}
for secret in cands:
pattern = evaluate(secret, word)
partitions.setdefault(pattern, []).append(secret)
ok = True
for pattern, subset in partitions.items():
if pattern == 'ggggg':
continue
res = helper(tuple(sorted(subset)), rem - 1)
if res is None:
ok = False
break
if ok:
cache[key] = word
return word
cache[key] = None
return None
return helper(tuple(sorted(candidates)), remaining)
def minimax_guess(candidates: List[str], allowed: List[str], used: Set[str]) -> str:
best_word = None
best_score = float('inf')
best_is_candidate = False
for word in allowed:
if word in used:
continue
partitions = {}
for secret in candidates:
pattern = evaluate(secret, word)
partitions[pattern] = partitions.get(pattern, 0) + 1
score = max(partitions.values())
is_candidate = word in candidates
if score < best_score or (score == best_score and is_candidate and not best_is_candidate):
best_score = score
best_word = word
best_is_candidate = is_candidate
if best_score == 1 and best_is_candidate:
break
return best_word if best_word else candidates[0]
def choose_guess(candidates: List[str], allowed: List[str], used: Set[str], remaining_attempts: int) -> str:
if not candidates:
return 'arise'
if len(candidates) == 1:
return candidates[0]
if len(candidates) <= 8:
forced = forced_guess(candidates, allowed, remaining_attempts)
if forced:
return forced
if len(candidates) <= 20:
return minimax_guess(candidates, allowed, used)
letter_freq = Counter()
pos_freq = [Counter() for _ in range(5)]
for word in candidates:
unique_letters = set(word)
letter_freq.update(unique_letters)
for idx, ch in enumerate(word):
pos_freq[idx][ch] += 1
letter_set = set(letter_freq)
best_word = None
best_score = -1.0
for word in allowed:
if word in used:
continue
unique_letters = set(word)
if not unique_letters & letter_set:
continue
score = sum(letter_freq[ch] for ch in unique_letters)
positional = sum(pos_freq[idx][ch] for idx, ch in enumerate(word))
if word in candidates:
score += positional
else:
score += 0.2 * positional
duplicate_penalty = len(word) - len(unique_letters)
if duplicate_penalty:
score -= duplicate_penalty * 0.5
if score > best_score:
best_score = score
best_word = word
if best_word is None:
for word in candidates:
if word not in used:
return word
return candidates[0]
return best_word
def parse_feedback(text: str):
matches = COLOR_RE.findall(text)
if len(matches) < 5:
raise ValueError(f"Unable to parse feedback: {text!r}")
matches = matches[-5:]
letters = []
pattern_chars = []
for code, letter in matches:
color = code.split(';')[-1]
if color == '32':
pattern_chars.append('g')
elif color == '33':
pattern_chars.append('y')
else:
pattern_chars.append('b')
letters.append(letter)
return ''.join(letters), ''.join(pattern_chars)
def read_until(sock: socket.socket, targets: List[bytes], timeout: float = 5.0) -> bytes:
sock.settimeout(timeout)
data = b''
deadline = time.time() + timeout
while time.time() < deadline:
for target in targets:
if target in data:
return data
try:
chunk = sock.recv(4096)
except socket.timeout:
continue
if not chunk:
return data
data += chunk
return data
def drain_socket(sock: socket.socket, timeout: float = 2.0) -> str:
sock.settimeout(timeout)
chunks = []
while True:
try:
chunk = sock.recv(4096)
except socket.timeout:
break
if not chunk:
break
chunks.append(chunk)
if b'FLAG{' in chunk.upper():
break
if not chunks:
return ''
return b''.join(chunks).decode('utf-8', errors='replace')
def main():
global ALLOWED_WORDS
sock = socket.create_connection(('3.39.86.84', 31337))
try:
initial = read_until(sock, [b'Enter your guess:'])
if b'FLAG{' in initial.upper():
print(initial.decode('utf-8', errors='replace'))
return
print('Connected to server, starting solver...')
round_num = 1
while True:
candidates = WORDS[:]
used = set()
solved = False
attempt = 1
max_attempts = 6
attempts_used = None
last_followup = b''
while attempt <= max_attempts:
remaining = max_attempts - attempt + 1
if attempt == 1:
first_choices = [w for w in ('arise', 'stare', 'alone', 'irate', 'raise') if w in candidates]
guess = first_choices[0] if first_choices else choose_guess(candidates, ALLOWED_WORDS, used, remaining)
else:
guess = choose_guess(candidates, ALLOWED_WORDS, used, remaining)
print(f'[Round {round_num} Attempt {attempt}] {guess}')
sock.sendall((guess + '\n').encode())
response = read_until(sock, [b'Enter your guess:', b'FLAG', b'Flag', b'Correct', b'Round '], timeout=2.0)
decoded = response.decode('utf-8', errors='replace')
if 'Invalid word' in decoded:
print(f' -> rejected by server')
if guess in ALLOWED_WORDS:
ALLOWED_WORDS = [w for w in ALLOWED_WORDS if w != guess]
candidates = [word for word in candidates if word != guess]
continue
if 'You lost!' in decoded:
raise RuntimeError(f'Lost round {round_num}')
used.add(guess)
if 'Correct!' in decoded:
solved = True
attempts_used = attempt
print(f' -> solved (server confirmed)')
followup = read_until(sock, [b'Enter your guess:', b'Round', b'FLAG', b'Flag'], timeout=2.0)
if followup:
last_followup = followup
if b'FLAG{' in followup.upper():
print(followup.decode('utf-8', errors='replace'))
return
break
before = len(candidates)
try:
_, pattern = parse_feedback(decoded)
except ValueError:
if 'FLAG{' in decoded.upper():
print(decoded)
return
raise
if pattern == 'ggggg':
solved = True
attempts_used = attempt
print(' -> solved exactly')
followup = read_until(sock, [b'Enter your guess:', b'Round', b'FLAG', b'Flag'], timeout=2.0)
if followup:
last_followup = followup
if b'FLAG{' in followup.upper():
print(followup.decode('utf-8', errors='replace'))
return
break
candidates = filter_candidates(candidates, guess, pattern)
print(f' -> pattern {pattern}, {before} -> {len(candidates)} candidates')
attempt += 1
if not solved:
raise RuntimeError(f'Failed to solve round {round_num}')
if attempts_used is not None:
print(f'Round {round_num} solved in {attempts_used} attempts')
if round_num >= 100:
if last_followup:
text_out = last_followup.decode('utf-8', errors='replace')
print(text_out)
if 'FLAG{' in text_out.upper():
return
remainder = drain_socket(sock)
if remainder:
print(remainder)
return
round_num += 1
finally:
sock.close()
if __name__ == '__main__':
main()
  1. Wordle 단어 목록을 만들어서 솔버를 구성. World_word.txt
  2. ANSI를 정규식으로 파싱하여 색상 코드별 패턴으로 변환한다.
  3. 후보를 유지하며 피드백 패턴에 맞는 단어만 남김
[Round 100 Attempt 4] snack
-> solved (server confirmed)
Round 100 solved in 4 attempts
--------------------------------
Congratulations! You cleared 100 rounds!
Here is your flag: hspace{830fc3076a183c74dfa2e053b689747f}