| Bytes | Lang | Time | Link |
|---|---|---|---|
| 431 | C | 250305T043342Z | gsitcia |
| nan | 250305T024314Z | huanglx | |
| 692 | R | 250226T004221Z | user1502 |
| 115 | P | 250225T001204Z | huanglx |
| nan | 250212T134952Z | user1502 |
C, 431 bytes
from random import*
A=range
N=Color.NONE
def E(g,r,c):
q=[0]*9;s=1
for d in[0,1,1,-1]:
for o in A(5):
l=[g.board.get(r+i*s,c+i*d)for i in A(-o,5-o)]
if Color.INVALID not in l:x,y=map(l.count,[g.current_player,N]);q[x]+=x+y>4;q[y-5]+=x<1
s=d
return q[4],q[5],q[3]>1,q[6],q[2]>3,q[7],q[2],q[1],q[8],q[0]
class C:
def move(s,g):g.place(*max((E(g,r,c),random(),r,c)for r in A(15)for c in A(15)if g.board.get(r,c)==N)[2:])
Ungolfed:
import random
def evaluate(game, row, col):
# These count number of lines that are possible to complete based on how close to being complete they are
friendly = [0]*5
opponent = [0]*5
# counts each line of 5 that includes (row, col)
for dr, dc in [(1,0), (0,1), (1,1), (1,-1)]:
for offset in range(-5, 1):
l = [game.board.get(row + i*dr, col + i*dc) for i in range(offset, offset+5)]
if Color.INVALID in l: continue
num_empty = l.count(Color.NONE)
num_friendly = l.count(game.current_player)
num_opponent = 5 - num_empty - num_friendly
if num_friendly == 0:
opponent[num_opponent] += 1
if num_opponent == 0:
friendly[num_friendly] += 1
# priorities:
# winning not losing guarantee win ? ? ? ? ? ? avoid playing near the edge of the board
return friendly[4], opponent[4], friendly[3]>1, opponent[3], friendly[2]>3, opponent[2], friendly[2], friendly[1], opponent[1], friendly[0]
class C:
def move(self, game):
_, _, row, col = max(
(evaluate(game, row, col), random.random(), row, col)
for row in range(15)
for col in range(15)
if game.board.get(row, col) == Color.NONE
)
game.place(row, col)
The idea is to score moves based on how much they benefit us/hinder the opponent. That said, it's probably possible to tune the exact priorities better.
Chain Blocker
If I can't get 5 in a row, no one can!
Tries its hardest to stop the opponent from getting 5 in a row. The idea is to outlast the opponent by frustrating their efforts to win as much as possible.
Since there are not many submissions, length is not a concern for this bot.
import collections
import math
import random
class ChainBlocker:
def move(self, game):
Chain = collections.namedtuple("Chain", ("start", "end", "threat", "startavail", "endavail", "rowdir", "coldir"))
chains = {}
empty_squares = []
for scan_row in range(len(game.board.board)):
for scan_col in range(len(game.board.board[scan_row])):
if game.board.board[scan_row][scan_col] == Color.NONE:
empty_squares.append((scan_row, scan_col))
elif game.board.board[scan_row][scan_col] == game.current_player:
pass
else:
directions = ((0, 1), (1, 1), (1, 0), (1, -1))
for rowdir, coldir in directions:
chain_start = (scan_row, scan_col)
chain_end = (scan_row, scan_col)
chain_threat = 0
chain_startavail = False
chain_endavail = False
curr_row, curr_col = scan_row, scan_col
while True:
curr_row -= rowdir
curr_col -= coldir
if curr_row < 0 or curr_col < 0 or curr_col >= game.COLS or \
game.board.board[curr_row][curr_col] == game.current_player:
chain_start = (curr_row + rowdir, curr_col + coldir)
break
elif game.board.board[curr_row][curr_col] == Color.NONE:
chain_startavail = True
chain_threat += 1
chain_start = (curr_row + rowdir, curr_col + coldir)
break
while True:
curr_row += rowdir
curr_col += coldir
chain_threat += 1
if curr_row >= game.ROWS or curr_col >= game.COLS or curr_col < 0 \
or game.board.board[curr_row][curr_col] == game.current_player:
chain_end = (curr_row - rowdir, curr_col - coldir)
chain_threat -= 1
break
elif game.board.board[curr_row][curr_col] == Color.NONE:
chain_end = (curr_row - rowdir, curr_col - coldir)
chain_endavail = True
break
if not chain_startavail and not chain_endavail:
chain_threat = 0
if chain_threat > 0:
try:
chains[chain_threat].append(Chain(chain_start, chain_end, chain_threat,
chain_startavail, chain_endavail, rowdir, coldir))
except KeyError:
chains[chain_threat] = [Chain(chain_start, chain_end, chain_threat,
chain_startavail, chain_endavail, rowdir, coldir)]
move_candidates = []
if chains:
threatening_chains = chains[max(chains)]
for chain in threatening_chains:
if chain.startavail:
move_candidates.append((chain.start[0] - chain.rowdir, chain.start[1] - chain.coldir))
if chain.endavail:
move_candidates.append((chain.end[0] + chain.rowdir, chain.end[1] + chain.coldir))
else:
move_candidates = empty_squares
move_choice = random.choice(move_candidates)
game.place(move_choice[0], move_choice[1])
R, 692 bytes
A reasonably strong agent using simulation-based search.
from random import *
import copy
import time
class R:
def move(_,G):
r,d={},{(i, j):G.board.board[i][j]for i in range(15)for j in range(15)};f=[p for p,x in d.items()if x.is_player]or[(6,7)];c={p:[1,1]for p in d};t=time.time()
while time.time()-t<9:
M,l,s,g=[(0,0)],f[:],set(f),copy.deepcopy(G)
while not g.is_over:
m=r.get(M[-1],0)
while m not in d or m in s:
m=tuple(k+randint(-1, 1)for k in choice(l))
M+=[(m,M[-1][1]^1<<15*m[0]+m[1])];g.place(*m);l+=[m];s.add(m)
M,p=M[::-1],1
for(x,y)in zip(M,M[1:]):
u=r.get(y);r[y]=x if p else 0 if u==x else u;p^=1
c[M[-2][0]][p]+=1
G.place(*max((a/(a+b),m)for m,(a,b)in c.items()if a+b>2)[1])
P, 115 bytes
Since only the shortest 1/3 of submissions will be picked, let's make sure we make the cut! With only 115 bytes, surely no one can make a smaller program than this!
class P:
def move(s,g):
e=(0,0)
while g.board.get(*e).value!='.':e=(e[0]+(e[1]==14),(e[1]+1)%15)
g.place(*e)
Features
- Meets all requirements in the specification
- Won't make an illegal move unless the board is full
- Beats all of the other submissions (at the time of posting)
Why P?
The name P was chosen because it is short and Python, the language used, starts with the letter "P".
Explanation
class P: # Required code
def move(self, game): # Shortened to s, g in an attempt to get the fewest bytes possible.
e = (0,0) # Initialize the current move (e) to be the first square
# Continue looping until we get an empty square
while game.board.get(*e).value != '.':
# Increment the
e = (e[0] + (e[1] == 14), # e[1] == 14 is 1 if we are on the 14th square, 0 otherwise
(e[1] + 1) % 15) # We will continue incrementing by 1 until we get to the end, when we wrap around using the mod 15.
game.place(*e) # Run the place() function. The *e unpacks the two values of e into the two arguments of place(row, col)
from random import *
class Gamma:
def move(self, game):
if not game.move_counter:
return game.place(7, 7)
grid = {(i, j): x for i, row in enumerate(game.board.board) for j, x in enumerate(row)}
filled = [p for p, x in grid.items() if x.is_player]
t = 0
while True:
t += 1
i,j = p = choice(filled)
if t < 1000 and grid[p] != game.current_player:
continue
q = (i + randint(-1, 1), j + randint(-1, 1))
if q in grid and q not in filled:
return game.place(*q)