class Form(FormPanel): def __init__(self, svc, **kwargs): self.describe_listeners = [] if kwargs.has_key('listener'): listener = kwargs.pop('listener') self.addDescribeListener(listener) if kwargs.has_key('data'): data = kwargs.pop('data') else: data = None FormPanel.__init__(self, **kwargs) self.svc = svc self.grid = Grid() self.grid.resize(0, 3) self.add(self.grid) self.describer = FormDescribeGrid(self) self.saver = FormSaveGrid(self) self.getter = FormGetGrid(self) self.formsetup(data) def addDescribeListener(self, l): self.describe_listeners.append(l) def add_widget(self, description, widget): """ adds a widget, with error rows interspersed """ num_rows = self.grid.getRowCount() self.grid.resize((num_rows+1), 3) self.grid.setHTML(num_rows, 0, description) self.grid.setWidget(num_rows, 1, widget) def get(self, **kwargs): writebr(repr(kwargs)) self.svc({}, {'get': kwargs}, self.getter) def save(self, data=None): self.clear_errors() if data is None: data = self.getValue() self.data = data writebr(repr(self.data)) self.svc(data, {'save': None}, self.saver) def save_respond(self, response): if not response['success']: errors = response['errors'] self.set_errors(errors) for l in self.describe_listeners: l.onErrors(self, errors) return for l in self.describe_listeners: l.onSaveDone(self, response) def formsetup(self, data=None): if data is None: data = {} self.data = data self.svc(data, {'describe': None}, self.describer) def clear_errors(self): for idx, fname in enumerate(self.fields): self.grid.setHTML(idx, 2, None) def set_errors(self, errors): offsets = {} for idx, fname in enumerate(self.fields): offsets[fname] = idx for k, err in errors.items(): err = "<br />".join(err) idx = offsets[k] self.grid.setHTML(idx, 2, err) def update_values(self, data = None): if data is not None: self.data = data for idx, fname in enumerate(self.fields): val = None if self.data.has_key(fname): val = self.data[fname] w = self.grid.getWidget(idx, 1) w.setValue(val) def do_get(self, response): fields = response.get('instance', None) if fields: self.update_values(fields) for l in self.describe_listeners: l.onRetrieveDone(self, fields) def do_describe(self, fields): self.fields = fields.keys() for idx, fname in enumerate(self.fields): field = fields[fname] if self.data and self.data.has_key(fname): field['initial'] = self.data[fname] field_type = field['type'] widget_kls = widget_factory.get(field_type, CharField) fv = {} for (k, v) in field.items(): fv[str(k)] = v w = widget_kls(**fv) self.add_widget(field['label'], w) for l in self.describe_listeners: l.onDescribeDone(self) def getValue(self): res = {} for idx, fname in enumerate(self.fields): w = self.grid.getWidget(idx, 1) val = w.getValue() res[fname] = val self.data[fname] = val return res
class InputBox(FocusPanel): _props = [ ("maxLength", "Max Length", "MaxLength", int), ("text", "Text", "Text", None), ("matchPattern", "Match Pattern", "MatchPattern", None), ("cursorStyle", "Cursor Style", "CursorStyle", None), ] @classmethod def _getProps(self): return FocusPanel._getProps() + self._props def __init__(self, **kwargs): """ setMatchPattern - defaults to '' to match everything match pattern examples: '^[0-9]*$' is for digits only '^[0-9,A-Z]*$' is for digits and uppercase setMaxLength setText OB """ kwargs['MatchPattern'] = kwargs.pop('MatchPattern', '') cs = kwargs.pop('CursorStyle', "inputbox-cursor") gs = kwargs.pop('StyleName', 'gwt-inputbox') ap = AbsolutePanel(StyleName="inputbox") self.tp = Grid(StyleName=gs, Width="100%", Height="100%", CellPadding=0, CellSpacing=0) self.cursor = HTML(StyleName=cs) ap.add(self.tp) ap.add(self.cursor, 0, 0) self.cf = self.tp.getCellFormatter() FocusPanel.__init__(self, Widget=ap, **kwargs) self.addTableListener(self) self.addKeyboardListener(self) self.addFocusListener(self) self.word_selected_pos = 0 self.ctimer = Timer(notify=self.cursorFlash) self.focusset = False self.cstate = False self._keypressListeners = [] def addKeypressListener(self, listener): self._keypressListeners.append(listener) def removeKeypressListener(self, listener): self._keypressListeners.remove(listener) def getMatchPattern(self): return self.mp def setMatchPattern(self, mp): self.mp = mp self.rexp = re.compile(self.mp) def addDblTableListener(self, listener): self.tp.addDblTableListener(listener) def addTableListener(self, listener): self.tp.addTableListener(listener) def _move_cursor(self, col): """ moves the css-styled cursor """ #old style (useful for insert mode - don't delete this line for now!) #self.cf.setStyleName(0, col, "inputbox-square-word-cursor", highlight) el = self.cf.getRawElement(0, col+1) w = self.getWidget() px = self.tp.getAbsoluteLeft() py = self.tp.getAbsoluteTop() width = DOM.getOffsetWidth(el) px = DOM.getAbsoluteLeft(el) - px py = DOM.getAbsoluteTop(el) - py w.setWidgetPosition(self.cursor, px, py) def _highlight_cursor(self, col, highlight): """ highlights (or dehighlights) the currently selected cell """ #old style (useful for insert mode - don't delete this line for now!) #self.cf.setStyleName(0, col, "inputbox-square-word-cursor", highlight) print "_highlight", col, highlight self._move_cursor(col) self.cursor.setStyleName("inputbox-square-word-cursor", highlight) def set_grid_value(self, v, y, x): if v: w = "" else: w = "0px" v = v or EMPTY s = HTML(v, StyleName="inputbox-square") self.tp.setWidget(y, x, s) self.cf.setAlignment(y, x, HasAlignment.ALIGN_LEFT, HasAlignment.ALIGN_MIDDLE) self.cf.setWidth(y, x, w) s.setWidth(w) def onKeyDown(self, sender, keycode, modifiers): evt = DOM.eventGetCurrentEvent() DOM.eventPreventDefault(evt) if self.word_selected_pos is None: return val = chr(keycode) done = False if keycode == KeyboardListener.KEY_DELETE: self.shift_letters_back() done = True elif keycode == KeyboardListener.KEY_BACKSPACE: if not self.nasty_hack(): if self.moveCursor(-1): self.shift_letters_back() done = True elif keycode == KeyboardListener.KEY_LEFT: self.moveCursor(-1) done = True elif keycode == KeyboardListener.KEY_RIGHT: self.moveCursor(1) done = True print "onKeyDown", keycode, val, self.rexp.match(val) if not done: if self.rexp.match(val): self.press_letter(val) for listener in self._keypressListeners: listener.onKeyPressed(sender, keycode, modifiers) def press_letter(self, val): print "press letter", val # check the key is a letter, and there's a grid position highlighted if self.word_selected_pos is None: return row = 0 col = self.word_selected_pos self.highlight_cursor(False) self.set_grid_value(val, row, col) self.moveCursor(1) def nasty_hack(self): """ breaking of backspace/delete rules for the final character """ row = 0 col = self.word_selected_pos txt = self.get_char(col) if txt is None or txt == EMPTY: return False self.set_grid_value(EMPTY, row, col) return True def shift_letters_back(self): """ this function is used by del and backspace, to move the letters backwards from after the cursor. the only difference between backspace and delete is that backspace moves the cursor and _then_ does letter-moving. """ self.highlight_cursor(False) row = 0 col = self.word_selected_pos x2 = self.tp.getColumnCount()-2 while (x2 != col): txt = self.get_char(col+1) self.set_grid_value(txt, row, col) col += 1 self.set_grid_value(EMPTY, row, col) def setCursorPos(self, col): x2 = self.tp.getColumnCount()-1 col = min(x2, col) col = max(col, 0) if self.get_char(0) is None or self.get_char(0) == EMPTY: col = 0 while (self.get_char(col-1) is None or \ self.get_char(col-1) == EMPTY) and col > 1: col -= 1 self._move_cursor(col) self.highlight_cursor(False) self.word_selected_pos = col self.highlight_cursor(self.focusset) return True def getCursorPos(self): return self.word_selected_pos def moveCursor(self, dirn): x2 = self.tp.getColumnCount()-1 row = 0 col = self.word_selected_pos if dirn == 1 and x2 == col+1: return False if dirn == -1 and 0 == col+1: return False return self.setCursorPos(col+dirn) def onKeyUp(self, sender, keycode, modifiers): evt = DOM.eventGetCurrentEvent() DOM.eventCancelBubble(evt, True) DOM.eventPreventDefault(evt) def onKeyPress(self, sender, keycode, modifiers): evt = DOM.eventGetCurrentEvent() DOM.eventPreventDefault(evt) def highlight_cursor(self, highlight): """ highlights (or dehighlights) the currently selected cell """ if self.word_selected_pos is None: return col = self.word_selected_pos self._highlight_cursor(col, highlight) def getMaxLength(self): return self.tp.getColumnCount()-1 def setMaxLength(self, x): l = max(0, self.tp.getColumnCount()-1) self.tp.resize(1, x+1) self.clear(l) def clear(self, fromrange=0): for i in range(fromrange, self.tp.getColumnCount()): self.set_grid_value(EMPTY, 0, i) self.cf.setWidth(0, self.tp.getColumnCount()-1, "100%") def onCellClicked(self, listener, row, col, direction=None): self.setCursorPos(col) def cursorFlash(self, timr): if not self.focusset: return self.highlight_cursor(self.cstate) self.cstate = not self.cstate def onFocus(self, sender): print "onFocus", sender self.focusset = True self.ctimer.scheduleRepeating(800) def onLostFocus(self, sender): print "onLostFocus", sender self.focusset = False self.ctimer.cancel() self.highlight_cursor(False) def get_char(self, x): w = self.tp.getWidget(0, x) return w and w.getHTML() def getText(self): txt = '' for i in range(self.tp.getColumnCount()-1): c = self.get_char(i) if c is None or c == EMPTY: break txt += c return txt def setText(self, txt): self.highlight_cursor(False) self.clear() txt = txt[:self.getMaxLength()] for (i, c) in enumerate(txt): self.set_grid_value(c, 0, i) self.setCursorPos(min(self.getMaxLength()-1, len(txt)))
class GridWidget(AbsolutePanel): def __init__(self): self.state = State() self.game_over = False self.TD_CONSTS = {'c3': 0.767944, 'c2': 1.049451, 'c1': 3.074038, 'c6': 0.220823, 'c5': 0.281883, 'c4': 0.605861} AbsolutePanel.__init__(self) StyleSheetCssText(margins) # initialize css... self.welcome_label = HTML('<H2 align="center">Welcome to Meta-Tic-Tac-Toe!</H2><p>Play first by clicking on one of the positions in the middle board or let the AI go first by clicking on "AI first". To change the difficulty click on "Increase/Decrease search depth". Note: if there is a pop-up saying that the script is taking a long time to complete, this is not a bug - the AI is just taking a while to find the next move. Select the option to continue the script.</p>', StyleName='margins_both') self.add(self.welcome_label) self.depthLimit = 3 self.human_first = True self.ai_first = Button("AI first.", self, StyleName='margins_left') self.add(self.ai_first) self.increase_depth = Button("Increase search depth", self) self.decrease_depth = Button("Decrease search depth", self) self.depth_label = HTML("""AI will search to a <a href="#depth_explanation">depth</a> of """ + str(self.depthLimit) +".") self.depth_grid = Grid(StyleName='margins_left') self.depth_grid.resize(1, 3) self.depth_grid.setBorderWidth(2) self.depth_grid.setCellPadding(9) self.depth_grid.setCellSpacing(1) self.add(self.depth_grid) self.depth_grid.setWidget(0, 0, self.decrease_depth) self.depth_grid.setWidget(0, 1, self.depth_label) self.depth_grid.setWidget(0, 2, self.increase_depth) self.new_game = Button("New game", self, StyleName='margins_left') self.add(self.new_game) self.score_label = Label("CURRENT SCORE: Human: %d | AI: %d"% (0,0), StyleName='margins_left') self.add(self.score_label) self.game_over_msg = HTML("", StyleName='margins_left') self.add(self.game_over_msg) # initialize the board grid: self.g=Grid(StyleName='margins_left') self.g.resize(3, 3) self.g.setBorderWidth(2) self.g.setCellPadding(9) self.g.setCellSpacing(1) self.init() self.add(self.g) # initialize the contstants adjustment grid: self.adj_grid = Grid(StyleName='margins_left') self.adj_grid.resize(7, 3) self.adj_grid.setBorderWidth(2) self.adj_grid.setCellPadding(9) self.adj_grid.setCellSpacing(1) self.init_constants_adj_grid() self.add(self.adj_grid) self.max_player = '-1' self.min_player = '-1' self.state_to_grid() def init_constants_adj_grid(self): '''Initializes the grid that allows the TD_CONSTS to be adjusted. ''' self.decr_buttons = {} self.adj_labels = {} self.incr_buttons = {} td_keys = ['c1', 'c2', 'c3', 'c4', 'c5', 'c6'] self.adj_grid.setWidget(0, 1, HTML('''Adjust the <a href="#utility_function">constants</a> to change<br>the AI's behavior.''')) for i, key in enumerate(td_keys): j = i + 1 self.decr_buttons[key] = Button('<', self) self.adj_grid.setWidget(j, 0, self.decr_buttons[key]) self.incr_buttons[key] = Button('>', self) self.adj_grid.setWidget(j, 2, self.incr_buttons[key]) self.adj_labels[key] = Label("Constant %d: %f" % (key[1], self.TD_CONSTS[key])) self.adj_grid.setWidget(j, 1, self.adj_labels[key]) def init(self): '''Initializes the grid on which the game is played. ''' for y_board in range(3): for x_board in range(3): g=Grid() g.resize(3, 3) g.setBorderWidth(2) g.setCellPadding(9) g.setCellSpacing(1) for x_cell in range(3): for y_cell in range(3): b = Button('Play here.', self) b.point = {'x_cell':x_cell, 'y_cell':y_cell, 'y_board': y_board, 'x_board': x_board} g.setWidget(y_cell, x_cell, b) self.add(g) self.g.setWidget(y_board, x_board, g) def start_new_game(self): #g.__init__() nope, can't use this :( self.state = State() self.game_over = False self.depthLimit = 3 self.human_first = True self.ai_first.setVisible(True) # new game, so we make this button visible self.check_win() self.max_player = '-1' self.min_player = '-1' self.state_to_grid() def onClick(self, sender): if sender == self.increase_depth: self.depthLimit += 1 self.depth_label.setHTML("""AI will search to a <a href="#depth_explanation">depth</a> of """ + str(self.depthLimit) +".") if sender == self.decrease_depth: self.depthLimit -= 1 self.depth_label.setHTML("""AI will search to a <a href="#depth_explanation">depth</a> of """ + str(self.depthLimit) +".") if sender == self.new_game: self.start_new_game() self.check_adjusts(sender) if not self.game_over: if sender == self.ai_first and self.min_player == -1: # we only set min_player and max_player when they are not yet initialized self.human_first = False self.max_player = '1' self.min_player = '2' self.state.next_piece[2] = self.max_player self.ai_first.setVisible(False) # can't go first any more so we make it invisible... new_state = ab(self.state, self.TD_CONSTS, depth_limit=self.depthLimit)[1] last_position = find_last_move(self.state, new_state) self.state = new_state self.state_to_grid(prev_x_board=last_position['x_board'], prev_y_board=last_position['y_board'], prev_x_cell=last_position['x_cell'], prev_y_cell=last_position['y_cell'],) if hasattr(sender, 'point'): if self.min_player == -1: # we only set min_player and max_player when they are not yet initialized self.max_player = '2' self.min_player = '1' self.ai_first.setVisible(False) # can't go first any more so we make it invisible... point = sender.point g = self.g.getWidget(point['y_board'], point['x_board']) g.setText(point['y_cell'], point['x_cell'], str(self.min_player)) self.grid_to_state(point) self.check_win() self.state.next_piece[2] = self.max_player new_state = ab(self.state, self.TD_CONSTS, depth_limit=self.depthLimit)[1] last_position = find_last_move(self.state, new_state) self.state = new_state self.state_to_grid(prev_x_board=last_position['x_board'], prev_y_board=last_position['y_board'], prev_x_cell=last_position['x_cell'], prev_y_cell=last_position['y_cell'],) self.check_win() def check_adjusts(self, sender): td_keys = ['c1', 'c2', 'c3', 'c4', 'c5', 'c6'] for key in td_keys: if self.incr_buttons[key] == sender: self.change_td_const(key, '+') if self.decr_buttons[key] == sender: self.change_td_const(key, '-') self.adj_labels[key].setText("Constant %d: %f" % (key[1], self.TD_CONSTS[key])) def change_td_const(self, key, sign): if sign == '+': self.TD_CONSTS[key] += INCREMENT_AMOUNT elif sign == '-': self.TD_CONSTS[key] -= INCREMENT_AMOUNT def check_win(self): self.depth_label.setHTML("""AI will search to a <a href="#depth_explanation">depth</a> of """ + str(self.depthLimit) +".") self.check_adjusts(None) human_score = self.state.score[str(self.min_player)] ai_score = self.state.score[str(self.max_player)] self.score_label.setText("CURRENT SCORE: Human(%d): %d | AI(%d): %d" % (self.min_player, human_score, self.max_player, ai_score)) if is_over(self.state): if human_score > ai_score: msg = "Congratulations, you won! To increase the difficulty, increase the search depth." elif human_score < ai_score: msg = "You lost! Better luck next time." elif human_score == ai_score: msg = "Game ends in a tie." self.game_over_msg.setHTML("<H3>" + msg + "</H3>") self.game_over = True if self.game_over: self.game_over_msg.setVisible(True) else: self.game_over_msg.setVisible(False) def will_buttons(self, y_board, x_board): # first we determine if the next_piece points to a playable board. board = self.state.boards piece = list(self.state.next_piece) playable = True if is_win(board[piece[0]][piece[1]]) or is_full(board[piece[0]][piece[1]]): playable = False if (not is_win(board[y_board][x_board])) and (not is_full(board[y_board][x_board])): if not playable: return True if playable: return (y_board == piece[0]) and (x_board == piece[1]) return False def state_to_grid(self, prev_x_board=-1, prev_y_board=-1, prev_x_cell=-1, prev_y_cell=-1): board = self.state.boards for y_board in range(3): for x_board in range(3): # for this mini-grid, do i make buttons or dashes? will_make_buttons = self.will_buttons(y_board, x_board) g=Grid() g.resize(3, 3) g.setBorderWidth(2) g.setCellPadding(9) g.setCellSpacing(1) for y_cell in range(3): for x_cell in range(3): if board[y_board][x_board][y_cell][x_cell]['cell'] == 0: if will_make_buttons: if self.min_player == -1: b = Button('Play 1 here.', self) else: b = Button('Play %d here.' % (self.state.next_piece[2]), self) b.point = {'x_cell':x_cell, 'y_cell':y_cell, 'y_board': y_board, 'x_board': x_board} else: b = HTML('-') elif board[y_board][x_board][y_cell][x_cell]['cell'] == 1: if (prev_x_cell == x_cell and prev_y_cell == y_cell and prev_y_board == y_board and prev_x_board == x_board): b = HTML('<p style="color:red">1</p>') else: b = HTML('1') elif board[y_board][x_board][y_cell][x_cell]['cell'] == 2: if (prev_x_cell == x_cell and prev_y_cell == y_cell and prev_y_board == y_board and prev_x_board == x_board): b = HTML('<p style="color:red">2</p>') else: b = HTML('2') g.setWidget(y_cell, x_cell, b) self.add(g) self.g.setWidget(y_board, x_board, g) def grid_to_state(self, point): board = self.state.boards for y_board in range(3): for x_board in range(3): g = self.g.getWidget(y_board, x_board) for y_cell in range(3): for x_cell in range(3): if isinstance(g.getWidget(y_cell, x_cell), Button): assert board[y_board][x_board][y_cell][x_cell]['cell'] == 0 elif (g.getText(y_cell, x_cell) == '1') or (g.getText(y_cell, x_cell) == '2'): if self.state.boards[y_board][x_board][y_cell][x_cell]['cell'] == 0: self.state.boards[y_board][x_board][y_cell][x_cell]['cell'] = int(g.getText(y_cell, x_cell)) piece = self.state.next_piece piece[0] = y_cell piece[1] = x_cell else: assert (g.getText(y_cell, x_cell) == '-') if is_win(self.state.boards[point['y_board']][point['x_board']]): self.state.score[str(self.min_player)] += 1
class GridWidget(AbsolutePanel): def __init__(self): AbsolutePanel.__init__(self) StyleSheetCssText(margins) # initialize css... header = """<div><H2 align="center">Welcome to Unbeatable Tic-Tac-Toe!</H2><br>My <a href="https://github.com/chetweger/min-max-games/blob/master/ttt/ttt.py">implementation</a> uses the min-max search algorithm with alpha beta pruning and a transposition table!</div>""" header = HTML(header, StyleName='margins_both') self.add(header) self.ai_first = Button("AI first.", self, StyleName='margins_left') self.add(self.ai_first) self.new_game = Button("New game", self, StyleName='margins_left') self.add(self.new_game) self.g=Grid(StyleName='margins_left') self.g.resize(3, 3) self.g.setBorderWidth(2) self.g.setCellPadding(4) self.g.setCellSpacing(1) self.init() self.add(self.g) self.state = State() self.game_resolution=Label("", StyleName='margins_left') self.add(self.game_resolution) def start_new_game(self): self.state = State() self.game_over = False self.ai_first.setVisible(True) self.state_to_grid(self.state) def onClick(self, sender): if sender == self.ai_first: print 'player is ', self.state.player self.state.max_v = 1 self.state.min_v = 2 self.ai_first.setVisible(False) print 'button ai_first exists', hasattr(self, 'ai_first') self.state.print_me() next_state = ab(self.state) self.state = next_state self.state_to_grid(next_state) print '[after]player is ', self.state.player elif sender == self.new_game: self.start_new_game() else: print 'player is ', self.state.player ''' self.g.setText(0, 1, 'wassup') self.g.setText(p['x'], p['y'], str(self.state.min_v)) ''' if self.ai_first.isVisible(): print 'Setting state.max_v' self.state.max_v = 2 self.state.min_v = 1 self.ai_first.setVisible(False) p = sender.point self.g.setText(p['y'], p['x'], str(self.state.player)) self.state = self.grid_to_state() self.check_for_tie() # end 1 if is_win(self.state): self.state_to_grid(self.state, game_over=True, over_message='You won! This should not happen. This is a bug. Please email [email protected] describing the conditions of the game.') self.state.player = next_player(self.state.player) self.state.print_me() next_state = ab(self.state) self.state = next_state self.state_to_grid(next_state) self.check_for_tie() # end 1 if is_win(self.state): self.state_to_grid(self.state, game_over=True, over_message='You lost! Better luck next time.') def check_for_tie(self): if is_over(self.state): self.state_to_grid(self.state, game_over=True, over_message='The game is a tie.') def state_to_grid(self, state, game_over=False, over_message=''): if over_message: self.game_resolution.setText(over_message) self.game_resolution.setVisible(True) else: self.game_resolution.setVisible(False) board = state.board for y in range(3): for x in range(3): if board[y][x] == 0: if not game_over: b = Button('Press', self) b.point = {'x':x, 'y':y} self.g.setWidget(y, x, b) else: self.g.setText(y, x, '-') elif board[y][x] == '1': self.g.setText(y, x, '1') elif board[y][x] == '2': self.g.setText(y, x, '2') else: print 'state_to_grid exception' #assert False def grid_to_state(self): next_state = State() for y in range(3): for x in range(3): if isinstance(self.g.getWidget(y, x), Button): print y, x next_state.board[y][x] = 0 elif self.g.getText(y, x) == '1' or self.g.getText(y, x) == '2': next_state.board[y][x] = int(self.g.getText(y,x)) else: print 'grid_to_state exception' #assert False next_state.min_v = self.state.min_v next_state.max_v = self.state.max_v next_state.player = self.state.player return next_state def init(self): for y in range(3): for x in range(3): b = Button('Press', self) b.point = {'x':x, 'y':y} self.g.setWidget(y, x, b)