Initial commit
This commit is contained in:
commit
a36b57ed2e
|
@ -0,0 +1,560 @@
|
|||
extends Node
|
||||
const THEME_EMPTY := preload('res://letter_empty.tres')
|
||||
const THEME_ABSENT := preload('res://letter_absent.tres')
|
||||
const THEME_PRESENT := preload('res://letter_present.tres')
|
||||
const THEME_CORRECT := preload('res://letter_correct.tres')
|
||||
|
||||
const WORD_LENGTH := 5
|
||||
const MAX_TURNS := 6
|
||||
var required_charmask := 0
|
||||
var absent_charmask := 0
|
||||
var charmask_bans := []
|
||||
|
||||
onready var GuessesPanel := $'/root/Control/VBoxContainer/GuessesPanel'
|
||||
onready var KeyboardPanel := $'/root/Control/VBoxContainer/KeyboardPanel'
|
||||
onready var PlayGameDialog := $'/root/Control/PlayGameDialog'
|
||||
onready var NewGameButton := $'/root/Control/PlayGameDialog/NewGameButton'
|
||||
|
||||
var WordContainers: Array # Nodes
|
||||
var WordStrings: Array = [] # Strings
|
||||
var WordMasks: Array = [] # Mask form
|
||||
var WordChars: Array = [] # Mask form
|
||||
var Words: Array = [] # Entered words, string form
|
||||
var WordsMaskLookup: Dictionary = {}
|
||||
var AllValidWords: Array = []
|
||||
var ValidWords: Array = []
|
||||
var BotOpeners: Array = []
|
||||
var botopener: PoolStringArray
|
||||
|
||||
var working_word = ''
|
||||
var game_is_over = false
|
||||
|
||||
func _generate_WordsMaskLookup_recursive(key:=0, next_key:=0, depth:=5): # This is many orders of magnitude slower than Python, let alone Rust :/
|
||||
for k in range(next_key, 26):
|
||||
var key2 = key | (1<<k)
|
||||
var wordslist = []
|
||||
for w in WordsMaskLookup[key]:
|
||||
if WordMasks[w] & key2 == key2:
|
||||
wordslist.push_back(w)
|
||||
if len(wordslist) > 0:
|
||||
WordsMaskLookup[key2] = PoolIntArray(wordslist)
|
||||
if depth > 0:
|
||||
_generate_WordsMaskLookup_recursive(key2, next_key+1, depth-1)
|
||||
|
||||
func _generate_WordsMaskLookup(): # This seems fine perf-wise
|
||||
print('Generating WordsMask cache')
|
||||
for k1 in range(0, 26):
|
||||
var key1 = (1<<k1)
|
||||
var wl1 = []
|
||||
for w in WordsMaskLookup[0]:
|
||||
if WordMasks[w] & key1 == key1:
|
||||
wl1.push_back(w)
|
||||
if len(wl1) > 0:
|
||||
WordsMaskLookup[key1] = wl1
|
||||
|
||||
for k2 in range(k1+1, 26):
|
||||
var key2 = key1 | (1<<k2)
|
||||
var wl2 = []
|
||||
for w in WordsMaskLookup[key1]:
|
||||
if WordMasks[w] & key2 == key2:
|
||||
wl2.push_back(w)
|
||||
if len(wl2) > 0:
|
||||
WordsMaskLookup[key2] = wl2
|
||||
|
||||
for k3 in range(k2+1, 26):
|
||||
var key3 = key2 | (1<<k3)
|
||||
var wl3 = []
|
||||
for w in WordsMaskLookup[key2]:
|
||||
if WordMasks[w] & key3 == key3:
|
||||
wl3.push_back(w)
|
||||
if len(wl3) > 0:
|
||||
WordsMaskLookup[key3] = wl3
|
||||
|
||||
for k4 in range(k3+1, 26):
|
||||
var key4 = key3 | (1<<k4)
|
||||
var wl4 = []
|
||||
for w in WordsMaskLookup[key3]:
|
||||
if WordMasks[w] & key4 == key4:
|
||||
wl4.push_back(w)
|
||||
if len(wl4) > 0:
|
||||
WordsMaskLookup[key4] = wl4
|
||||
|
||||
for k5 in range(k4+1, 26):
|
||||
var key5 = key4 | (1<<k5)
|
||||
var wl5 = []
|
||||
for w in WordsMaskLookup[key4]:
|
||||
if WordMasks[w] & key5 == key5:
|
||||
wl5.push_back(w)
|
||||
if len(wl5) > 0:
|
||||
WordsMaskLookup[key5] = wl5
|
||||
|
||||
func _setup_globals() -> void:
|
||||
print('Loading dictionary')
|
||||
var file = File.new()
|
||||
match file.open('res://WORDLE', File.READ):
|
||||
OK:
|
||||
WordStrings = file.get_as_text().split('\n', false)
|
||||
var err:
|
||||
get_tree().quit(err)
|
||||
|
||||
print('Loading openers')
|
||||
match file.open('res://opening_words.txt', File.READ):
|
||||
OK:
|
||||
for line in file.get_as_text().split('\n', false):
|
||||
BotOpeners.push_back(PoolStringArray(line.split(',')))
|
||||
var err:
|
||||
get_tree().quit(err)
|
||||
|
||||
for s in WordStrings:
|
||||
var w = str2Word(s)
|
||||
AllValidWords.push_back(w)
|
||||
WordMasks.push_back(w[1])
|
||||
WordChars.push_back(w[2])
|
||||
var w_idxs = []
|
||||
for i in len(AllValidWords):
|
||||
w_idxs.push_back(i)
|
||||
WordsMaskLookup[0] = w_idxs
|
||||
_generate_WordsMaskLookup()
|
||||
|
||||
func new_game() -> void:
|
||||
PlayGameDialog.hide()
|
||||
print('Preparing new game')
|
||||
ValidWords = AllValidWords.slice(0, len(AllValidWords)-1)
|
||||
charmask_bans = []
|
||||
for i in WORD_LENGTH:
|
||||
charmask_bans.push_back(0)
|
||||
required_charmask = 0
|
||||
absent_charmask = 0
|
||||
botopener = BotOpeners[randi()%(len(BotOpeners))]
|
||||
Words = []
|
||||
for guess in WordContainers:
|
||||
for button in guess.letter_btns:
|
||||
button.text = ''
|
||||
button.theme = THEME_EMPTY
|
||||
for child in button.get_children():
|
||||
child.queue_free()
|
||||
game_is_over = false
|
||||
update_keyboard()
|
||||
bot_guess()
|
||||
|
||||
func _ready() -> void:
|
||||
WordContainers = GuessesPanel.get_children().slice(2, -1)
|
||||
assert(len(WordContainers)%2 == 0)
|
||||
|
||||
_setup_globals()
|
||||
print('Cache completed')
|
||||
|
||||
randomize()
|
||||
print('RNG seeded')
|
||||
|
||||
# yield(KeyboardPanel, 'ready')
|
||||
print('Skinning keyboard')
|
||||
update_keyboard()
|
||||
|
||||
NewGameButton.connect('pressed', self, 'new_game')
|
||||
print('Ready to play')
|
||||
|
||||
|
||||
func game_over() -> void:
|
||||
game_is_over = true
|
||||
var guesses := int(len(Words)/2)
|
||||
var ws := len(ValidWords)
|
||||
var endstr := ''
|
||||
if guesses <= 1:
|
||||
endstr = 'DEFEAT:\nThe goal is to pick different words...'
|
||||
elif guesses < 6:
|
||||
endstr = 'DEFEAT:\nYou were caught by the bot in %d guesses!'%guesses
|
||||
if ws > 1:
|
||||
endstr = endstr + '\nYou did have %d other words though...'%ws
|
||||
else:
|
||||
endstr = 'VICTORY:\nYou escaped the bot with %d words left to hide behind!'%ws
|
||||
PlayGameDialog.get_child(0).text = endstr
|
||||
PlayGameDialog.show()
|
||||
|
||||
|
||||
func _exhaustive_search_best() -> String:
|
||||
print('Doing a full exhaustive search')
|
||||
var best := 0
|
||||
var best_remainder := 1<<20
|
||||
for idx in len(WordMasks):
|
||||
var guessmask: int = WordMasks[idx]
|
||||
var guesschars: Array = WordChars[idx]
|
||||
var worst_remainder := 0
|
||||
for soli in len(ValidWords):
|
||||
var solw: Array = ValidWords[soli]
|
||||
var remainder := 0
|
||||
var reqmask: int = required_charmask | (guessmask & solw[1])
|
||||
var banmask: int = absent_charmask | (guessmask & ~solw[1])
|
||||
var charmasks := []
|
||||
var solc: Array = solw[2]
|
||||
for c in WORD_LENGTH:
|
||||
if solc[c] == guesschars[c]:
|
||||
charmasks.push_back(~solc[c])
|
||||
else:
|
||||
charmasks.push_back(charmask_bans[c] | guesschars[c])
|
||||
for w in ValidWords:
|
||||
if (w[1] & reqmask == reqmask) and (w[1] & banmask == 0):
|
||||
var remains := true
|
||||
var wc = w[2]
|
||||
for c in WORD_LENGTH:
|
||||
if wc[c] & charmasks[c] != 0:
|
||||
remains = false
|
||||
break
|
||||
if remains:
|
||||
remainder += 1
|
||||
worst_remainder = max(worst_remainder, remainder)
|
||||
if worst_remainder < best_remainder:
|
||||
best = idx
|
||||
best_remainder = worst_remainder
|
||||
if best_remainder == 1:
|
||||
break
|
||||
return WordStrings[best]
|
||||
|
||||
func _exhaustive_search_fast() -> String:
|
||||
print('Doing a fast exhaustive search')
|
||||
var best := 0
|
||||
var best_remainder := 1<<30
|
||||
for idx in len(WordMasks):
|
||||
var guessmask: int = WordMasks[idx]
|
||||
var worst_remainder := 0
|
||||
for soli in len(ValidWords):
|
||||
var solw: Array = ValidWords[soli]
|
||||
var remainder := 0
|
||||
var reqmask: Array = required_charmask | (guessmask & solw[1])
|
||||
var banmask: Array = absent_charmask | (guessmask & ~solw[1])
|
||||
for w in ValidWords:
|
||||
if (w[1] & reqmask == reqmask) and (w[1] & banmask == 0):
|
||||
remainder += 1
|
||||
worst_remainder = max(worst_remainder, remainder)
|
||||
if worst_remainder < best_remainder:
|
||||
best = idx
|
||||
best_remainder = worst_remainder
|
||||
if best_remainder == 1:
|
||||
break
|
||||
return WordStrings[best]
|
||||
|
||||
func _eliminative_search() -> String:
|
||||
print('Doing an eliminative search')
|
||||
var best := 0
|
||||
var untested_mask: int = ~(required_charmask | absent_charmask) & (1<<26 - 1)
|
||||
var highest_key = 0
|
||||
for key in WordsMaskLookup.keys():
|
||||
if key & untested_mask > highest_key:
|
||||
highest_key = key
|
||||
var indices = WordsMaskLookup[highest_key]
|
||||
best = indices[randi()%len(indices)]
|
||||
return WordStrings[best]
|
||||
|
||||
func bot_guess() -> void:
|
||||
assert(len(Words)%2 == 0)
|
||||
if game_is_over:
|
||||
return
|
||||
var words_left = len(ValidWords)
|
||||
var lw = len(Words)
|
||||
|
||||
var guess = botopener[0]
|
||||
if lw == 0:
|
||||
guess = botopener[0]
|
||||
elif lw == 2 and words_left > 20:
|
||||
guess = botopener[1]
|
||||
elif words_left <= 2: # 50:50
|
||||
match randi()%2:
|
||||
0: guess = Words[-1] # Make a cool effect for when it peeks answer to pressure you?
|
||||
_: guess = ValidWords[randi()%words_left][0] # This is fine
|
||||
elif words_left > 100: # Favor mass elimination
|
||||
match randi()%100:
|
||||
0: guess = Words[-1] # Make a cool effect for when it peeks answer to pressure you?
|
||||
1,2,3: guess = ValidWords[randi()%words_left][0] # Maybe adjust this?
|
||||
_: guess = _eliminative_search()
|
||||
elif words_left > 30: # Maybe some reasonably fast checking?
|
||||
match randi()%80:
|
||||
0: guess = Words[-1] # Make a cool effect for when it peeks answer to pressure you?
|
||||
1,2,3: guess = ValidWords[randi()%words_left][0] # Maybe adjust this?
|
||||
_: guess = _exhaustive_search_fast()
|
||||
else: # Go exhaustive bestcase
|
||||
match randi()%50:
|
||||
0: guess = Words[-1] # Make a cool effect for when it peeks answer to pressure you?
|
||||
1,2,3: guess = ValidWords[randi()%words_left][0] # Losing move tbh
|
||||
_: guess = _exhaustive_search_best()
|
||||
push_word(guess)
|
||||
|
||||
|
||||
func _exhaustive_answer_judgement(solution: String) -> String:
|
||||
print('Doing a full exhaustive answer grade')
|
||||
assert(len(Words)%2 == 1)
|
||||
var guess: Array = str2Word(Words[-1])
|
||||
var guessmask: int = guess[1]
|
||||
var guesschars: Array = guess[2]
|
||||
var results := []
|
||||
var chosen_result := 0
|
||||
for ans in ValidWords:
|
||||
var remainder := 0
|
||||
var reqmask: int = required_charmask | (guessmask & ans[1])
|
||||
var banmask: int = absent_charmask | (guessmask & ~ans[1])
|
||||
var charmasks := []
|
||||
var ansc: Array = ans[2]
|
||||
for c in WORD_LENGTH:
|
||||
if ansc[c] == guesschars[c]:
|
||||
charmasks.push_back(~ansc[c])
|
||||
else:
|
||||
charmasks.push_back(charmask_bans[c] | guesschars[c])
|
||||
for w in ValidWords:
|
||||
if (w[1] & reqmask == reqmask) and (w[1] & banmask == 0):
|
||||
var remains := true
|
||||
var wc = w[2]
|
||||
for c in WORD_LENGTH:
|
||||
if wc[c] & charmasks[c] != 0:
|
||||
remains = false
|
||||
break
|
||||
if remains:
|
||||
remainder += 1
|
||||
results.push_back(remainder)
|
||||
if ans[0] == solution:
|
||||
chosen_result = remainder
|
||||
# Note: we don't care about returning which word was best, just the possible scores
|
||||
results.sort()
|
||||
var sol_position = float(results.rfind(chosen_result))/len(results)
|
||||
if sol_position > 0.97 and len(results) > 200:
|
||||
return '!!'
|
||||
if sol_position > 0.9:
|
||||
return '!'
|
||||
elif sol_position > 0.5:
|
||||
return ''
|
||||
elif sol_position > 0.4:
|
||||
return '?!'
|
||||
elif sol_position > 0.2:
|
||||
return '?'
|
||||
else:
|
||||
return '??'
|
||||
|
||||
func _fast_answer_judgement(solution: String) -> String:
|
||||
print('Doing a fast exhaustive answer grade')
|
||||
assert(len(Words)%2 == 1)
|
||||
var guess: Array = str2Word(Words[-1])
|
||||
var guessmask: int = guess[1]
|
||||
var results := []
|
||||
var chosen_result := 0
|
||||
for ans in ValidWords:
|
||||
var remainder := 0
|
||||
var reqmask: int = required_charmask | (guessmask & ans[1])
|
||||
var banmask: int = absent_charmask | (guessmask & ~ans[1])
|
||||
for w in ValidWords:
|
||||
if (w[1] & reqmask == reqmask) and (w[1] & banmask == 0):
|
||||
remainder += 1
|
||||
results.push_back(remainder)
|
||||
if ans[0] == solution:
|
||||
chosen_result = remainder
|
||||
# Note: we don't care about returning which word was best, just the possible scores
|
||||
results.sort()
|
||||
var sol_position = float(results.rfind(chosen_result))/len(results)
|
||||
if sol_position > 0.9:
|
||||
return '!'
|
||||
elif sol_position > 0.4:
|
||||
return ''
|
||||
elif sol_position > 0.25:
|
||||
return '?!'
|
||||
elif sol_position > 0.1:
|
||||
return '?'
|
||||
else:
|
||||
return '??'
|
||||
|
||||
|
||||
func _estimated_answer_judgement(solution: String) -> String:
|
||||
print('Estimating answer grade')
|
||||
assert(len(Words)%2 == 1)
|
||||
var guess: Array = str2Word(Words[-1])
|
||||
var guessmask: int = guess[1]
|
||||
var guesschars: Array = guess[2]
|
||||
var ans = str2Word(solution)
|
||||
var remainder := 0
|
||||
var reqmask: int = required_charmask | (guessmask & ans[1])
|
||||
var banmask: int = absent_charmask | (guessmask & ~ans[1])
|
||||
for w in ValidWords:
|
||||
if (w[1] & reqmask == reqmask) and (w[1] & banmask == 0):
|
||||
remainder += 1
|
||||
var sol_position = float(remainder)/len(ValidWords)
|
||||
if sol_position > 0.75:
|
||||
return '!!'
|
||||
elif sol_position > 0.5:
|
||||
return '!'
|
||||
elif sol_position > 0.09:
|
||||
return ''
|
||||
elif sol_position > 0.025:
|
||||
return '?!'
|
||||
elif sol_position > 0.01:
|
||||
return '?'
|
||||
else:
|
||||
return '??'
|
||||
|
||||
func _answer_judgement(solution: String) -> String:
|
||||
if len(ValidWords) < 2:
|
||||
return '#'
|
||||
elif len(ValidWords) > 2400:
|
||||
return _estimated_answer_judgement(solution)
|
||||
elif len(ValidWords) > 300:
|
||||
return _fast_answer_judgement(solution)
|
||||
else:
|
||||
return _exhaustive_answer_judgement(solution)
|
||||
|
||||
func is_word_valid(word: Array) -> bool:
|
||||
if word[1] & required_charmask != required_charmask:
|
||||
return false
|
||||
for i in WORD_LENGTH:
|
||||
if word[2][i] & charmask_bans[i] != 0:
|
||||
return false
|
||||
return true
|
||||
|
||||
func push_word(word: String) -> void:
|
||||
update_word(word)
|
||||
Words.push_back(word)
|
||||
if len(Words)%2 == 0: # Just entered a solution
|
||||
var guess: String = Words[-2]
|
||||
var solution: String = Words[-1]
|
||||
if guess == solution:
|
||||
game_over()
|
||||
return
|
||||
for i in WORD_LENGTH:
|
||||
var gcm = ascii2cm(guess[i])
|
||||
if guess[i] == solution[i]:
|
||||
charmask_bans[i] = ~gcm
|
||||
required_charmask |= gcm
|
||||
elif guess[i] in solution:
|
||||
charmask_bans[i] |= gcm
|
||||
required_charmask |= gcm
|
||||
else:
|
||||
absent_charmask |= gcm
|
||||
for j in WORD_LENGTH:
|
||||
charmask_bans[j] |= gcm
|
||||
for i in range(len(ValidWords)-1, -1, -1):
|
||||
if not is_word_valid(ValidWords[i]):
|
||||
ValidWords.remove(i)
|
||||
update_keyboard()
|
||||
|
||||
|
||||
func update_keyboard() -> void:
|
||||
for button in KeyboardPanel.buttons:
|
||||
var cm = ascii2cm(button.text)
|
||||
if cm & absent_charmask != 0:
|
||||
button.theme = THEME_ABSENT
|
||||
elif cm & required_charmask != 0:
|
||||
button.theme = THEME_PRESENT
|
||||
else:
|
||||
button.theme = THEME_EMPTY
|
||||
|
||||
|
||||
func update_word(word: String) -> void:
|
||||
WordContainers[len(Words)].set_word(word)
|
||||
if len(Words)%2 == 1: # Solution entry
|
||||
WordContainers[len(Words)-1].grade_word(word)
|
||||
WordContainers[len(Words)].grade_solution(Words[-1])
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and not event.pressed:
|
||||
print('Processing InputEvent', event)
|
||||
for i in event.unicode:
|
||||
randi()
|
||||
var c = char(event.unicode).capitalize()
|
||||
var cm = ascii2cm(c)
|
||||
if cm > 0:
|
||||
if len(working_word) < WORD_LENGTH:
|
||||
if cm & charmask_bans[len(working_word)] == 0:
|
||||
working_word += c
|
||||
else:
|
||||
if cm & charmask_bans[-1] == 0:
|
||||
working_word[-1] = c
|
||||
update_word(working_word)
|
||||
else:
|
||||
match event.scancode:
|
||||
KEY_BACKSPACE:
|
||||
working_word.erase(len(working_word)-1, 1)
|
||||
update_word(working_word)
|
||||
KEY_ENTER:
|
||||
if (len(working_word) == WORD_LENGTH) and working_word in WordStrings:
|
||||
var judgement = _answer_judgement(working_word)
|
||||
push_word(working_word)
|
||||
if judgement != '':
|
||||
var lbl := Label.new()
|
||||
lbl.text = judgement
|
||||
lbl.align = Label.ALIGN_CENTER
|
||||
lbl.anchor_left = 1.0
|
||||
lbl.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
lbl.margin_top = 1
|
||||
lbl.add_color_override("font_color_shadow", Color(0, 0, 0))
|
||||
lbl.add_constant_override("shadow_offset_x", 1)
|
||||
lbl.add_constant_override("shadow_offset_y", 1)
|
||||
lbl.add_constant_override("shadow_as_outline", 1)
|
||||
match lbl.text:
|
||||
'?!': lbl.add_color_override("font_color", Color(1, 0.5, 0.5))
|
||||
'?': lbl.add_color_override("font_color", Color(1, 0.25, 0.25))
|
||||
'??': lbl.add_color_override("font_color", Color(1, 0, 0))
|
||||
WordContainers[len(Words)-1].letter_btns[-1].add_child(lbl)
|
||||
working_word = ''
|
||||
if len(Words) < 12:
|
||||
bot_guess()
|
||||
else:
|
||||
game_over()
|
||||
_:
|
||||
pass
|
||||
|
||||
func _osk_pressed(c):
|
||||
c = c[0]
|
||||
if c == '⌫':
|
||||
working_word.erase(len(working_word)-1, 1)
|
||||
update_word(working_word)
|
||||
else:
|
||||
var cm = ascii2cm(c)
|
||||
if len(working_word) < WORD_LENGTH:
|
||||
if cm & charmask_bans[len(working_word)] == 0:
|
||||
working_word += c
|
||||
else:
|
||||
if cm & charmask_bans[-1] == 0:
|
||||
working_word[-1] = c
|
||||
update_word(working_word)
|
||||
|
||||
|
||||
|
||||
func ascii2cm(c: String) -> int:
|
||||
match c:
|
||||
'S': return 1<<25
|
||||
'E': return 1<<24
|
||||
'A': return 1<<23
|
||||
'O': return 1<<22
|
||||
'R': return 1<<21
|
||||
'I': return 1<<20
|
||||
'L': return 1<<19
|
||||
'T': return 1<<18
|
||||
'N': return 1<<17
|
||||
'U': return 1<<16
|
||||
'D': return 1<<15
|
||||
'Y': return 1<<14
|
||||
'C': return 1<<13
|
||||
'P': return 1<<12
|
||||
'M': return 1<<11
|
||||
'H': return 1<<10
|
||||
'G': return 1<<9
|
||||
'B': return 1<<8
|
||||
'K': return 1<<7
|
||||
'W': return 1<<6
|
||||
'F': return 1<<5
|
||||
'V': return 1<<4
|
||||
'Z': return 1<<3
|
||||
'J': return 1<<2
|
||||
'X': return 1<<1
|
||||
'Q': return 1<<0
|
||||
_: return 0
|
||||
|
||||
func popcnt(x: int) -> int:
|
||||
var c = 0
|
||||
while x != 0:
|
||||
x &= x-1
|
||||
c += 1
|
||||
return c
|
||||
|
||||
func str2Word(s: String) -> Array:
|
||||
var chars = []
|
||||
var mask = 0
|
||||
for i in WORD_LENGTH:
|
||||
var cm = ascii2cm(s[i])
|
||||
mask |= cm
|
||||
chars.push_back(cm)
|
||||
return [s, mask, chars]
|
|
@ -0,0 +1,45 @@
|
|||
tool
|
||||
extends PanelContainer
|
||||
|
||||
const BACKSPACE = '⌫'
|
||||
const QWERTY := ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM'+BACKSPACE]
|
||||
const ALPHABETICAL := ['ABCDEFGHI', 'JKLMNOPQR', 'STUVWXYZ']
|
||||
const BY_FREQUENCY := ['SEAORILTN', 'UDYCPMHGB', 'KFWVZJXQ']
|
||||
enum KeyboardLayout {
|
||||
QWERTY,
|
||||
ALPHABETICAL,
|
||||
BY_FREQUENCY
|
||||
}
|
||||
|
||||
var rows := []
|
||||
var buttons := []
|
||||
var vcont
|
||||
|
||||
func make_buttons() -> void:
|
||||
# delete children and then
|
||||
vcont = VBoxContainer.new()
|
||||
for r in QWERTY:
|
||||
var row = HBoxContainer.new()
|
||||
row.alignment = BoxContainer.ALIGN_CENTER
|
||||
for c in r:
|
||||
var button = Button.new()
|
||||
button.text = c
|
||||
if c == BACKSPACE:
|
||||
button.text = '<-'
|
||||
button.connect('pressed', Game, '_osk_pressed', [c])
|
||||
button.rect_min_size = Vector2(32, 32)
|
||||
buttons.append(button)
|
||||
row.add_child(button)
|
||||
rows.push_back(row)
|
||||
vcont.add_child(row)
|
||||
add_child(vcont)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
make_buttons()
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
#func _process(delta: float) -> void:
|
||||
# pass
|
|
@ -0,0 +1,137 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://KeyboardPanel.gd" type="Script" id=1]
|
||||
[ext_resource path="res://WordContainer.tscn" type="PackedScene" id=2]
|
||||
|
||||
[node name="Control" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="GuessesPanel" type="GridContainer" parent="VBoxContainer"]
|
||||
margin_right = 420.0
|
||||
margin_bottom = 478.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
columns = 2
|
||||
|
||||
[node name="lbl_guess" type="Label" parent="VBoxContainer/GuessesPanel"]
|
||||
margin_right = 208.0
|
||||
margin_bottom = 14.0
|
||||
size_flags_horizontal = 3
|
||||
text = "Computer's Guess"
|
||||
align = 1
|
||||
|
||||
[node name="lbl_solution" type="Label" parent="VBoxContainer/GuessesPanel"]
|
||||
margin_left = 212.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 14.0
|
||||
size_flags_horizontal = 3
|
||||
text = "Your Solution"
|
||||
align = 1
|
||||
|
||||
[node name="WordContainer" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_right = 208.0
|
||||
|
||||
[node name="WordContainer2" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_left = 212.0
|
||||
margin_right = 420.0
|
||||
|
||||
[node name="WordContainer3" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_top = 54.0
|
||||
margin_right = 208.0
|
||||
margin_bottom = 86.0
|
||||
|
||||
[node name="WordContainer4" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_left = 212.0
|
||||
margin_top = 54.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 86.0
|
||||
|
||||
[node name="WordContainer5" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_top = 90.0
|
||||
margin_right = 208.0
|
||||
margin_bottom = 122.0
|
||||
|
||||
[node name="WordContainer6" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_left = 212.0
|
||||
margin_top = 90.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 122.0
|
||||
|
||||
[node name="WordContainer7" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_top = 126.0
|
||||
margin_right = 208.0
|
||||
margin_bottom = 158.0
|
||||
|
||||
[node name="WordContainer8" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_left = 212.0
|
||||
margin_top = 126.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 158.0
|
||||
|
||||
[node name="WordContainer9" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_top = 162.0
|
||||
margin_right = 208.0
|
||||
margin_bottom = 194.0
|
||||
|
||||
[node name="WordContainer10" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_left = 212.0
|
||||
margin_top = 162.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 194.0
|
||||
|
||||
[node name="WordContainer11" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_top = 198.0
|
||||
margin_right = 208.0
|
||||
margin_bottom = 230.0
|
||||
|
||||
[node name="WordContainer12" parent="VBoxContainer/GuessesPanel" instance=ExtResource( 2 )]
|
||||
margin_left = 212.0
|
||||
margin_top = 198.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 230.0
|
||||
|
||||
[node name="KeyboardPanel" type="PanelContainer" parent="VBoxContainer"]
|
||||
margin_top = 482.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 600.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource( 1 )
|
||||
|
||||
[node name="PlayGameDialog" type="ColorRect" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color( 0, 0, 0, 0.705882 )
|
||||
|
||||
[node name="Label" type="Label" parent="PlayGameDialog"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_bottom = -300.0
|
||||
text = "A bot is trying to hunt you down!
|
||||
When it guesses a word, choose a word to hide behind.
|
||||
You can change words each turn,
|
||||
but you can't contradict your previous clues!
|
||||
|
||||
Try to last 6 turns!"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="NewGameButton" type="Button" parent="PlayGameDialog"]
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_top = -300.0
|
||||
text = "Play New Game"
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
extends HBoxContainer
|
||||
|
||||
const WORD_LENGTH := 5
|
||||
const THEME_EMPTY := preload('res://letter_empty.tres')
|
||||
const THEME_ABSENT := preload('res://letter_absent.tres')
|
||||
const THEME_PRESENT := preload('res://letter_present.tres')
|
||||
const THEME_CORRECT := preload('res://letter_correct.tres')
|
||||
|
||||
var word := ''
|
||||
var solution := ''
|
||||
var guess := ''
|
||||
onready var letter_btns := get_children()
|
||||
|
||||
func set_word(word: String):
|
||||
self.word = word
|
||||
for i in WORD_LENGTH:
|
||||
letter_btns[i].text = word[i] if len(word) > i else ''
|
||||
|
||||
func grade_word(solution: String):
|
||||
assert(self.word.length() == WORD_LENGTH)
|
||||
if solution.length() < WORD_LENGTH:
|
||||
self.solution = solution + ' '.repeat(WORD_LENGTH - len(solution))
|
||||
elif solution.length() == WORD_LENGTH:
|
||||
self.solution = solution
|
||||
else:
|
||||
self.solution = solution.left(WORD_LENGTH)
|
||||
for i in WORD_LENGTH:
|
||||
if self.word[i] == self.solution[i]:
|
||||
self.letter_btns[i].theme = THEME_CORRECT
|
||||
elif self.word[i] in self.solution:
|
||||
self.letter_btns[i].theme = THEME_PRESENT
|
||||
else:
|
||||
self.letter_btns[i].theme = THEME_ABSENT
|
||||
|
||||
func grade_solution(guess: String):
|
||||
if guess.length() < WORD_LENGTH:
|
||||
self.guess = guess + ' '.repeat(WORD_LENGTH - len(guess))
|
||||
elif guess.length() == WORD_LENGTH:
|
||||
self.guess = guess
|
||||
else:
|
||||
self.guess = guess.left(WORD_LENGTH)
|
||||
for i in WORD_LENGTH:
|
||||
if len(self.word) > i:
|
||||
if self.word[i] == self.guess[i]:
|
||||
self.letter_btns[i].theme = THEME_CORRECT
|
||||
elif self.word[i] in self.guess:
|
||||
self.letter_btns[i].theme = THEME_PRESENT
|
||||
else:
|
||||
self.letter_btns[i].theme = THEME_EMPTY
|
||||
else:
|
||||
self.letter_btns[i].theme = THEME_EMPTY
|
|
@ -0,0 +1,45 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://letter_empty.tres" type="Theme" id=1]
|
||||
[ext_resource path="res://WordContainer.gd" type="Script" id=2]
|
||||
|
||||
[node name="WordContainer" type="HBoxContainer"]
|
||||
margin_top = 18.0
|
||||
margin_right = 198.0
|
||||
margin_bottom = 50.0
|
||||
theme = ExtResource( 1 )
|
||||
alignment = 1
|
||||
script = ExtResource( 2 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="WordChar1" type="Button" parent="."]
|
||||
margin_left = 11.0
|
||||
margin_right = 43.0
|
||||
margin_bottom = 32.0
|
||||
rect_min_size = Vector2( 32, 32 )
|
||||
|
||||
[node name="WordChar2" type="Button" parent="."]
|
||||
margin_left = 47.0
|
||||
margin_right = 79.0
|
||||
margin_bottom = 32.0
|
||||
rect_min_size = Vector2( 32, 32 )
|
||||
|
||||
[node name="WordChar3" type="Button" parent="."]
|
||||
margin_left = 83.0
|
||||
margin_right = 115.0
|
||||
margin_bottom = 32.0
|
||||
rect_min_size = Vector2( 32, 32 )
|
||||
|
||||
[node name="WordChar4" type="Button" parent="."]
|
||||
margin_left = 119.0
|
||||
margin_right = 151.0
|
||||
margin_bottom = 32.0
|
||||
rect_min_size = Vector2( 32, 32 )
|
||||
|
||||
[node name="WordChar5" type="Button" parent="."]
|
||||
margin_left = 155.0
|
||||
margin_right = 187.0
|
||||
margin_bottom = 32.0
|
||||
rect_min_size = Vector2( 32, 32 )
|
|
@ -0,0 +1,29 @@
|
|||
[gd_resource type="Theme" load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://stylebox_letter_normal_absent.tres" type="StyleBox" id=1]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.18, 0.207, 0.279, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.14, 0.161, 0.217, 1 )
|
||||
|
||||
[resource]
|
||||
Button/colors/font_color = Color( 0.8, 0.8075, 0.8275, 1 )
|
||||
Button/colors/font_color_disabled = Color( 1, 1, 1, 0.3 )
|
||||
Button/colors/font_color_focus = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_hover = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_pressed = Color( 0.41, 0.61, 0.91, 1 )
|
||||
Button/colors/icon_color_hover = Color( 1.15, 1.15, 1.15, 1 )
|
||||
Button/colors/icon_color_pressed = Color( 0.4715, 0.7015, 1.0465, 1 )
|
||||
Button/styles/disabled = SubResource( 1 )
|
||||
Button/styles/focus = ExtResource( 1 )
|
||||
Button/styles/hover = ExtResource( 1 )
|
||||
Button/styles/normal = ExtResource( 1 )
|
||||
Button/styles/pressed = ExtResource( 1 )
|
|
@ -0,0 +1,29 @@
|
|||
[gd_resource type="Theme" load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://stylebox_letter_normal_correct.tres" type="StyleBox" id=1]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.18, 0.207, 0.279, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.14, 0.161, 0.217, 1 )
|
||||
|
||||
[resource]
|
||||
Button/colors/font_color = Color( 0.8, 0.8075, 0.8275, 1 )
|
||||
Button/colors/font_color_disabled = Color( 1, 1, 1, 0.3 )
|
||||
Button/colors/font_color_focus = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_hover = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_pressed = Color( 0.41, 0.61, 0.91, 1 )
|
||||
Button/colors/icon_color_hover = Color( 1.15, 1.15, 1.15, 1 )
|
||||
Button/colors/icon_color_pressed = Color( 0.4715, 0.7015, 1.0465, 1 )
|
||||
Button/styles/disabled = SubResource( 1 )
|
||||
Button/styles/focus = ExtResource( 1 )
|
||||
Button/styles/hover = ExtResource( 1 )
|
||||
Button/styles/normal = ExtResource( 1 )
|
||||
Button/styles/pressed = ExtResource( 1 )
|
|
@ -0,0 +1,29 @@
|
|||
[gd_resource type="Theme" load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://stylebox_letter_normal_empty.tres" type="StyleBox" id=1]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.18, 0.207, 0.279, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.14, 0.161, 0.217, 1 )
|
||||
|
||||
[resource]
|
||||
Button/colors/font_color = Color( 0.8, 0.8075, 0.8275, 1 )
|
||||
Button/colors/font_color_disabled = Color( 1, 1, 1, 0.3 )
|
||||
Button/colors/font_color_focus = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_hover = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_pressed = Color( 0.41, 0.61, 0.91, 1 )
|
||||
Button/colors/icon_color_hover = Color( 1.15, 1.15, 1.15, 1 )
|
||||
Button/colors/icon_color_pressed = Color( 0.4715, 0.7015, 1.0465, 1 )
|
||||
Button/styles/disabled = SubResource( 1 )
|
||||
Button/styles/focus = ExtResource( 1 )
|
||||
Button/styles/hover = ExtResource( 1 )
|
||||
Button/styles/normal = ExtResource( 1 )
|
||||
Button/styles/pressed = ExtResource( 1 )
|
|
@ -0,0 +1,29 @@
|
|||
[gd_resource type="Theme" load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://stylebox_letter_normal_present.tres" type="StyleBox" id=1]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.18, 0.207, 0.279, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.14, 0.161, 0.217, 1 )
|
||||
|
||||
[resource]
|
||||
Button/colors/font_color = Color( 0.8, 0.8075, 0.8275, 1 )
|
||||
Button/colors/font_color_disabled = Color( 1, 1, 1, 0.3 )
|
||||
Button/colors/font_color_focus = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_hover = Color( 0.88, 0.8845, 0.8965, 1 )
|
||||
Button/colors/font_color_pressed = Color( 0.41, 0.61, 0.91, 1 )
|
||||
Button/colors/icon_color_hover = Color( 1.15, 1.15, 1.15, 1 )
|
||||
Button/colors/icon_color_pressed = Color( 0.4715, 0.7015, 1.0465, 1 )
|
||||
Button/styles/disabled = SubResource( 1 )
|
||||
Button/styles/focus = ExtResource( 1 )
|
||||
Button/styles/hover = ExtResource( 1 )
|
||||
Button/styles/normal = ExtResource( 1 )
|
||||
Button/styles/pressed = ExtResource( 1 )
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,38 @@
|
|||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=4
|
||||
|
||||
_global_script_classes=[ ]
|
||||
_global_script_class_icons={
|
||||
}
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Word Hunt"
|
||||
run/main_scene="res://Main.tscn"
|
||||
config/icon="res://icon.png"
|
||||
|
||||
[autoload]
|
||||
|
||||
Game="*res://Game.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/width=420
|
||||
window/dpi/allow_hidpi=true
|
||||
window/handheld/orientation="portrait"
|
||||
|
||||
[physics]
|
||||
|
||||
common/enable_pause_aware_picking=true
|
||||
|
||||
[rendering]
|
||||
|
||||
quality/driver/driver_name="GLES2"
|
||||
environment/default_environment="res://default_env.tres"
|
|
@ -0,0 +1,13 @@
|
|||
[gd_resource type="StyleBoxFlat" format=2]
|
||||
|
||||
[resource]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.109804, 0.109804, 0.109804, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.125, 0.14375, 0.19375, 1 )
|
|
@ -0,0 +1,13 @@
|
|||
[gd_resource type="StyleBoxFlat" format=2]
|
||||
|
||||
[resource]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.447059, 0.439216, 0.631373, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.125, 0.14375, 0.19375, 1 )
|
|
@ -0,0 +1,13 @@
|
|||
[gd_resource type="StyleBoxFlat" format=2]
|
||||
|
||||
[resource]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.160784, 0.156863, 0.219608, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.125, 0.14375, 0.19375, 1 )
|
|
@ -0,0 +1,13 @@
|
|||
[gd_resource type="StyleBoxFlat" format=2]
|
||||
|
||||
[resource]
|
||||
content_margin_left = 6.0
|
||||
content_margin_right = 6.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color( 0.631373, 0.439216, 0.439216, 1 )
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color( 0.125, 0.14375, 0.19375, 1 )
|
Loading…
Reference in New Issue