#!/usr/bin/python3 import sys import subprocess import re import os import time import syslog progname = sys.argv[0].rpartition("/")[2] modroot = os.path.realpath(sys.argv[0] + '/../..') colour_codes = { 'clear':'\x1b[0m', 'black':'\x1b[97;40m', 'green':'\x1b[97;42m', 'yellow':'\x1b[97;43m' } #dictionary = '%s/etc/words' % (modroot) dictionary = '/usr/share/dict/words' def main(): global modroot, verboselevel, word_length, dictionary # Defaults for options verboselevel = 2 mode = 'random' word = None word_length = 5 # Process options, old school while True: if len(sys.argv) == 1: break # Application-specific options elif sys.argv[1].startswith('--word='): mode = 'word' word = sys.argv[1][len('--word='):] elif sys.argv[1] == '--random': mode = 'random' elif sys.argv[1].startswith('--dict='): dictionary = sys.argv[1][len('--dict='):] elif sys.argv[1].startswith('--length='): word_length = int(sys.argv[1][len('--length='):]) # Standard options elif sys.argv[1].startswith('--debug='): verboselevel = sys.argv[1][len('--debug='):] if not re.search('^\d+$', verboselevel): usage() verboselevel = int(verboselevel) elif sys.argv[1] == '--verbose': verboselevel = 3 elif sys.argv[1] == '--help': usage(0) elif sys.argv[1] == '-v': verboselevel = 3 elif sys.argv[1] == '-d': if len(sys.argv) < 3: usage() verboselevel = sys.argv[2] del sys.argv[1] if not re.search('^\d+$', verboselevel): usage() verboselevel = int(verboselevel) elif sys.argv[1] == '-h': usage(0) elif sys.argv[1] == '--': del sys.argv[1] break elif sys.argv[1].startswith('-'): usage() else: break del sys.argv[1] # Process arguments if len(sys.argv) != 1: usage() # Sanity checks and derivations # Choose a word if mode == 'random': actual_word = subprocess.Popen('egrep \'^[a-z]{%d}$\' %s | shuf -n 1' % (word_length, dictionary), shell=True, stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].strip() elif mode == 'word': actual_word = word debug(10, 'main: chosen word is: %s' % (actual_word)) # Guts # We write the chosen word into a file; this serves two purposes: # # 1) it allows a very simple solver to pick a few random wrong words *and* # the right word, shuffle that list and use those words as guesses. # # 2) should the solver exit early (e.g. because it's being run interactively # and the user got bored before guessing the work) then the solver # can use the set work in some commiseraton message. #fh = open('/tmp/%s' % (progname), 'w') #fh.write(actual_word) #fh.close() while True: # Await a guess (or EOF or invalid guess) debug(10, 'main: awaiting guess ...') try: # Take only the first five letters (the rest is the solver being decorative). guess_word = input().strip()[0:word_length] except (EOFError, KeyboardInterrupt): break # Once, I saw a UTF error except: sys.stdout.write('invalid guess (case #2)\n') continue debug(10, 'main: received guess \'%s\'' % (guess_word)) if not re.search('^[a-z]{%d}$' % (word_length), guess_word): sys.stdout.write('invalid guess (case #1)\n') continue if guess_word == 'enuff': break if guess_word == 'telme': sys.stdout.write('%s\n' % (actual_word)) continue # To correctly work out the colours requires some tracking of which # letters have already been 'used' (e.g. if the hidden word contains # one 'X' and the guess contains two 'X's then *only one* should get # a green or yelloe tile, not both of them!). This applies *both* # to the letters in the hidden word and to the letters in the guess. actual_data = [ { 'letter':x, 'used':False } for x in actual_word.upper() ] guess_data = [ { 'letter':x, 'colour':'black', 'used':False } for x in guess_word.upper() ] # First scan for letters in the right place and mark them green and used. for i in range(0,word_length): if actual_data[i]['letter'] == guess_data[i]['letter']: guess_data[i]['colour'] = 'green' actual_data[i]['used'] = True guess_data[i]['used'] = True # Second scan for letters in the wrong place *and* that are not yet marked used and mark them yellow and used. for i in range(0,word_length): if guess_data[i]['used']: continue # Construct list of letters with any that are to be green replaced with None. Then search # that list (using .index()) for the guess letter under consideration. If the guess letter # is not present then we get ValueError, hence doing all this under a try. try: j = [ x['letter'] if not x['used'] else None for x in actual_data ].index(guess_data[i]['letter']) guess_data[i]['colour'] = 'yellow' actual_data[j]['used'] = True guess_data[i]['used'] = True except ValueError: pass # Construct machine-readable and human-readable responses and display them. s1 = ''.join([ { 'black':'b', 'yellow':'y', 'green':'g' }[x['colour']] for x in guess_data ]) s2 = ''.join([ '%s%s%s' % (colour_codes[x['colour']], x['letter'], colour_codes['clear']) for x in guess_data ]) sys.stdout.write('%s (%s)\n' % (s1, s2)) # Messaging functions def usage(rc=1): global progname fh = {True:sys.stdout, False:sys.stderr}[rc == 0] fh.write('Usage: %s [ ]\n' % progname) sys.exit(rc) def debug(level, text): global verboselevel if verboselevel < level: return msg_writer("DEBUG[%s]: %s" % (level, text), syslog.LOG_DEBUG) def info(text): global verboselevel if verboselevel < 3: return msg_writer("INFO: %s" % (text), syslog.LOG_INFO) def warning(text): global verboselevel if verboselevel < 2: return msg_writer("WARNING: %s" % (text), syslog.LOG_WARNING) def error(text): msg_writer("ERROR: %s" % (text), syslog.LOG_ERR) sys.exit(1) def internal(text): msg_writer("INTERNAL ERROR: %s" % (text), syslog.LOG_CRIT) sys.exit(2) def msg_writer(text, syslog_level): msg_writer_stderr(text) if not sys.stderr.isatty(): msg_writer_syslog(text, syslog_level) def msg_writer_stderr(text): global progname sys.stderr.write("%s: %s\n" % (progname, text)) def msg_writer_syslog(text, syslog_level): global syslog_opened_flag, progname if not syslog_opened_flag: if sys.version_info < (2, 7): syslog.openlog(progname, syslog.LOG_PID) else: syslog.openlog(progname, logoption=syslog.LOG_PID) syslog_opened_flag = True syslog.syslog((syslog.LOG_LOCAL0|syslog_level), text) def signal_handler(sig, frame): sys.stderr.write('^C\n') sys.exit(1) # Entry point if __name__=="__main__": main()