diff --git a/2048.cpp b/2048.cpp index d1124a9..313d134 100644 --- a/2048.cpp +++ b/2048.cpp @@ -224,7 +224,7 @@ static inline board_t execute_move_3(board_t board) { } /* Execute a move. */ -static inline board_t execute_move(int move, board_t board) { +board_t execute_move(int move, board_t board) { switch(move) { case 0: // up return execute_move_0(board); diff --git a/2048.h b/2048.h index 2f0827f..61d3bf0 100644 --- a/2048.h +++ b/2048.h @@ -52,6 +52,7 @@ extern "C" { #endif DLL_PUBLIC void init_tables(); +DLL_PUBLIC board_t execute_move(int move, board_t board); typedef int (*get_move_func_t)(board_t); DLL_PUBLIC float score_toplevel_move(board_t board, int move); diff --git a/2048.py b/2048.py index 3b928a2..c297137 100755 --- a/2048.py +++ b/2048.py @@ -4,50 +4,21 @@ ''' Help the user achieve a high score in a real game of 2048 by using a move searcher. ''' from __future__ import print_function -import ctypes import time -import os + +from ailib import ailib, to_c_board, from_c_index # Enable multithreading? MULTITHREAD = True -for suffix in ['so', 'dll', 'dylib']: - dllfn = 'bin/2048.' + suffix - if not os.path.isfile(dllfn): - continue - ailib = ctypes.CDLL(dllfn) - break -else: - print("Couldn't find 2048 library bin/2048.{so,dll,dylib}! Make sure to build it first.") - exit() - -ailib.init_tables() - -ailib.find_best_move.argtypes = [ctypes.c_uint64] -ailib.score_toplevel_move.argtypes = [ctypes.c_uint64, ctypes.c_int] -ailib.score_toplevel_move.restype = ctypes.c_float - -def to_c_board(m): - board = 0 - i = 0 - for row in m: - for c in row: - board |= int(c) << (4*i) - i += 1 - return board - def print_board(m): for row in m: for c in row: print('%8d' % c, end=' ') print() -def _to_val(c): - if c == 0: return 0 - return 2**c - def to_val(m): - return [[_to_val(c) for c in row] for row in m] + return [[from_c_index(c) for c in row] for row in m] def _to_score(c): if c <= 1: @@ -110,7 +81,7 @@ def parse_args(argv): parser = argparse.ArgumentParser(description="Use the AI to play 2048 via browser control") parser.add_argument('-p', '--port', help="Port number to control on (default: 32000 for Firefox, 9222 for Chrome)", type=int) - parser.add_argument('-b', '--browser', help="Browser you're using. Only Firefox with remote debugging, Firefox with the Remote Control extension (deprecated), and Chrome with remote debugging, are supported right now.", default='firefox', choices=('firefox', 'firefox-rc', 'chrome')) + parser.add_argument('-b', '--browser', help="Browser you're using. Only Firefox with remote debugging, Firefox with the Remote Control extension (deprecated), and Chrome with remote debugging, are supported right now.", default='firefox', choices=('firefox', 'firefox-rc', 'chrome', 'manual')) parser.add_argument('-k', '--ctrlmode', help="Control mode to use. If the browser control doesn't seem to work, try changing this.", default='hybrid', choices=('keyboard', 'fast', 'hybrid')) return parser.parse_args(argv) @@ -134,7 +105,10 @@ def main(argv): args.port = 9222 ctrl = ChromeDebuggerControl(args.port) - if args.ctrlmode == 'keyboard': + if args.browser == 'manual': + from manualctrl import ManualControl + gamectrl = ManualControl() + elif args.ctrlmode == 'keyboard': from gamectrl import Keyboard2048Control gamectrl = Keyboard2048Control(ctrl) elif args.ctrlmode == 'fast': diff --git a/README.md b/README.md index b568b5e..e5784f8 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,57 @@ Open the game in a new tab, then run `2048.py -b firefox` and watch the game! Th Enable Chrome remote debugging by quitting it and then restarting it with the `remote-debugging-port` command-line switch (e.g. `google-chrome --remote-debugging-port=9222`). Open the game in a new tab, then run `2048.py -b chrome` and watch the game! The `-p` option can be used to set the port to connect to. + +## Using the AI interactively + +You can also use `2048.py` interactively using `2048.py -b manual`. In this mode, you'll be asked to input the board, after which the AI will give its suggested move. This might be useful for getting hints while playing the game on a platform without autoplay (e.g. on a phone), or for getting the AI's analysis of a given situation. + +After each recommendation, the AI will assume you make that recommended move, and then prompt you to make any necessary adjustments to the board (usually, the location and value of the newly spawned tile) before giving its next suggestion. + +Sample run: + +``` +Enter board one row at a time, with entries separated by spaces +Row 1: 16 128 256 1024 +Row 2: 16 8 2 0 +Row 3: 8 2 0 0 +Row 4: 0 4 0 0 +Current board: + 16 128 256 1024 + 16 8 2 0 + 8 2 0 0 + 0 4 0 0 +Enter updates in the form r,c,n (1-indexed row/column), separated by spaces: + 16 128 256 1024 + 16 8 2 0 + 8 2 0 0 + 0 4 0 0 +005.030340: Score 0, Move 1: up +EXECUTE MOVE: up +Current board: + 32 128 256 1024 + 8 8 2 0 + 0 2 0 0 + 0 4 0 0 +Enter updates in the form r,c,n (1-indexed row/column), separated by spaces: 3,1,4 + 32 128 256 1024 + 8 8 2 0 + 4 2 0 0 + 0 4 0 0 +035.648508: Score 0, Move 2: left +EXECUTE MOVE: left +Current board: + 32 128 256 1024 + 16 2 0 0 + 4 2 0 0 + 4 0 0 0 +Enter updates in the form r,c,n (1-indexed row/column), separated by spaces: 4,3,2 + 32 128 256 1024 + 16 2 0 0 + 4 2 0 0 + 4 0 2 0 +058.927319: Score 0, Move 3: left +EXECUTE MOVE: left +``` + +This tells the bot that after the first move, a 4 spawned in the 3rd row, 1st column, and after the second move, a 2 spawned in the 4th row, 3rd column. diff --git a/ailib.py b/ailib.py new file mode 100644 index 0000000..4f9ef29 --- /dev/null +++ b/ailib.py @@ -0,0 +1,47 @@ +import ctypes +import os + +for suffix in ['so', 'dll', 'dylib']: + dllfn = 'bin/2048.' + suffix + if not os.path.isfile(dllfn): + continue + ailib = ctypes.CDLL(dllfn) + break +else: + print("Couldn't find 2048 library bin/2048.{so,dll,dylib}! Make sure to build it first.") + exit() + +ailib.init_tables() + +ailib.find_best_move.argtypes = [ctypes.c_uint64] +ailib.score_toplevel_move.argtypes = [ctypes.c_uint64, ctypes.c_int] +ailib.score_toplevel_move.restype = ctypes.c_float +ailib.execute_move.argtypes = [ctypes.c_int, ctypes.c_uint64] +ailib.execute_move.restype = ctypes.c_uint64 + +def to_c_board(m): + board = 0 + i = 0 + for row in m: + for c in row: + board |= int(c) << (4*i) + i += 1 + return board + +def from_c_board(n): + board = [] + i = 0 + for ri in range(4): + row = [] + for ci in range(4): + row.append((n >> (4 * i)) & 0xf) + i += 1 + board.append(row) + return board + +def to_c_index(n): + return [0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768].index(n) + +def from_c_index(c): + if c == 0: return 0 + return 2**c diff --git a/manualctrl.py b/manualctrl.py new file mode 100644 index 0000000..71146b1 --- /dev/null +++ b/manualctrl.py @@ -0,0 +1,44 @@ +from ailib import ailib, to_c_board, from_c_board, to_c_index, from_c_index +from gamectrl import Generic2048Control + +def print_board(m): + for row in m: + for c in row: + print('%8d' % from_c_index(c), end=' ') + print() + +class ManualControl(Generic2048Control): + def __init__(self): + print("Enter board one row at a time, with entries separated by spaces") + board = [] + for ri in range(4): + board.append([to_c_index(int(c)) for c in input("Row %d: " % (ri + 1)).split()]) + self.cur_board = board + + def get_status(self): + return "running" + + def restart_game(self): + print("Game over - time to restart!") + + def continue_game(self): + pass + + def get_score(self): + # don't care + return 0 + + def get_board(self): + print("Current board:") + print_board(self.cur_board) + + updates = input("Enter updates in the form r,c,n (1-indexed row/column), separated by spaces: ") + for item in updates.split(): + r, c, n = map(int, item.split(",")) + self.cur_board[r-1][c-1] = to_c_index(n) + + return self.cur_board + + def execute_move(self, move): + print("EXECUTE MOVE:", ["up", "down", "left", "right"][move]) + self.cur_board = from_c_board(ailib.execute_move(move, to_c_board(self.cur_board)))