def play(challenge, workspace, keys=None, diff=False): logger.info('play(...)') infile = os.path.join(workspace, 'in') if challenge.in_extension: infile += challenge.in_extension outfile = os.path.join(workspace, 'out') if challenge.out_extension: outfile += challenge.out_extension scriptfile = os.path.join(workspace, 'script') logfile = os.path.join(workspace, 'log') with open(outfile, 'w') as f: f.write(challenge.out_text) write('Launching vimgolf session', fg='yellow') main_loop( challenge=challenge, infile=infile, logfile=logfile, outfile=outfile, scriptfile=scriptfile, initial_keys=keys, diff=diff, ) write('Thanks for playing!', fg='green')
def fetch_and_validate_challenge(challenge_id): write('Downloading vimgolf challenge {}'.format(challenge_id), fg='yellow') challenge = Challenge(challenge_id) challenge.load_or_download() challenge_spec = challenge.spec compliant = challenge_spec.get('client') == RUBY_CLIENT_VERSION_COMPLIANCE if not compliant: message = 'vimgolf=={} is not compliant with vimgolf.com'.format( __version__) write(message, err=True, fg='red') write('Uploading to vimgolf.com is disabled', err=True, fg='red') write('vimgolf may not function properly', fg='red') try: from distutils.version import StrictVersion client_compliance_version = StrictVersion( RUBY_CLIENT_VERSION_COMPLIANCE) api_version = StrictVersion(challenge_spec['client']) action = 'upgrade' if api_version > client_compliance_version else 'downgrade' except Exception: action = 'update' write('Please {} vimgolf to a compliant version'.format(action), fg='yellow') if not confirm('Try to play without uploads?'): raise Failure() return compliant
def vim(args, **run_kwargs): try: _vim(args, **run_kwargs) except Failure: raise except Exception: logger.exception('{} execution failed'.format(GOLF_VIM)) write('The execution of {} has failed'.format(GOLF_VIM), err=True, fg='red') raise Failure()
def inspect(challenge_id, keys, literal_lt, literal_gt): challenge_id = expand_challenge_id(challenge_id) logger.info('inspect(%s)', challenge_id) if not validate_challenge_id(challenge_id): show_challenge_id_error() raise Failure() try: challenge = Challenge(challenge_id) challenge.load_or_download() except Failure: raise except Exception: logger.exception('challenge retrieval failed') write('The challenge retrieval has failed', err=True, fg='red') write('Please check the challenge ID on vimgolf.com', err=True, fg='red') raise Failure() src_in_path = challenge.in_path name, ext = os.path.splitext(os.path.basename(src_in_path)) sequences = build_sequences( keys=keys, literal_gt=literal_gt, literal_lt=literal_lt ) with tempfile.TemporaryDirectory() as workspace: zfill = lambda s: str(s).zfill(3) def dst_path(index): return os.path.join(workspace, '{}{}{}'.format(name, zfill(index), ext)) def script_path(index): return os.path.join(workspace, 'mapping{}.vim'.format(zfill(index))) def in_path(index): return os.path.join(workspace, 'inspect-{}{}{}'.format(name, zfill(index), ext)) replay_sequences( dst_path=dst_path, script_path=script_path, sequences=sequences, src_in_path=src_in_path ) inspect_sequences( workspace=workspace, dst_path=dst_path, in_path=in_path, sequences=sequences )
def config(api_key=None): logger.info('config(...)') if api_key is not None and not validate_api_key(api_key): show_api_key_error() raise Failure() if api_key: set_api_key(api_key) else: api_key = get_api_key() if api_key: write(api_key) else: show_api_key_help()
def put(challenge_id, keys, diff=False): challenge_id = expand_challenge_id(challenge_id) logger.info('put(%s)', challenge_id) if not validate_challenge_id(challenge_id): show_challenge_id_error() raise Failure() api_key = get_api_key() if not validate_api_key(api_key): write('An API key has not been configured', fg='red') write('Uploading to vimgolf.com is disabled', fg='red') show_api_key_help() if not confirm('Play without uploads?'): raise Failure() try: compliant = fetch_and_validate_challenge(challenge_id) except Failure: raise except Exception: logger.exception('challenge retrieval failed') write('The challenge retrieval has failed', err=True, fg='red') write('Please check the challenge ID on vimgolf.com', err=True, fg='red') raise Failure() challenge = Challenge(id=challenge_id, compliant=compliant, api_key=api_key).load() with tempfile.TemporaryDirectory() as d: play(challenge, d, keys=keys, diff=diff) challenge.update_metadata()
def ls(incomplete=False, page=None, limit=LISTING_LIMIT): logger.info('list_(%s, %s)', page, limit) stored_challenges = get_stored_challenges() try: url = GOLF_HOST if page is not None: url = urllib.parse.urljoin(GOLF_HOST, '/?page={}'.format(page)) response = http_request(url) listings = extract_listings_from_page( page_html=response.body, limit=limit, stored_challenges=stored_challenges) except Failure: raise except Exception: logger.exception('challenge retrieval failed') write('The challenge list retrieval has failed', err=True, fg='red') raise Failure() table_rows = [['#', 'Name', 'Entries', 'ID', '⬆', 'Score', 'Answers']] for idx, listing in enumerate(listings): if incomplete and listing.uploaded: continue table_row = [ '{}{} '.format(EXPANSION_PREFIX, idx + 1), listing.name, listing.n_entries, style(listing.id, fg='yellow'), bool_to_mark(listing.uploaded), listing.score if listing.score and listing.score > 0 else '-', listing.answers or '-', ] table_rows.append(table_row) write(AsciiTable(table_rows).table) id_lookup = { str(idx + 1): listing.id for idx, listing in enumerate(listings) } set_id_lookup(id_lookup)
def _vim(args, **run_kwargs): vim_path = find_executable(GOLF_VIM) if not vim_path: write('Unable to find "{}"'.format(GOLF_VIM), fg='red') write('Please update your PATH to include the directory with "{}"'. format(GOLF_VIM), fg='red') raise Failure() vim_name = os.path.basename(os.path.realpath(vim_path)) if sys.platform == 'win32': # Remove executable extension (.exe, .bat, .cmd, etc.) from 'vim_name' base, ext = os.path.splitext(vim_name) pathexts = os.environ.get('PATHEXT', '.EXE').split(os.pathsep) for pathext in pathexts: if ext.upper() == pathext.upper(): vim_name = base break # As of 2019/3/2, on Windows, nvim-qt doesn't support --nofork. # Issue a warning as opposed to failing, since this may change. if vim_name == 'nvim-qt' and sys.platform == 'win32': write('vimgolf with nvim-qt on Windows may not function properly', fg='red') write( 'If there are issues, please try using a different version of vim', fg='yellow') if not confirm('Continue trying to play?'): raise Failure() # Configure args used by all vim invocations (for both playing and diffing) # 'vim_path' is used instead of GOLF_VIM to handle 'vim.bat' on the PATH. # subprocess.run would not launch vim.bat with GOLF_VIM == 'vim', but 'find_executable' # will return the full path to vim.bat in that case. vim_args = [vim_path] # Add --nofork so gvim, mvim, and nvim-qt don't return immediately # Add special-case handling since nvim doesn't accept that option. if vim_name != 'nvim': vim_args.append('--nofork') # For nvim-qt, options after '--' are passed to nvim. if vim_name == 'nvim-qt': vim_args.append('--') vim_args.extend(args) subprocess.run(vim_args, **run_kwargs) # On Windows, vimgolf freezes when reading input after nvim's exit. # For an unknown reason, shell'ing out an effective no-op works-around the issue if vim_name == 'nvim' and sys.platform == 'win32': os.system('')
def show_api_key_error(): write('Invalid API key', err=True, fg='red') write('Please check your API key on vimgolf.com', err=True, fg='red')
def show_api_key_help(): write('An API key can be obtained from vimgolf.com', fg='yellow') write('Please run "vimgolf config API_KEY" to set your API key', fg='yellow')
def main_loop(challenge, infile, logfile, outfile, scriptfile, initial_keys, diff): keys = initial_keys while True: with open(infile, 'w') as f: f.write(challenge.in_text) with open(scriptfile, 'w') as f: if keys: f.write(KeycodeReprs(keys).call_feedkeys) if not diff: play_result = play_single( infile=infile, logfile=logfile, outfile=outfile, scriptfile=scriptfile, ) raw_keys = play_result['raw_keys'] keycode_reprs = play_result['keycode_reprs'] correct = play_result['correct'] score = play_result['score'] write('Here are your keystrokes:', fg='green') for keycode_repr in keycode_reprs: color = 'magenta' if len(keycode_repr) > 1 else None write(keycode_repr, fg=color, nl=False) write() if correct: write('Success! Your output matches.', fg='green') write('Your score:', fg='green') else: write('Uh oh, looks like your entry does not match the desired output.', fg='red') write('Your score for this failed attempt:', fg='red') write(score) else: correct = False raw_keys = '' keycode_reprs = '' score = 0 menu_loop_result = menu_loop( challenge=challenge, correct=correct, infile=infile, outfile=outfile, raw_keys=raw_keys, diff=diff, ) keys = menu_loop_result['keys'] if challenge.id and not diff: challenge.add_answer( keys=keycode_reprs, score=score, correct=correct, uploaded=menu_loop_result['uploaded'], ) if menu_loop_result['should_quit']: break
def menu_loop( challenge, correct, infile, outfile, raw_keys, diff): upload_eligible = ( challenge.id and challenge.compliant and challenge.api_key and not diff ) uploaded = False while True: # Generate the menu items inside the loop since it can change across iterations # (e.g., upload option can be removed) menu = [] if not correct: menu.append(('d', 'Show diff')) if upload_eligible and correct: menu.append(('w', 'Upload result')) menu.append(('r', 'Retry the current challenge')) menu.append(('k', 'Retry the current challenge with key sequence')) menu.append(('q', 'Quit vimgolf')) valid_codes = [x[0] for x in menu] if diff: selection = 'd' else: for opt in menu: write('[{}] {}'.format(*opt), fg='yellow') selection = input_loop('Choice> ') if selection not in valid_codes: write('Invalid selection: {}'.format(selection), err=True, fg='red') elif selection == 'd': diff_args = ['-d', '-n', infile, outfile] vim(diff_args) if diff: selection = 'q' break elif selection == 'w': success = upload_result(challenge.id, challenge.api_key, raw_keys) if success: write('Uploaded entry!', fg='green') leaderboard_url = get_challenge_url(challenge.id) write('View the leaderboard: {}'.format(leaderboard_url), fg='green') uploaded = True upload_eligible = False else: write('The entry upload has failed', err=True, fg='red') message = 'Please check your API key on vimgolf.com' write(message, err=True, fg='red') else: break should_quit = selection == 'q' keys = None if not should_quit: if selection == 'k': keys = input_loop('Enter key sequence: ') write('Retrying vimgolf challenge', fg='yellow') return { 'should_quit': should_quit, 'uploaded': uploaded, 'keys': keys, }
def show_challenge_id_error(): write('Invalid challenge ID', err=True, fg='red') write('Please check the ID on vimgolf.com', err=True, fg='red')
def show(challenge_id): challenge_id = expand_challenge_id(challenge_id) logger.info('show(%s)', challenge_id) if not validate_challenge_id(challenge_id): show_challenge_id_error() raise Failure() try: fetched = fetch_challenge_spec_and_page(challenge_id) challenge = fetched['challenge'] page_response = fetched['page'] page_url = fetched['url'] data = extract_data_from_page(page_response.body) name = data['name'] description = data['description'] leaders = data['leaders'] challenge.update_metadata(name, description) metadata = challenge.metadata start_file = challenge.in_text if not start_file.endswith('\n'): start_file += '\n' end_file = challenge.out_text if not end_file.endswith('\n'): end_file += '\n' separator = '-' * 50 write(separator) write('{} ('.format(name), nl=False) write(challenge_id, fg='yellow', nl=False) write(')') write(separator) write(page_url) write(separator) write('Leaderboard', fg='green') if leaders: for leader in leaders[:LEADER_LIMIT]: write('{} {}'.format(leader.username.ljust(15), leader.score)) if len(leaders) > LEADER_LIMIT: write('...') else: write('no entries yet', fg='yellow') write(separator) write(description) write(separator) write('Start File', fg='green') write(start_file, nl=False) write(separator) write('End File', fg='green') write(end_file, nl=False) write(separator) write('Stats', fg='green') write('Entered Solutions: {}'.format(metadata['answers'])) write('Uploaded: {}'.format(metadata['uploaded'])) write('Correct Solutions: {}'.format(metadata['correct'])) write('Self Best Score: {}'.format(metadata['best_score'])) answers = challenge.answers ignored_answer_suffix = 'ZQ' answer_rows = [['Keys', 'Correct', 'Submitted', 'Score', 'Timestamp']] for answer in answers: keys = ''.join(answer['keys']) if keys.endswith(ignored_answer_suffix): continue answer_row = [ keys, bool_to_mark(answer['correct']), bool_to_mark(answer['uploaded']), answer['score'], answer['timestamp'], ] answer_rows.append(answer_row) if len(answer_rows) > 1: write(AsciiTable(answer_rows).table) except Failure: raise except Exception: logger.exception('challenge retrieval failed') write('The challenge retrieval has failed', err=True, fg='red') write('Please check the challenge ID on vimgolf.com', err=True, fg='red') raise Failure()
def version(): """display the version number""" write(__version__)