diff --git a/2018/may/code/first_bot.py b/2018/may/code/first_bot.py new file mode 100644 index 0000000..4c8dca9 --- /dev/null +++ b/2018/may/code/first_bot.py @@ -0,0 +1,12 @@ +while True: + board, square = [int(x) for x in input().split()] + + count = int(input()) + + moves = [] + for i in range(count): + board, square = [int(x) for x in input().split()] + moves.append((board, square)) + + move = moves[0] + print(move[0], move[1]) diff --git a/2018/may/code/heuristic-2.py b/2018/may/code/heuristic-2.py new file mode 100644 index 0000000..34bfc77 --- /dev/null +++ b/2018/may/code/heuristic-2.py @@ -0,0 +1,50 @@ +from collections import namedtuple + +Move = namedtuple("Move", "board square") + +DIAGONALS = ((0, 4, 8), (2, 4, 6)) +COLUMNS = ((0, 3, 6), (1, 4, 7), (2, 5, 8)) +ROWS = ((0, 1, 2), (3, 4, 5), (6, 7, 8)) +LINES = ROWS + COLUMNS + DIAGONALS + +CORNERS = {0, 2, 6, 8} +CENTRE = 4 + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +def evaluate(move): + current = boards[move.board].copy() + current[move.square] = 1 + + if any(sum(current[x] for x in line) == 3 for line in LINES): + return 10 + if move.square in CORNERS: + return 2 + if move.square == CENTRE: + return 1 + return 0 + + +boards = tuple([0 for i in range(9)] for i in range(9)) + +while True: + # place opponents move + enemy_move = read_move() + if enemy_move.board >= 0: + boards[enemy_move.board][enemy_move.square] = -1 + + count = int(input()) + moves = [read_move() for i in range(count)] + + move = max(moves, key=evaluate) + write_move(move) + + # place my move + boards[move.board][move.square] = 1 diff --git a/2018/may/code/heuristic-3.py b/2018/may/code/heuristic-3.py new file mode 100644 index 0000000..bdf096f --- /dev/null +++ b/2018/may/code/heuristic-3.py @@ -0,0 +1,63 @@ +from copy import deepcopy +from collections import namedtuple + +Move = namedtuple("Move", "board square") +State = namedtuple("State", "meta boards") + +DIAGONALS = ((0, 4, 8), (2, 4, 6)) +COLUMNS = ((0, 3, 6), (1, 4, 7), (2, 5, 8)) +ROWS = ((0, 1, 2), (3, 4, 5), (6, 7, 8)) +LINES = ROWS + COLUMNS + DIAGONALS + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +def has_line(board, player=1): + return any(sum(board[x] for x in line) == player * 3 for line in LINES) + + +def update(state, move, player): + current = state.boards[move.board] + current[move.square] = player + + if has_line(current, player): + state.meta[move.board] = player + + return current + + +def evaluate(move): + new_state = deepcopy(state) + new_board = update(new_state, move, 1) + + if has_line(new_state.meta): + return 100 + if has_line(new_board): + return 10 + return 0 + + +meta = [0 for i in range(9)] +boards = tuple([0 for i in range(9)] for i in range(9)) +state = State(meta, boards) + +while True: + # place opponents move + enemy_move = read_move() + if enemy_move.board >= 0: + update(state, enemy_move, -1) + + count = int(input()) + moves = [read_move() for i in range(count)] + + move = max(moves, key=evaluate) + write_move(move) + + # place my move + update(state, move, 1) diff --git a/2018/may/code/heuristic-4.py b/2018/may/code/heuristic-4.py new file mode 100644 index 0000000..95126f2 --- /dev/null +++ b/2018/may/code/heuristic-4.py @@ -0,0 +1,83 @@ +from copy import deepcopy +from collections import namedtuple + +Move = namedtuple("Move", "board square") +State = namedtuple("State", "meta boards") + +DIAGONALS = ((0, 4, 8), (2, 4, 6)) +COLUMNS = ((0, 3, 6), (1, 4, 7), (2, 5, 8)) +ROWS = ((0, 1, 2), (3, 4, 5), (6, 7, 8)) +LINES = ROWS + COLUMNS + DIAGONALS + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +def has_line(board, player=1): + return any(sum(board[x] for x in line) == player * 3 for line in LINES) + + +def under_threat(board): + return any(sum(board[x] for x in line) == -2 for line in LINES) + + +def update(state, move, player=1): + current = state.boards[move.board] + current[move.square] = player + + if has_line(current, player): + state.meta[move.board] = player + + return current + + +def evaluate(move): + new_state = deepcopy(state) + new_board = update(new_state, move) + next_board = new_state.boards[move.square] + + if has_line(new_state.meta): + return 100 + + new_state.meta[move.square] = -1 + if has_line(new_state.meta, -1) and under_threat(next_board): + return -100 + + if has_line(new_board): + return 10 + + # TODO: what if the next board is out of play (full or has a line)? + # need to check all other boards for threats + + if under_threat(next_board): + return -10 + + # TODO: should we give extra points to moves that put the current + # board under our threat? + + return 0 + + +meta = [0 for i in range(9)] +board = tuple([0 for i in range(9)] for i in range(9)) +state = State(meta, board) + +while True: + # place opponents move + enemy_move = read_move() + if enemy_move.board >= 0: + update(state, enemy_move, -1) + + count = int(input()) + moves = [read_move() for i in range(count)] + + move = max(moves, key=evaluate) + write_move(move) + + # place my move + update(state, move) diff --git a/2018/may/code/heuristic.py b/2018/may/code/heuristic.py new file mode 100644 index 0000000..2200c94 --- /dev/null +++ b/2018/may/code/heuristic.py @@ -0,0 +1,32 @@ +from collections import namedtuple + +Move = namedtuple("Move", "board square") + +CORNERS = {0, 2, 6, 8} +CENTRE = 4 + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +def evaluate(move): + if move.square in CORNERS: + return 2 + if move.square == CENTRE: + return 1 + return 0 + + +while True: + enemy_move = read_move() + + count = int(input()) + moves = [read_move() for i in range(count)] + + move = max(moves, key=evaluate) + write_move(move) diff --git a/2018/may/code/mcts.py b/2018/may/code/mcts.py new file mode 100644 index 0000000..7fbd1bf --- /dev/null +++ b/2018/may/code/mcts.py @@ -0,0 +1,164 @@ +import sys +import time +import random +import statistics +import itertools + +from copy import deepcopy +from collections import namedtuple + +Move = namedtuple("Move", "board square") +State = namedtuple("State", "meta boards") + +DIAGONALS = ((0, 4, 8), (2, 4, 6)) +COLUMNS = ((0, 3, 6), (1, 4, 7), (2, 5, 8)) +ROWS = ((0, 1, 2), (3, 4, 5), (6, 7, 8)) +LINES = ROWS + COLUMNS + DIAGONALS +DEPTH = 4 + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +def has_line(board, player=1): + return any(sum(board[x] for x in line) == player * 3 for line in LINES) + + +def under_threat(board, player=1): + return any(sum(board[x] for x in line) == player * 2 for line in LINES) + + +def game_over(meta): + return any(abs(sum(meta[x] for x in line)) == 3 for line in LINES) + + +def valid_moves(state, next_board): + # next board is playable (not captured and not full) + if (state.meta[next_board] == 0 and not all(state.boards[next_board])): + return (Move(next_board, x) for x in range(9) if state.boards[next_board][x] == 0) + + # cannot play on the next board, list valid moves on all other boards + boards = (x for x in range(9) if state.meta[x] == 0) + return (Move(b, x) for b in boards for x in range(9) if state.boards[b][x] == 0) + + +def update(state, move, player=1): + current = state.boards[move.board] + current[move.square] = player + + if has_line(current, player): + state.meta[move.board] = player + + return current + + +def evaluate(state, move, player=1): + new_state = deepcopy(state) + new_board = update(new_state, move, player) + next_board = new_state.boards[move.square] + + if has_line(new_state.meta, player): + return 100 + + new_state.meta[move.square] = -player + if has_line(new_state.meta, -player) and under_threat(next_board, -player): + return -100 + + # TODO: is this working well when multiple move has been made? + + # TODO: does it matter that we can complete one grid if the opponent + # can complete two in the same playout? + + # TODO: do all grids count the same? should we value centre and corners more? + + if has_line(new_board, player): + return 10 + + if under_threat(next_board, -player): + return -10 + + if under_threat(new_state.meta, player): + return 5 + + # free grid choice for the opponent + if new_state.meta[move.square] != 0: + return -5 + + return 0 + + +def random_play(move): + new_state = deepcopy(state) + player = 1 + + for i in range(0, DEPTH): + update(new_state, move, player) + if game_over(new_state.meta): + return player * 100 + + next_moves = list(valid_moves(new_state, move.square)) + if not next_moves: + return sum(new_state.meta) * 20 # difference in captured boards + + move = random.choice(next_moves) + player = -1 * player + + return evaluate(new_state, move) + + +def best_play(moves, start_time): + runs = 0 + scores = [0] * len(moves) + + for i, move in itertools.cycle(enumerate(moves)): + # stop when running out of time + if (time.perf_counter() - start_time) > 0.097: + break + # best score at the end of a random play is the move score + scores[i] = max(scores[i], random_play(move)) + runs += 1 + + print("runs =", runs, file=sys.stderr) + print(scores, file=sys.stderr) + best_index = max(range(len(moves)), key=lambda x: scores[x]) + return moves[best_index] + + +def report_time(times, start_time): + new_time = time.perf_counter() + times.append(new_time - start_time) + print((3 * "{:.6f} ").format(min(times), statistics.median(times), max(times)), file=sys.stderr) + + +meta = [0 for i in range(9)] +board = tuple([0 for i in range(9)] for i in range(9)) +state = State(meta, board) + +times = [] + +while True: + enemy_move = read_move() + count = int(input()) + moves = [read_move() for i in range(count)] + + start_time = time.perf_counter() + + # play first move in the centre + if enemy_move.board < 0: + move = Move(4, 4) + + # place enemy move and choose the best response + else: + update(state, enemy_move, -1) + move = best_play(moves, start_time) + + write_move(move) + report_time(times, start_time) + + # place my move + update(state, move) diff --git a/2018/may/code/minimax.py b/2018/may/code/minimax.py new file mode 100644 index 0000000..b2dba33 --- /dev/null +++ b/2018/may/code/minimax.py @@ -0,0 +1,119 @@ +from copy import deepcopy +from collections import namedtuple + +Move = namedtuple("Move", "board square") +State = namedtuple("State", "meta boards") + +DIAGONALS = ((0, 4, 8), (2, 4, 6)) +COLUMNS = ((0, 3, 6), (1, 4, 7), (2, 5, 8)) +ROWS = ((0, 1, 2), (3, 4, 5), (6, 7, 8)) +LINES = ROWS + COLUMNS + DIAGONALS + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +def has_line(board, player=1): + return any(sum(board[x] for x in line) == player * 3 for line in LINES) + + +def under_threat(board, player=1): + return any(sum(board[x] for x in line) == player * 2 for line in LINES) + + +def game_over(meta): + return any(abs(sum(meta[x] for x in line)) == 3 for line in LINES) + + +def valid_moves(state, next_board): + if game_over(state.meta): + return [] + + # next board is playable (not captured and not full) + if (state.meta[next_board] == 0 and not all(state.boards[next_board])): + return (Move(next_board, x) for x in range(9) if state.boards[next_board][x] == 0) + + # cannot play on the next board, list valid moves on all other boards + boards = (x for x in range(9) if state.meta[x] == 0) + return (Move(b, x) for b in boards for x in range(9) if state.boards[b][x] == 0) + + +def update(state, move, player=1): + current = state.boards[move.board] + current[move.square] = player + + if has_line(current, player): + state.meta[move.board] = player + + return current + + +def evaluate(state, move, player): + new_state = deepcopy(state) + new_board = update(new_state, move, player) + next_board = new_state.boards[move.square] + + if has_line(new_state.meta, player): + return 100 + + new_state.meta[move.square] = -player + if has_line(new_state.meta, -player) and under_threat(next_board, -player): + return -100 + + if has_line(new_board, player): + return 10 + + if under_threat(next_board, -player): + return -10 + + if under_threat(new_state.meta, player): + return 5 + + # free grid choice for the opponent + if new_state.meta[move.square] != 0: + return -5 + + # TODO: should we add up all the positives and negatives together? + + # TODO: what if the opponent plays defensively? how to value the moves then? + + return 0 + + +def best_enemy_play(move): + new_state = deepcopy(state) + update(new_state, move) + + best = -1000 + for enemy_move in valid_moves(new_state, move.square): + best = max(best, evaluate(new_state, enemy_move, -1)) + return best + + +meta = [0 for i in range(9)] +board = tuple([0 for i in range(9)] for i in range(9)) +state = State(meta, board) + +while True: + enemy_move = read_move() + count = int(input()) + moves = [read_move() for i in range(count)] + + # play first move in the centre + if enemy_move.board < 0: + move = Move(4, 4) + + # place enemy move and choose response + else: + update(state, enemy_move, -1) + move = min(moves, key=best_enemy_play) + + write_move(move) + + # place my move + update(state, move) diff --git a/2018/may/code/random_bot.py b/2018/may/code/random_bot.py new file mode 100644 index 0000000..2d7cfae --- /dev/null +++ b/2018/may/code/random_bot.py @@ -0,0 +1,22 @@ +import random + +from collections import namedtuple + +Move = namedtuple("Move", "board square") + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) + + +while True: + enemy_move = read_move() + + count = int(input()) + moves = [read_move() for i in range(count)] + + write_move(random.choice(moves)) diff --git a/2018/may/runner.jar b/2018/may/runner.jar new file mode 100644 index 0000000..b6389d2 Binary files /dev/null and b/2018/may/runner.jar differ diff --git a/2018/may/slides/Makefile b/2018/may/slides/Makefile new file mode 100644 index 0000000..ff72be0 --- /dev/null +++ b/2018/may/slides/Makefile @@ -0,0 +1,13 @@ +all: slides.html + +%.html: %.md + pandoc -s -o $@ -t dzslides --normalize --mathml --no-highlight --tab-stop=2 --data-dir=. $< + + +IMAGES = codingame ai hacker board board_simple board_coordinates + +minify: $(addprefix resources/, $(addsuffix .svg, $(IMAGES))) + $(foreach IMAGE, $^, \ + scour -q -i $(IMAGE) -o $(IMAGE).min --create-groups --enable-id-stripping --enable-comment-stripping --enable-viewboxing --remove-metadata --set-precision=3; \ + mv $(IMAGE).min $(IMAGE); \ + ) diff --git a/2018/may/slides/resources/ai.svg b/2018/may/slides/resources/ai.svg new file mode 100644 index 0000000..2a383e0 --- /dev/null +++ b/2018/may/slides/resources/ai.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/may/slides/resources/board.svg b/2018/may/slides/resources/board.svg new file mode 100644 index 0000000..5e8de93 --- /dev/null +++ b/2018/may/slides/resources/board.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/may/slides/resources/board_coordinates.svg b/2018/may/slides/resources/board_coordinates.svg new file mode 100644 index 0000000..52b5b35 --- /dev/null +++ b/2018/may/slides/resources/board_coordinates.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/may/slides/resources/board_simple.svg b/2018/may/slides/resources/board_simple.svg new file mode 100644 index 0000000..51a9a29 --- /dev/null +++ b/2018/may/slides/resources/board_simple.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/2018/may/slides/resources/cc-by-sa.svg b/2018/may/slides/resources/cc-by-sa.svg new file mode 100644 index 0000000..622efb8 --- /dev/null +++ b/2018/may/slides/resources/cc-by-sa.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/may/slides/resources/codingame.svg b/2018/may/slides/resources/codingame.svg new file mode 100644 index 0000000..2ca1db1 --- /dev/null +++ b/2018/may/slides/resources/codingame.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/2018/may/slides/resources/edsac.jpg b/2018/may/slides/resources/edsac.jpg new file mode 100644 index 0000000..d98acde Binary files /dev/null and b/2018/may/slides/resources/edsac.jpg differ diff --git a/2018/may/slides/resources/firacode.woff b/2018/may/slides/resources/firacode.woff new file mode 100644 index 0000000..7233c3f Binary files /dev/null and b/2018/may/slides/resources/firacode.woff differ diff --git a/2018/may/slides/resources/firacode_bold.woff b/2018/may/slides/resources/firacode_bold.woff new file mode 100644 index 0000000..3e8a8e6 Binary files /dev/null and b/2018/may/slides/resources/firacode_bold.woff differ diff --git a/2018/may/slides/resources/hacker.jpg b/2018/may/slides/resources/hacker.jpg new file mode 100644 index 0000000..55dc398 Binary files /dev/null and b/2018/may/slides/resources/hacker.jpg differ diff --git a/2018/may/slides/resources/hacker.svg b/2018/may/slides/resources/hacker.svg new file mode 100644 index 0000000..bd61bad --- /dev/null +++ b/2018/may/slides/resources/hacker.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/2018/may/slides/resources/highlight.pack.js b/2018/may/slides/resources/highlight.pack.js new file mode 100644 index 0000000..c28d72b --- /dev/null +++ b/2018/may/slides/resources/highlight.pack.js @@ -0,0 +1 @@ +!function(e){"undefined"!=typeof exports?e(exports):(window.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return window.hljs}))}(function(e){function n(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}function a(e){var n=(e.className+" "+(e.parentNode?e.parentNode.className:"")).split(/\s+/);return n=n.map(function(e){return e.replace(/^lang(uage)?-/,"")}),n.filter(function(e){return N(e)||/no(-?)highlight/.test(e)})[0]}function o(e,n){var t={};for(var r in e)t[r]=e[r];if(n)for(var r in n)t[r]=n[r];return t}function i(e){var n=[];return function r(e,a){for(var o=e.firstChild;o;o=o.nextSibling)3==o.nodeType?a+=o.nodeValue.length:1==o.nodeType&&(n.push({event:"start",offset:a,node:o}),a=r(o,a),t(o).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:o}));return a}(e,0),n}function c(e,r,a){function o(){return e.length&&r.length?e[0].offset!=r[0].offset?e[0].offset"}function c(e){l+=""}function u(e){("start"==e.event?i:c)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=o();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g==e){f.reverse().forEach(c);do u(g.splice(0,1)[0]),g=o();while(g==e&&g.length&&g[0].offset==s);f.reverse().forEach(i)}else"start"==g[0].event?f.push(g[0].node):f.pop(),u(g.splice(0,1)[0])}return l+n(a.substr(s))}function u(e){function n(e){return e&&e.source||e}function t(t,r){return RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var c={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");c[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):Object.keys(a.k).forEach(function(e){u(e,a.k[e])}),a.k=c}a.lR=t(a.l||/\b[A-Za-z0-9_]+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"==e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function s(e,t,a,o){function i(e,n){for(var t=0;t";return o+=e+'">',o+n+i}function d(){if(!w.k)return n(y);var e="",t=0;w.lR.lastIndex=0;for(var r=w.lR.exec(y);r;){e+=n(y.substr(t,r.index-t));var a=g(w,r);a?(B+=a[1],e+=p(a[0],n(r[0]))):e+=n(r[0]),t=w.lR.lastIndex,r=w.lR.exec(y)}return e+n(y.substr(t))}function h(){if(w.sL&&!R[w.sL])return n(y);var e=w.sL?s(w.sL,y,!0,L[w.sL]):l(y);return w.r>0&&(B+=e.r),"continuous"==w.subLanguageMode&&(L[w.sL]=e.top),p(e.language,e.value,!1,!0)}function v(){return void 0!==w.sL?h():d()}function b(e,t){var r=e.cN?p(e.cN,"",!0):"";e.rB?(M+=r,y=""):e.eB?(M+=n(t)+r,y=""):(M+=r,y=t),w=Object.create(e,{parent:{value:w}})}function m(e,t){if(y+=e,void 0===t)return M+=v(),0;var r=i(t,w);if(r)return M+=v(),b(r,t),r.rB?0:t.length;var a=c(w,t);if(a){var o=w;o.rE||o.eE||(y+=t),M+=v();do w.cN&&(M+=""),B+=w.r,w=w.parent;while(w!=a.parent);return o.eE&&(M+=n(t)),y="",a.starts&&b(a.starts,""),o.rE?0:t.length}if(f(t,w))throw new Error('Illegal lexeme "'+t+'" for mode "'+(w.cN||"")+'"');return y+=t,t.length||1}var x=N(e);if(!x)throw new Error('Unknown language: "'+e+'"');u(x);for(var w=o||x,L={},M="",k=w;k!=x;k=k.parent)k.cN&&(M=p(k.cN,"",!0)+M);var y="",B=0;try{for(var C,j,I=0;;){if(w.t.lastIndex=I,C=w.t.exec(t),!C)break;j=m(t.substr(I,C.index-I),C[0]),I=C.index+j}m(t.substr(I));for(var k=w;k.parent;k=k.parent)k.cN&&(M+="");return{r:B,value:M,language:e,top:w}}catch(A){if(-1!=A.message.indexOf("Illegal"))return{r:0,value:n(t)};throw A}}function l(e,t){t=t||E.languages||Object.keys(R);var r={r:0,value:n(e)},a=r;return t.forEach(function(n){if(N(n)){var t=s(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}}),a.language&&(r.second_best=a),r}function f(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,n){return n.replace(/\t/g,E.tabReplace)})),E.useBR&&(e=e.replace(/\n/g,"
")),e}function g(e,n,t){var r=n?x[n]:t,a=[e.trim()];return e.match(/(\s|^)hljs(\s|$)/)||a.push("hljs"),r&&a.push(r),a.join(" ").trim()}function p(e){var n=a(e);if(!/no(-?)highlight/.test(n)){var t;E.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var r=t.textContent,o=n?s(n,r,!0):l(r),u=i(t);if(u.length){var p=document.createElementNS("http://www.w3.org/1999/xhtml","div");p.innerHTML=o.value,o.value=c(u,i(p),r)}o.value=f(o.value),e.innerHTML=o.value,e.className=g(e.className,n,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function d(e){E=o(E,e)}function h(){if(!h.called){h.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",h,!1),addEventListener("load",h,!1)}function b(n,t){var r=R[n]=t(e);r.aliases&&r.aliases.forEach(function(e){x[e]=n})}function m(){return Object.keys(R)}function N(e){return R[e]||R[x[e]]}var E={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},R={},x={};return e.highlight=s,e.highlightAuto=l,e.fixMarkup=f,e.highlightBlock=p,e.configure=d,e.initHighlighting=h,e.initHighlightingOnLoad=v,e.registerLanguage=b,e.listLanguages=m,e.getLanguage=N,e.inherit=o,e.IR="[a-zA-Z][a-zA-Z0-9_]*",e.UIR="[a-zA-Z_][a-zA-Z0-9_]*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/},e.CLCM={cN:"comment",b:"//",e:"$",c:[e.PWM]},e.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[e.PWM]},e.HCM={cN:"comment",b:"#",e:"$",c:[e.PWM]},e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e});hljs.registerLanguage("python",function(e){var r={cN:"prompt",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},l={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},c={cN:"params",b:/\(/,e:/\)/,c:["self",r,l,b]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda",built_in:"set dict list tuple map filter int float str input len any all sum min max abs round range enumerate sorted format",literal:"None True False"},i:/(<\/|->|\?)/,c:[r,l,b,e.HCM,{v:[{cN:"function",bK:"def",r:10},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n]/,c:[e.UTM,c]},{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}}); diff --git a/2018/may/slides/resources/monokai_sublime.css b/2018/may/slides/resources/monokai_sublime.css new file mode 100644 index 0000000..cbdbb7e --- /dev/null +++ b/2018/may/slides/resources/monokai_sublime.css @@ -0,0 +1,154 @@ +/* + +Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #23241f; + -webkit-text-size-adjust: none; +} + +.hljs, +.hljs-tag, +.css .hljs-rules, +.css .hljs-value, +.aspectj .hljs-function, +.css .hljs-function +.hljs-preprocessor, +.hljs-pragma { + color: #f8f8f2; +} + +.hljs-strongemphasis, +.hljs-strong, +.hljs-emphasis { + color: #a8a8a2; +} + +.hljs-bullet, +.hljs-blockquote, +.hljs-horizontal_rule, +.hljs-number, +.hljs-regexp, +.alias .hljs-keyword, +.hljs-literal, +.hljs-hexcolor { + color: #ae81ff; +} + +.hljs-tag .hljs-value, +.hljs-code, +.hljs-title, +.css .hljs-class, +.hljs-class .hljs-title:last-child { + color: #a6e22e; +} + +.hljs-link_url { + font-size: 80%; +} + +.hljs-strong, +.hljs-strongemphasis { + font-weight: bold; +} + +.hljs-emphasis, +.hljs-strongemphasis, +.hljs-class .hljs-title:last-child, +.hljs-typename { + font-style: italic; +} + +.hljs-keyword, +.ruby .hljs-class .hljs-keyword:first-child, +.ruby .hljs-function .hljs-keyword, +.hljs-function, +.hljs-change, +.hljs-winutils, +.hljs-flow, +.nginx .hljs-title, +.tex .hljs-special, +.hljs-header, +.hljs-attribute, +.hljs-symbol, +.hljs-symbol .hljs-string, +.hljs-tag .hljs-title, +.hljs-value, +.alias .hljs-keyword:first-child, +.css .hljs-tag, +.css .unit, +.css .hljs-important { + color: #f92672; +} + +.hljs-function .hljs-keyword, +.hljs-class .hljs-keyword:first-child, +.hljs-aspect .hljs-keyword:first-child, +.hljs-constant, +.hljs-typename, +.hljs-built_in, +.css .hljs-attribute { + color: #66d9ef; +} + +.hljs-variable, +.hljs-params, +.hljs-class .hljs-title, +.hljs-aspect .hljs-title { + color: #fd971f; +} + +.hljs-string, +.css .hljs-id, +.hljs-subst, +.hljs-type, +.ruby .hljs-class .hljs-parent, +.django .hljs-template_tag, +.django .hljs-variable, +.smalltalk .hljs-class, +.django .hljs-filter .hljs-argument, +.smalltalk .hljs-localvars, +.smalltalk .hljs-array, +.hljs-attr_selector, +.hljs-pseudo, +.hljs-addition, +.hljs-stream, +.hljs-envvar, +.apache .hljs-tag, +.apache .hljs-cbracket, +.tex .hljs-command, +.hljs-prompt, +.hljs-link_label, +.hljs-link_url { + color: #e6db74; +} + +.hljs-comment, +.hljs-javadoc, +.hljs-annotation, +.hljs-decorator, +.hljs-pi, +.hljs-doctype, +.hljs-deletion, +.hljs-shebang, +.apache .hljs-sqbracket, +.tex .hljs-formula { + color: #75715e; +} + +.coffeescript .javascript, +.javascript .xml, +.tex .hljs-formula, +.xml .javascript, +.xml .vbscript, +.xml .css, +.xml .hljs-cdata, +.xml .php, +.php .xml { + opacity: 0.5; +} diff --git a/2018/may/slides/resources/oswald.woff b/2018/may/slides/resources/oswald.woff new file mode 100644 index 0000000..196fbcf Binary files /dev/null and b/2018/may/slides/resources/oswald.woff differ diff --git a/2018/may/slides/resources/oxo.webm b/2018/may/slides/resources/oxo.webm new file mode 100644 index 0000000..1fef057 Binary files /dev/null and b/2018/may/slides/resources/oxo.webm differ diff --git a/2018/may/slides/slides.md b/2018/may/slides/slides.md new file mode 100644 index 0000000..dd819d4 --- /dev/null +++ b/2018/may/slides/slides.md @@ -0,0 +1,472 @@ +% Artificial Intelligence in gamesThe Python Tutorial +% Paweł Widera +% 2018-05-09 + +## What is this about? + +- writing **AI** in Python +- learning about **adversarial search** +-
+- making an **AlphaGO**-like player +-
+- friendly **competition** + +
+ +
+ + +## Tutorial organisation + +**Copy** fragments of **code** from slides to your editor
+or **download** complete _code_ using the link at a **slide corner**. + +

+ +

+ +
+Feel **free** to ask **questions** at any time! + +**Experiment** with the code, make it yours. + +## Setup + +- **Python 3** (for our AI bots) +- **Java (JRE) 1.8** (for the game environment) +-

[runner.jar][runner] (9.9MB)*

+ +
+ +
+ +*game engine adapted from the code published by [CodinGame][cg] + +. . . + +~~~{.shell} +>$ java -jar runner.jar + +Usage: java -jar runner.jar [-r] +~~~ + +. . . + +~~~ +for Firefox in about:config +"dom.moduleScripts.enabled" == True +~~~ + + +# The Game + +## Hints + +- played in **Egypt** in year **8700** HE
+ (see [The Human Era][he] Kurzgesagt video) +-
+- run on the **EDSAC** computer
+ in Cambridge in year **11952** HE + +
+ +CC-BY Computer Laboratory, University of Cambridge +
+ + +## OXO + + + + +## Tic-tac-toe + +
+
+**m** x **n** board with **k** in a row to win +
+ + +## Ultimate Tic-tac-toe + +
+
+Response time: **100ms** except 1st turn (**1s**) +
+ +
+ + +## Input / Output + +- **opponents** move +- number of **valid moves** +- **n** lines with moves +-
+- ~~~{.python} +-1 -1 0 2 +81 79 +0 0 0 0 +0 1 0 1 +0 2 0 3 +⋮ ⋮ +8 7 8 7 +8 8 8 8 +~~~ +-
+- **our** move + + +
+ +
+` ` +
+ +# Simple AI + +## Let's make some moves + + + +~~~{.python} +while True: + board, square = [int(x) for x in input().split()] + + count = int(input()) + + moves = [] + for i in range(count): + board, square = [int(x) for x in input().split()] + moves.append((board, square)) + + move = moves[0] + print(move[0], move[1]) +~~~ + +. . . + +
+AI system is an **intelligent agent** that can decide
+**what** actions to take and **when** to take them + +. . . + +~~~{.python} + # debugging + import sys + print(x, file=sys.sdterr) +~~~ + + +## Move lottery + + + +~~~{.python} +Move = namedtuple("Move", "board square") + + +def read_move(): + return Move(*map(int, input().split())) + + +def write_move(move): + print(move.board, move.square) +~~~ + +. . . + +~~~{.python} +while True: + enemy_move = read_move() + + count = int(input()) + moves = [read_move() for i in range(count)] + + write_move(random.choice(moves)) +~~~ + + +## Move evaluation + + + +~~~{.python} +CORNERS = {0, 2, 6, 8} +CENTRE = 4 + + +def evaluate(move): + if move.square in CORNERS: + return 2 + if move.square == CENTRE: + return 1 + return 0 +~~~ + +. . . + +~~~{.python} +while True: + + ... + + move = max(moves, key=evaluate) + write_move(move) +~~~ + + +## State evaluation + + + +~~~{.python} +boards = tuple([0 for i in range(9)] for i in range(9)) + +while True: + # place opponents move + enemy_move = read_move() + if enemy_move.board >= 0: + boards[enemy_move.board][enemy_move.square] = -1 + + ... + + # place my move + boards[move.board][move.square] = 1 +~~~ + +. . . + +~~~{.python} +DIAGONALS = ((0, 4, 8), (2, 4, 6)) +COLUMNS = ((0, 3, 6), (1, 4, 7), (2, 5, 8)) +ROWS = ((0, 1, 2), (3, 4, 5), (6, 7, 8)) +LINES = ROWS + COLUMNS + DIAGONALS + + +def evaluate(move): + current = boards[move.board].copy() + current[move.square] = 1 + + if any(sum(current[x] for x in line) == 3 for line in LINES): + return 10 + + ... +~~~ + + +## Game ending + + + +~~~{.python} +State = namedtuple("State", "meta boards") + +... + +meta = [0 for i in range(9)] +boards = tuple([0 for i in range(9)] for i in range(9)) +state = State(meta, boards) +~~~ + +. . . + +~~~{.python} +def has_line(board, player=1): + return any(sum(board[x] for x in line) == player * 3 for line in LINES) + +def update(state, move, player): + current = state.boards[move.board] + current[move.square] = player + if has_line(current, player): + state.meta[move.board] = player + return current +~~~ + +. . . + +~~~{.python} +def evaluate(move): + new_state = deepcopy(state) + new_board = update(new_state, move, 1) + + if has_line(new_state.meta): + return 100 + if has_line(new_board): + return 10 + return 0 +~~~ + + +## Not helping the opponent... + + + +~~~{.python} +def under_threat(board): + return any(sum(board[x] for x in line) == -2 for line in LINES) + + +def evaluate(move): + + ... + + next_board = new_state.boards[move.square] + if under_threat(next_board): + return -10 + + ... +~~~ + +. . . + +~~~{.python} + +def evaluate(move): + + ... + + new_state.meta[move.square] = -1 + if has_line(new_state.meta, -1) and under_threat(next_board): + return -100 + + ... +~~~ + +# Forward thinking + +## Minimax algorithm + +~~~{.python} +def best_enemy_play(move): + new_state = deepcopy(state) + update(new_state, move) + + best = -1000 + for enemy_move in valid_moves(new_state, move.square): + best = max(best, evaluate(new_state, enemy_move, -1)) + return best + +... + + # choose best move + move = min(moves, key=best_enemy_play) +~~~ + +. . . + +~~~{.python} +def game_over(meta): + return any(abs(sum(meta[x] for x in line)) == 3 for line in LINES) + +def valid_moves(state, next_board): + if game_over(state.meta): + return [] + + # next board is playable (not captured and not full) + if (state.meta[next_board] == 0 and not all(state.boards[next_board])): + return (Move(next_board, x) for x in range(9) if state.boards[next_board][x] == 0) + + # cannot play on the next board, list valid moves on all other boards + boards = (x for x in range(9) if state.meta[x] == 0) + return (Move(b, x) for b in boards for x in range(9) if state.boards[b][x] == 0) +~~~ + +## Minimax evaluation + + + +~~~{.python} +def evaluate(state, move, player): + new_state = deepcopy(state) + new_board = update(new_state, move, player) + next_board = new_state.boards[move.square] + + if has_line(new_state.meta, player): + return 100 + + new_state.meta[move.square] = -player + if has_line(new_state.meta, -player) and under_threat(next_board, -player): + return -100 + + if has_line(new_board, player): + return 10 + + if under_threat(next_board, -player): + return -10 + + if under_threat(new_state.meta, player): + return 5 + + # free grid choice for the opponent + if new_state.meta[move.square] != 0: + return -5 + + return 0 +~~~ + +# Guessing the future + +## Monte Carlo tree search + + + +~~~{.python} +def best_play(moves, start_time): + scores = [0] * len(moves) + for i, move in itertools.cycle(enumerate(moves)): + # stop when running out of time + if (time.perf_counter() - start_time) > 0.097: + break + # best score at the end of a random play is the move score + scores[i] = max(scores[i], random_play(move)) + + best_index = max(range(len(moves)), key=lambda x: scores[x]) + return moves[best_index] +~~~ + +. . . + +~~~{.python} +def random_play(move): + new_state = deepcopy(state) + player = 1 + + for i in range(0, DEPTH): + update(new_state, move, player) + if game_over(new_state.meta): + return player * 100 + + next_moves = list(valid_moves(new_state, move.square)) + move = random.choice(next_moves) + player = -1 * player + + return evaluate(new_state, move, 1) +~~~ + + +# Tournament + +## Rules + +- **just play** the game, **no hacks** please +- **your code** will be visible to **other teams** + +
+ +
+ +
+ +

+ +

+ +

+Other options: **USB stick** or + + +[runner]: ../runner.jar +[cg]: https://github.com/CodinGame/game-tictactoe +[he]: https://www.youtube.com/watch?v=czgOWmtGVGs diff --git a/2018/may/slides/templates/default.dzslides b/2018/may/slides/templates/default.dzslides new file mode 100644 index 0000000..7df8a97 --- /dev/null +++ b/2018/may/slides/templates/default.dzslides @@ -0,0 +1,738 @@ + + + +$for(author-meta)$ + +$endfor$ +$if(date-meta)$ + +$endif$ + $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ + + + + +$if(math)$ + $math$ +$endif$ +$for(header-includes)$ + $header-includes$ +$endfor$ + + + + + + +$if(title)$ +
+

$title$

+$for(author)$ +

$author$

+$endfor$ +
+
$date$
+ + Creative Commons Attribution-ShareAlike Licence +
+
+$endif$ +$for(include-before)$ +$include-before$ +$endfor$ +$body$ +$for(include-after)$ +$include-after$ +$endfor$ + + + +
+ + + + + + + + diff --git a/README.md b/README.md index 538ba07..2fe4037 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ This repo contains the following slides from our meetups: +### 2018 + +* Paweł Widera - [tutorial][ai] on AI in games (May) + ### 2016 * Paweł Widera - [talk][microbit] on BBC micro:bit (August) @@ -39,3 +43,4 @@ This repo contains the following slides from our meetups: [evoalg]: http://homepages.cs.ncl.ac.uk/pawel.widera/pyne-evolution/slides/ [datahack]: http://homepages.cs.ncl.ac.uk/pawel.widera/pyne-data-hacking/slides/ [microbit]: http://homepages.cs.ncl.ac.uk/pawel.widera/pyne-micro:bit/slides/ +[ai]: http://homepages.cs.ncl.ac.uk/pawel.widera/pyne-ai-games/slides/