vimgolf_gym.vimgolf
Vimgolf API wrapper
1""" 2Vimgolf API wrapper 3""" 4 5# TODO: convert vimgolf keys to terminal input string, and convert terminal input string to vimgolf keys 6# TODO: figure out if the vim -w recorded things are the same as terminal input string 7 8# TODO: figure out the action space of all three input representations 9 10import argparse 11import json 12import logging 13import typing 14from vimgolf_gym._vimrc import ( 15 _CYBERGOD_VIMGOLF_VIMRC_FILEPATH, 16 _prepare_cybergod_vimrc_with_buffer_file, 17 _VIMGOLF_VIMRC_FILEPATH, 18) 19 20from vimgolf.vimgolf import ( 21 IGNORED_KEYSTROKES, 22 Challenge, 23 Result, 24 Status, 25 VimRunner, 26 filecmp, 27 format_, 28 get_challenge_url, 29 get_keycode_repr, 30 input_loop, 31 logger, 32 os, 33 parse_keycodes, 34 sys, 35 tempfile, 36 tokenize_keycode_reprs, 37 upload_result, 38 working_directory, 39 write, 40) 41 42 43def parse_commandline_arguments(): 44 """ 45 Parse the command line arguments using argparse. 46 47 Args: 48 None 49 50 Returns: 51 A Namespace object with the parsed arguments. 52 53 The parsed arguments are: 54 - --input_file: str, the path to the input file 55 - --output_file: str, the path to the output file 56 - --log_file: str, the path to the log file 57 - --buffer_file: str, the path to the buffer file 58 - --init_keys: str, the initial keys to type into Vim 59 """ 60 61 parser = argparse.ArgumentParser() 62 parser.add_argument("--input_file", type=str, required=True) 63 parser.add_argument("--output_file", type=str, required=True) 64 parser.add_argument("--log_file", type=str, required=True) 65 parser.add_argument("--buffer_file", type=str, default=None) 66 parser.add_argument("--init_keys", type=str, default="") 67 args = parser.parse_args() 68 return args 69 70 71def main(): 72 """ 73 Parse command line arguments and execute the vimgolf local challenge. 74 75 It sets up the logging to a file and then calls the local function to execute 76 the challenge. 77 78 The local function is called with the input file, output file, buffer file and init keys. 79 80 """ 81 args = parse_commandline_arguments() 82 log_file = args.log_file 83 file_handler = logging.FileHandler(log_file) 84 file_handler.setLevel(logging.DEBUG) 85 logger.addHandler(file_handler) 86 if args.buffer_file: 87 local( 88 infile=args.input_file, 89 outfile=args.output_file, 90 buffer_file=args.buffer_file, 91 init_keys=args.init_keys, 92 ) 93 else: 94 local( 95 infile=args.input_file, 96 outfile=args.output_file, 97 init_keys=args.init_keys, 98 ) 99 100 101def local( 102 infile: str, outfile: str, buffer_file: typing.Optional[str] = None, init_keys="" 103): 104 """ 105 Execute a local VimGolf challenge. 106 107 It reads the input file, output file, and runs the challenge. The challenge 108 is defined by the input and output text and the file extensions. 109 110 Args: 111 infile: str, the path to the input file 112 outfile: str, the path to the output file 113 init_keys: str, the initial keys to type into Vim 114 buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None. 115 116 Returns: 117 Status: The status of the challenge 118 """ 119 logger.info("local(%s, %s)", infile, outfile) 120 with open(infile, "r") as f: 121 in_text = format_(f.read()) 122 with open(outfile, "r") as f: 123 out_text = format_(f.read()) 124 _, in_extension = os.path.splitext(infile) 125 _, out_extension = os.path.splitext(outfile) 126 challenge = Challenge( 127 in_text=in_text, 128 out_text=out_text, 129 in_extension=in_extension, 130 out_extension=out_extension, 131 id=None, 132 compliant=None, 133 api_key=None, 134 init_keys=init_keys, 135 ) 136 status = play(challenge, buffer_file=buffer_file) 137 return status 138 139 140def play(challenge, results=None, buffer_file: typing.Optional[str] = None): 141 """ 142 Execute a VimGolf challenge. 143 144 Args: 145 challenge: Challenge, the challenge to play 146 results: list of Result, the results of previous plays 147 buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None. 148 149 Returns: 150 Status: The status of the challenge 151 """ 152 if results is None: 153 results = [] 154 logger.info("play(...)") 155 with tempfile.TemporaryDirectory() as workspace, working_directory(workspace): 156 infile = "in" 157 if challenge.in_extension: 158 infile += challenge.in_extension 159 outfile = "out" 160 if challenge.out_extension: 161 outfile += challenge.out_extension 162 logfile = "log" 163 with open(outfile, "w") as f: 164 f.write(challenge.out_text) 165 166 try: 167 # If there were init keys specified, we need to convert them to a 168 # form suitable for feedkeys(). We can't use Vim's -s option since 169 # it takes escape codes, not key codes. See Vim #4041 and TODO.txt 170 # ("Bug: script written with "-W scriptout" contains Key codes, 171 # while the script read with "-s scriptin" expects escape codes"). 172 # The conversion is conducted here so that we can fail fast on 173 # error (prior to playing) and to avoid repeated computation. 174 keycode_reprs = tokenize_keycode_reprs(challenge.init_keys) 175 init_feedkeys = [] 176 for item in keycode_reprs: 177 if item == "\\": 178 item = "\\\\" # Replace '\' with '\\' 179 elif item == '"': 180 item = '\\"' # Replace '"' with '\"' 181 elif item.startswith("<") and item.endswith(">"): 182 item = "\\" + item # Escape special keys ("<left>" -> "\<left>") 183 init_feedkeys.append(item) 184 init_feedkeys = "".join(init_feedkeys) 185 except Exception: 186 logger.exception("invalid init keys") 187 write("Invalid keys: {}".format(challenge.init_keys), color="red") 188 return Status.FAILURE 189 190 write("Launching vimgolf session", color="yellow") 191 while True: 192 with open(infile, "w") as f: 193 f.write(challenge.in_text) 194 with open(outfile, "w") as f: 195 f.write(challenge.out_text) 196 if buffer_file: 197 _prepare_cybergod_vimrc_with_buffer_file(buffer_file) 198 vimrc = _CYBERGOD_VIMGOLF_VIMRC_FILEPATH 199 else: 200 vimrc = _VIMGOLF_VIMRC_FILEPATH 201 play_args = [ 202 "-Z", # restricted mode, utilities not allowed 203 "-n", # no swap file, memory only editing 204 "--noplugin", # no plugins 205 "-i", 206 "NONE", # don't load .viminfo (e.g., has saved macros, etc.) 207 "+0", # start on line 0 208 "-u", 209 vimrc, # vimgolf .vimrc 210 "-U", 211 "NONE", # don't load .gvimrc 212 "-W", 213 logfile, # keylog file (overwrites existing) 214 '+call feedkeys("{}", "t")'.format(init_feedkeys), # initial keys 215 infile, 216 ] 217 if VimRunner.run(play_args) != Status.SUCCESS: 218 return Status.FAILURE 219 220 correct = filecmp.cmp(infile, outfile, shallow=False) 221 logger.info("correct: %s", str(correct).lower()) 222 with open(logfile, "rb") as _f: 223 # raw keypress representation saved by vim's -w 224 raw_keys = _f.read() 225 226 # list of parsed keycode byte strings 227 keycodes = parse_keycodes(raw_keys) 228 keycodes = [ 229 keycode for keycode in keycodes if keycode not in IGNORED_KEYSTROKES 230 ] 231 232 # list of human-readable key strings 233 keycode_reprs = [get_keycode_repr(keycode) for keycode in keycodes] 234 logger.info("keys: %s", "".join(keycode_reprs)) 235 236 score = len(keycodes) 237 logger.info("score: %d", score) 238 239 result = Result(correct=correct, keys="".join(keycode_reprs), score=score) 240 logger.info( 241 json.dumps( 242 dict( 243 event_type="vimgolf_result", 244 event_data=dict( 245 correct=correct, keys="".join(keycode_reprs), score=score 246 ), 247 ) 248 ) 249 ) 250 results.append(result) 251 252 write("Here are your keystrokes:", color="green") 253 for keycode_repr in keycode_reprs: 254 color = "magenta" if len(keycode_repr) > 1 else None 255 write(keycode_repr, color=color, end=None) 256 write("") 257 258 if correct: 259 write("Success! Your output matches.", color="green") 260 write("Your score:", color="green") 261 else: 262 write( 263 "Uh oh, looks like your entry does not match the desired output.", 264 color="red", 265 ) 266 write("Your score for this failed attempt:", color="red") 267 write(score) 268 269 upload_eligible = challenge.id and challenge.compliant and challenge.api_key 270 271 while True: 272 # Generate the menu items inside the loop since it can change across iterations 273 # (e.g., upload option can be removed) 274 with open(infile, "w") as f: 275 f.write(challenge.in_text) 276 with open(outfile, "w") as f: 277 f.write(challenge.out_text) 278 menu = [] 279 if not correct: 280 menu.append(("d", "Show diff")) 281 if upload_eligible and correct: 282 menu.append(("w", "Upload result")) 283 menu.append(("r", "Retry the current challenge")) 284 menu.append(("q", "Quit vimgolf")) 285 valid_codes = [x[0] for x in menu] 286 for option in menu: 287 write("[{}] {}".format(*option), color="yellow") 288 selection = input_loop("Choice> ") 289 if selection not in valid_codes: 290 write( 291 "Invalid selection: {}".format(selection), 292 stream=sys.stderr, 293 color="red", 294 ) 295 elif selection == "d": 296 # diffsplit is used instead of 'vim -d' to avoid the "2 files to edit" message. 297 diff_args = [ 298 "-n", 299 outfile, 300 "-c", 301 "vertical diffsplit {}".format(infile), 302 ] 303 if VimRunner.run(diff_args) != Status.SUCCESS: 304 return Status.FAILURE 305 elif selection == "w": 306 upload_status = upload_result( 307 challenge.id, challenge.api_key, raw_keys 308 ) 309 if upload_status == Status.SUCCESS: 310 write("Uploaded entry!", color="green") 311 leaderboard_url = get_challenge_url(challenge.id) 312 write( 313 "View the leaderboard: {}".format(leaderboard_url), 314 color="green", 315 ) 316 upload_eligible = False 317 else: 318 write( 319 "The entry upload has failed", 320 stream=sys.stderr, 321 color="red", 322 ) 323 message = "Please check your API key on vimgolf.com" 324 write(message, stream=sys.stderr, color="red") 325 else: 326 break 327 if selection == "q": 328 break 329 write("Retrying vimgolf challenge", color="yellow") 330 331 write("Thanks for playing!", color="green") 332 return Status.SUCCESS 333 334 335if __name__ == "__main__": 336 main()
def
parse_commandline_arguments():
44def parse_commandline_arguments(): 45 """ 46 Parse the command line arguments using argparse. 47 48 Args: 49 None 50 51 Returns: 52 A Namespace object with the parsed arguments. 53 54 The parsed arguments are: 55 - --input_file: str, the path to the input file 56 - --output_file: str, the path to the output file 57 - --log_file: str, the path to the log file 58 - --buffer_file: str, the path to the buffer file 59 - --init_keys: str, the initial keys to type into Vim 60 """ 61 62 parser = argparse.ArgumentParser() 63 parser.add_argument("--input_file", type=str, required=True) 64 parser.add_argument("--output_file", type=str, required=True) 65 parser.add_argument("--log_file", type=str, required=True) 66 parser.add_argument("--buffer_file", type=str, default=None) 67 parser.add_argument("--init_keys", type=str, default="") 68 args = parser.parse_args() 69 return args
Parse the command line arguments using argparse.
Arguments:
- None
Returns:
A Namespace object with the parsed arguments.
The parsed arguments are:
- --input_file: str, the path to the input file
- --output_file: str, the path to the output file
- --log_file: str, the path to the log file
- --buffer_file: str, the path to the buffer file
- --init_keys: str, the initial keys to type into Vim
def
main():
72def main(): 73 """ 74 Parse command line arguments and execute the vimgolf local challenge. 75 76 It sets up the logging to a file and then calls the local function to execute 77 the challenge. 78 79 The local function is called with the input file, output file, buffer file and init keys. 80 81 """ 82 args = parse_commandline_arguments() 83 log_file = args.log_file 84 file_handler = logging.FileHandler(log_file) 85 file_handler.setLevel(logging.DEBUG) 86 logger.addHandler(file_handler) 87 if args.buffer_file: 88 local( 89 infile=args.input_file, 90 outfile=args.output_file, 91 buffer_file=args.buffer_file, 92 init_keys=args.init_keys, 93 ) 94 else: 95 local( 96 infile=args.input_file, 97 outfile=args.output_file, 98 init_keys=args.init_keys, 99 )
Parse command line arguments and execute the vimgolf local challenge.
It sets up the logging to a file and then calls the local function to execute the challenge.
The local function is called with the input file, output file, buffer file and init keys.
def
local( infile: str, outfile: str, buffer_file: Optional[str] = None, init_keys=''):
102def local( 103 infile: str, outfile: str, buffer_file: typing.Optional[str] = None, init_keys="" 104): 105 """ 106 Execute a local VimGolf challenge. 107 108 It reads the input file, output file, and runs the challenge. The challenge 109 is defined by the input and output text and the file extensions. 110 111 Args: 112 infile: str, the path to the input file 113 outfile: str, the path to the output file 114 init_keys: str, the initial keys to type into Vim 115 buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None. 116 117 Returns: 118 Status: The status of the challenge 119 """ 120 logger.info("local(%s, %s)", infile, outfile) 121 with open(infile, "r") as f: 122 in_text = format_(f.read()) 123 with open(outfile, "r") as f: 124 out_text = format_(f.read()) 125 _, in_extension = os.path.splitext(infile) 126 _, out_extension = os.path.splitext(outfile) 127 challenge = Challenge( 128 in_text=in_text, 129 out_text=out_text, 130 in_extension=in_extension, 131 out_extension=out_extension, 132 id=None, 133 compliant=None, 134 api_key=None, 135 init_keys=init_keys, 136 ) 137 status = play(challenge, buffer_file=buffer_file) 138 return status
Execute a local VimGolf challenge.
It reads the input file, output file, and runs the challenge. The challenge is defined by the input and output text and the file extensions.
Arguments:
- infile: str, the path to the input file
- outfile: str, the path to the output file
- init_keys: str, the initial keys to type into Vim
- buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None.
Returns:
Status: The status of the challenge
def
play(challenge, results=None, buffer_file: Optional[str] = None):
141def play(challenge, results=None, buffer_file: typing.Optional[str] = None): 142 """ 143 Execute a VimGolf challenge. 144 145 Args: 146 challenge: Challenge, the challenge to play 147 results: list of Result, the results of previous plays 148 buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None. 149 150 Returns: 151 Status: The status of the challenge 152 """ 153 if results is None: 154 results = [] 155 logger.info("play(...)") 156 with tempfile.TemporaryDirectory() as workspace, working_directory(workspace): 157 infile = "in" 158 if challenge.in_extension: 159 infile += challenge.in_extension 160 outfile = "out" 161 if challenge.out_extension: 162 outfile += challenge.out_extension 163 logfile = "log" 164 with open(outfile, "w") as f: 165 f.write(challenge.out_text) 166 167 try: 168 # If there were init keys specified, we need to convert them to a 169 # form suitable for feedkeys(). We can't use Vim's -s option since 170 # it takes escape codes, not key codes. See Vim #4041 and TODO.txt 171 # ("Bug: script written with "-W scriptout" contains Key codes, 172 # while the script read with "-s scriptin" expects escape codes"). 173 # The conversion is conducted here so that we can fail fast on 174 # error (prior to playing) and to avoid repeated computation. 175 keycode_reprs = tokenize_keycode_reprs(challenge.init_keys) 176 init_feedkeys = [] 177 for item in keycode_reprs: 178 if item == "\\": 179 item = "\\\\" # Replace '\' with '\\' 180 elif item == '"': 181 item = '\\"' # Replace '"' with '\"' 182 elif item.startswith("<") and item.endswith(">"): 183 item = "\\" + item # Escape special keys ("<left>" -> "\<left>") 184 init_feedkeys.append(item) 185 init_feedkeys = "".join(init_feedkeys) 186 except Exception: 187 logger.exception("invalid init keys") 188 write("Invalid keys: {}".format(challenge.init_keys), color="red") 189 return Status.FAILURE 190 191 write("Launching vimgolf session", color="yellow") 192 while True: 193 with open(infile, "w") as f: 194 f.write(challenge.in_text) 195 with open(outfile, "w") as f: 196 f.write(challenge.out_text) 197 if buffer_file: 198 _prepare_cybergod_vimrc_with_buffer_file(buffer_file) 199 vimrc = _CYBERGOD_VIMGOLF_VIMRC_FILEPATH 200 else: 201 vimrc = _VIMGOLF_VIMRC_FILEPATH 202 play_args = [ 203 "-Z", # restricted mode, utilities not allowed 204 "-n", # no swap file, memory only editing 205 "--noplugin", # no plugins 206 "-i", 207 "NONE", # don't load .viminfo (e.g., has saved macros, etc.) 208 "+0", # start on line 0 209 "-u", 210 vimrc, # vimgolf .vimrc 211 "-U", 212 "NONE", # don't load .gvimrc 213 "-W", 214 logfile, # keylog file (overwrites existing) 215 '+call feedkeys("{}", "t")'.format(init_feedkeys), # initial keys 216 infile, 217 ] 218 if VimRunner.run(play_args) != Status.SUCCESS: 219 return Status.FAILURE 220 221 correct = filecmp.cmp(infile, outfile, shallow=False) 222 logger.info("correct: %s", str(correct).lower()) 223 with open(logfile, "rb") as _f: 224 # raw keypress representation saved by vim's -w 225 raw_keys = _f.read() 226 227 # list of parsed keycode byte strings 228 keycodes = parse_keycodes(raw_keys) 229 keycodes = [ 230 keycode for keycode in keycodes if keycode not in IGNORED_KEYSTROKES 231 ] 232 233 # list of human-readable key strings 234 keycode_reprs = [get_keycode_repr(keycode) for keycode in keycodes] 235 logger.info("keys: %s", "".join(keycode_reprs)) 236 237 score = len(keycodes) 238 logger.info("score: %d", score) 239 240 result = Result(correct=correct, keys="".join(keycode_reprs), score=score) 241 logger.info( 242 json.dumps( 243 dict( 244 event_type="vimgolf_result", 245 event_data=dict( 246 correct=correct, keys="".join(keycode_reprs), score=score 247 ), 248 ) 249 ) 250 ) 251 results.append(result) 252 253 write("Here are your keystrokes:", color="green") 254 for keycode_repr in keycode_reprs: 255 color = "magenta" if len(keycode_repr) > 1 else None 256 write(keycode_repr, color=color, end=None) 257 write("") 258 259 if correct: 260 write("Success! Your output matches.", color="green") 261 write("Your score:", color="green") 262 else: 263 write( 264 "Uh oh, looks like your entry does not match the desired output.", 265 color="red", 266 ) 267 write("Your score for this failed attempt:", color="red") 268 write(score) 269 270 upload_eligible = challenge.id and challenge.compliant and challenge.api_key 271 272 while True: 273 # Generate the menu items inside the loop since it can change across iterations 274 # (e.g., upload option can be removed) 275 with open(infile, "w") as f: 276 f.write(challenge.in_text) 277 with open(outfile, "w") as f: 278 f.write(challenge.out_text) 279 menu = [] 280 if not correct: 281 menu.append(("d", "Show diff")) 282 if upload_eligible and correct: 283 menu.append(("w", "Upload result")) 284 menu.append(("r", "Retry the current challenge")) 285 menu.append(("q", "Quit vimgolf")) 286 valid_codes = [x[0] for x in menu] 287 for option in menu: 288 write("[{}] {}".format(*option), color="yellow") 289 selection = input_loop("Choice> ") 290 if selection not in valid_codes: 291 write( 292 "Invalid selection: {}".format(selection), 293 stream=sys.stderr, 294 color="red", 295 ) 296 elif selection == "d": 297 # diffsplit is used instead of 'vim -d' to avoid the "2 files to edit" message. 298 diff_args = [ 299 "-n", 300 outfile, 301 "-c", 302 "vertical diffsplit {}".format(infile), 303 ] 304 if VimRunner.run(diff_args) != Status.SUCCESS: 305 return Status.FAILURE 306 elif selection == "w": 307 upload_status = upload_result( 308 challenge.id, challenge.api_key, raw_keys 309 ) 310 if upload_status == Status.SUCCESS: 311 write("Uploaded entry!", color="green") 312 leaderboard_url = get_challenge_url(challenge.id) 313 write( 314 "View the leaderboard: {}".format(leaderboard_url), 315 color="green", 316 ) 317 upload_eligible = False 318 else: 319 write( 320 "The entry upload has failed", 321 stream=sys.stderr, 322 color="red", 323 ) 324 message = "Please check your API key on vimgolf.com" 325 write(message, stream=sys.stderr, color="red") 326 else: 327 break 328 if selection == "q": 329 break 330 write("Retrying vimgolf challenge", color="yellow") 331 332 write("Thanks for playing!", color="green") 333 return Status.SUCCESS
Execute a VimGolf challenge.
Arguments:
- challenge: Challenge, the challenge to play
- results: list of Result, the results of previous plays
- buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None.
Returns:
Status: The status of the challenge