g | x | w | all
Bytes Lang Time Link
431C250305T043342Zgsitcia
nan250305T024314Zhuanglx
692R250226T004221Zuser1502
115P250225T001204Zhuanglx
nan250212T134952Zuser1502

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])

Attempt this online!

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)

Attempt this online!

Features

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)