Skip to content

Instantly share code, notes, and snippets.

@p4bl0-
Last active November 15, 2023 15:31
Show Gist options
  • Save p4bl0-/abf4960f07045d7a443230a46e73b849 to your computer and use it in GitHub Desktop.
Save p4bl0-/abf4960f07045d7a443230a46e73b849 to your computer and use it in GitHub Desktop.
Pure HTML playable TicTacToe game (no CSS nor JavaScript) in 367KB

This is an attempt to see how small a pure HTML playable TicTacToe game can be. The first script (ttt.py) generates a 560KB HTML file.

I made it in reaction to this Hacker News post: Implementing Tic Tac Toe with 170MB of HTML – No JavaScript or CSS, because my reaction to "170MB" was "wait, WAT?".

The second script (ttt_smaller.py) is an attempt to go further, by shortening IDs to maximum 2 chars rather than using 9 characters for full board descriptions as IDs. It does so by using a custom base74, because 74 + 74 × 74 = 5550 which is just above the 5478 game states that have to be represented.

This optimization allows to get down to 412KB, that is a saving of 148Ko, i.e., a 26% size-reduction!

UPDATE: The ttt_smallerer.py version of the script available below goes even further and generates a 367KB HTML file, these 45Ko are saved essentially by removing quotes around HTML attributes…


I also made "pretty" versions that use a minimal amount of CSS (54 bytes!) and a bit more HTML structure:

Added CSS: pre{font-size:20pt;display:none}:target{display:block}.

from copy import deepcopy
opponent = {'X':'O','O':'X'}
generated_boards = []
def board_id (board):
return ''.join([''.join(row) for row in board])
def winning (board, player):
return player == board[0][0] == board[0][1] == board[0][2] \
or player == board[1][0] == board[1][1] == board[1][2] \
or player == board[2][0] == board[2][1] == board[2][2] \
or player == board[0][0] == board[1][0] == board[2][0] \
or player == board[0][1] == board[1][1] == board[2][1] \
or player == board[0][2] == board[1][2] == board[2][2] \
or player == board[0][0] == board[1][1] == board[2][2] \
or player == board[0][2] == board[1][1] == board[2][0]
def full (board):
return '_' not in [cell for row in board for cell in row]
def print_board (board, player):
print(f'<hr id="{board_id(board)}">', end="")
links = True
if winning(board, opponent[player]):
links = False
print('WIN:')
elif full(board):
links = False
print('DRAW:')
for row in range(0, 3):
for col in range(0, 3):
if links and board[row][col] == '_':
board[row][col] = player
print(f'<a href="#{board_id(board)}">_</a>', end="")
board[row][col] = '_'
else:
print(f'{board[row][col]}', end="")
if row != 2: print('')
return links
def next_boards (board, player):
boards = []
for row in range(0, 3):
for col in range(0, 3):
if board[row][col] == '_':
board[row][col] = player
bid = board_id(board)
if bid not in generated_boards:
boards.append(deepcopy(board))
generated_boards.append(bid)
board[row][col] = '_'
return boards
def print_boards (board, player):
if not print_board(board, player): return
for board in next_boards(board, player):
print_boards(board, opponent[player])
print('<pre>')
print_boards([['_' for _ in range(0, 3)] for _ in range(0, 3)], 'X')
from copy import deepcopy
from itertools import product
opponent = {'X':'O','O':'X'}
generated_boards = []
counter = 0
digits = [d for s in
[ [str(i) for i in range(0,10)]
, [chr(c) for c in range(ord('a'), ord('z') + 1)]
, [chr(c) for c in range(ord('A'), ord('Z') + 1)]
, ['!', '~', '|', '-', '_', ':', '@', '=', '*', '$', '%', '?']
]
for d in s]
ids = digits + [x[0]+x[1] for x in product(digits, digits)]
board_ids = {}
def board_id (board):
global counter
bid = ''.join([''.join(row) for row in board])
if bid not in board_ids:
board_ids[bid] = ids[counter]
counter += 1
return board_ids[bid]
def winning (board, player):
return player == board[0][0] == board[0][1] == board[0][2] \
or player == board[1][0] == board[1][1] == board[1][2] \
or player == board[2][0] == board[2][1] == board[2][2] \
or player == board[0][0] == board[1][0] == board[2][0] \
or player == board[0][1] == board[1][1] == board[2][1] \
or player == board[0][2] == board[1][2] == board[2][2] \
or player == board[0][0] == board[1][1] == board[2][2] \
or player == board[0][2] == board[1][1] == board[2][0]
def full (board):
return '_' not in [cell for row in board for cell in row]
def print_board (board, player):
print(f'<hr id="{board_id(board)}">', end="")
links = True
if winning(board, opponent[player]):
links = False
print('WIN:')
elif full(board):
links = False
print('DRAW:')
for row in range(0, 3):
for col in range(0, 3):
if links and board[row][col] == '_':
board[row][col] = player
print(f'<a href="#{board_id(board)}">_</a>', end="")
board[row][col] = '_'
else:
print(f'{board[row][col]}', end="")
if row != 2: print('')
return links
def next_boards (board, player):
boards = []
for row in range(0, 3):
for col in range(0, 3):
if board[row][col] == '_':
board[row][col] = player
bid = board_id(board)
if bid not in generated_boards:
boards.append(deepcopy(board))
generated_boards.append(bid)
board[row][col] = '_'
return boards
def print_boards (board, player):
if not print_board(board, player): return
for board in next_boards(board, player):
print_boards(board, opponent[player])
print('<pre>')
print_boards([['_' for _ in range(0, 3)] for _ in range(0, 3)], 'X')
from copy import deepcopy
from itertools import product
opponent = {'X':'O','O':'X'}
generated_boards = []
counter = 0
digits = [d for s in
[ [str(i) for i in range(0,10)]
, [chr(c) for c in range(ord('a'), ord('z') + 1)]
, [chr(c) for c in range(ord('A'), ord('Z') + 1)]
, ['!', '~', '|', '-', '_', ':', '@', '=', '*', '$', '%', '?']
]
for d in s]
ids = digits + [x[0]+x[1] for x in product(digits, digits)]
board_ids = {}
def board_id (board):
global counter
bid = ''.join([''.join(row) for row in board])
if bid not in board_ids:
board_ids[bid] = ids[counter]
counter += 1
return board_ids[bid]
def winning (board, player):
return player == board[0][0] == board[0][1] == board[0][2] \
or player == board[1][0] == board[1][1] == board[1][2] \
or player == board[2][0] == board[2][1] == board[2][2] \
or player == board[0][0] == board[1][0] == board[2][0] \
or player == board[0][1] == board[1][1] == board[2][1] \
or player == board[0][2] == board[1][2] == board[2][2] \
or player == board[0][0] == board[1][1] == board[2][2] \
or player == board[0][2] == board[1][1] == board[2][0]
def full (board):
return '_' not in [cell for row in board for cell in row]
def print_board (board, player):
print(f'<hr id={board_id(board)}>', end="")
links = True
if winning(board, opponent[player]):
links = False
print(opponent[player])
elif full(board):
links = False
print('=')
for row in range(0, 3):
for col in range(0, 3):
if links and board[row][col] == '_':
board[row][col] = player
print(f'<a href=#{board_id(board)}>_</a>', end="")
board[row][col] = '_'
else:
print(f'{board[row][col]}', end="")
if row != 2: print('')
return links
def next_boards (board, player):
boards = []
for row in range(0, 3):
for col in range(0, 3):
if board[row][col] == '_':
board[row][col] = player
bid = board_id(board)
if bid not in generated_boards:
boards.append(deepcopy(board))
generated_boards.append(bid)
board[row][col] = '_'
return boards
def print_boards (board, player):
if not print_board(board, player): return
for board in next_boards(board, player):
print_boards(board, opponent[player])
print('<pre>')
print_boards([['_' for _ in range(0, 3)] for _ in range(0, 3)], 'X')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment