Skip to content

Commit

Permalink
feat: Add Open AI GPT model to autoplay move suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielrbarbosa committed Apr 22, 2023
1 parent 0b45566 commit 01a4a49
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 96 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ build/
*.egg-info

# Keys
lichs/key
lichs/key.txt
lichs/openai.key
lichs/token.key

# Others
Expand Down
34 changes: 10 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,20 @@
<h1 align="center">
<img height="200" src="docs/images/logo.png">
<br>
Lichs (Lichess in the Terminal)
Lichess GPT (Lichess in the Terminal)
</h1>

<p align="center">
<a href="https://github.com/Cqsi/lichs">
<img src="https://img.shields.io/badge/project-semi--active-orange" alt="project-active" />
</a>
<a href="https://github.com/Cqsi/lichs">
<img src="https://img.shields.io/badge/Contributions-welcome-brightgreen" alt="contributions-welcome" />
</a>
<a href="https://github.com/Cqsi/lichs/blob/master/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="license-mit" />
</a>
<a href="https://pypi.org/project/lichs/">
<img src="https://img.shields.io/pypi/v/lichs" alt="pypi-version" />
</a>
</p>

## NOTE!
This project has not been updated in over a year. Feel free to check out the code, however there could be multiple errors upon running it.
## Example
This experiment aims to demonstrate current state of GPT models playing chess.
In this first release it's suggesting 90% of invalid moves and the other 10% are blunders.
<iframe src="https://lichess.org/embed/game/I2YUGRgY?theme=auto&bg=auto#9"
width=600 height=400 frameborder=0></iframe>

## Info and requirements
* Uses [Lichess](https://lichess.org/), which means that you need to have a Lichess account
* [Lichess](https://lichess.org/), which means that you need to have a Lichess account
* Only Classical and Rapid games because the Lichess API doesn't allow anything else
* This program uses [SAN](https://en.wikipedia.org/wiki/Algebraic_notation_(chess))-notation, see the [Important](#Important)-section.

Lichs uses the Lichess API (more exactly [berserk](https://github.com/rhgrant10/berserk)) to make it possible for you to play against other real players directly in the terminal on Lichess servers. If you like this project, be sure to also check out [Nick Zuber's Chs-project](https://github.com/nickzuber/chs), since it was his project that inspired me to do this in the first place.


## Installation

This package is available on [PyPi](https://pypi.org/project/lichs/), therefore just run:
Expand All @@ -46,7 +31,8 @@ and the program will be installed. The next step is to generate a personal API-k
1. [Create a Lichess API token](https://lichess.org/account/oauth/token/create?scopes[]=board:play&description=Lichs+cli+play), log into Lichess if necessary
2. Click the button `Submit` in the lower right corner
3. Copy the token shown in the brown box
4. Jump into your terminal and write `lichs <api_token>` (put your API token instead of `<api_token>`) and run the command. To get this clear, an example would have been `lichs lzRceo5XOUND74Lm`. You should then see a message to confirm that the API token has been saved.
4. Jump into your terminal and write `lichs <api_token>` (put your API token instead of `<api_token>`) and run the command. To get this clear, an example would have been `lichs lzRceo5XOUND74Lm`. You should then see a message to confirm that the API token has been saved.
5. Set up your OpenAI [API Keys](https://platform.openai.com/account/api-keys), create a file in lichs directory named "openai.key" and save it. Quite expensive to run, so set and monitor your API key limits with OpenAI!


## Usage
Expand All @@ -55,7 +41,7 @@ and the program will be installed. The next step is to generate a personal API-k
<img src="docs/images/carbon.png">
</h1>

You start playing by typing the command `lichs` into your terminal:
You start playing by typing the command into your terminal:

```
$ lichs
Expand Down
110 changes: 57 additions & 53 deletions lichs/Game.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import threading
import datetime
import time
import chess

import openai
import re
import os
from pathlib import Path

chess_board = chess.Board()
runOnce = True
Expand All @@ -19,42 +22,46 @@ def __init__(self, board, game_id, player_id, isWhite, color, time, **kwargs):
self.color = color
self.clock = {'white': datetime.datetime(1970, 1, 1, 0, time, 0), 'black': datetime.datetime(1970, 1, 1, 0, time, 0)}
self.first_move = 2 # returns false after 2 moves have been made
self.canMove = True

openai_file = Path(__file__).parent.absolute() / "openai.key"
openai.api_key = openai_file.read_text()
if self.isWhite:
self.white_first_move()


def run(self):
for event in self.stream:
if event['type'] == "gameFull":
self.handle_game_full(event)
elif event['type'] == 'gameState':
if event['type'] == 'gameState':
self.handle_state_change(event)
elif event['type'] == 'chatLine':
self.handle_chat_line(event)

def handle_state_change(self, game_state):

global chess_board

self.canMove = True

if game_state.get(self.color[0].lower() + "draw") is True:
self.handle_draw_state(game_state)
print('DRAW!')
self.canMove = False
os._exit(0)
elif game_state["status"] == "resign":
print("The opponent resigned. Congrats!")
self.canMove = False
os._exit(0)

else:
# update time
self.clock['white'] = game_state['wtime']
self.clock['black'] = game_state['btime']

# there's no "amount of turns" variable in the JSON, so we have to construct one manually
turn = len(game_state["moves"].split())-1
if turn%2 == self.isWhite:
if turn % 2 == self.isWhite:

print(self.color + " moved.")
last_move = game_state["moves"].split()[-1]
print(self.color + " moved: " + last_move)
print()

chess_board.push_uci(game_state["moves"].split()[-1])
chess_board.push_uci(last_move)
self.display_board()
print()

Expand All @@ -66,55 +73,45 @@ def handle_state_change(self, game_state):

# user move start time
move_start = datetime.datetime.now()

while(True):
while(self.canMove):
try:
move = input("Make your move: ")
if move.lower() == "resign":
self.board.resign_game(self.game_id)
print("You resigned the game!")
print("Thanks for playing!")
os._exit(0)
#move = input("Make your move: ")
playerColor = 'white' if self.isWhite else 'black'
prompt = 'Given the current chess game: ' + game_state["moves"] + \
' output in one word the best next move for ' + playerColor + ' in SAN notation'

completion = openai.Completion.create(engine='text-davinci-002', prompt=prompt, temperature=0.5)
move = re.sub(r'\W+', '', completion.choices[0].text.strip())
print('ChatGPT move: %s' % move)
time.sleep(3)

self.board.make_move(self.game_id, chess_board.parse_san(move))
chess_board.push_san(move)
if self.first_move:
self.first_move -= 1
elif self.color[0] == 'b':
self.clock['white'] -= datetime.datetime.now() - move_start
else:
self.board.make_move(self.game_id, chess_board.parse_san(move))
chess_board.push_san(move)
if self.first_move:
self.first_move -= 1
elif self.color[0] == 'b':
self.clock['white'] -= datetime.datetime.now() - move_start
else:
self.clock['black'] -= datetime.datetime.now() - move_start
self.clock['black'] -= datetime.datetime.now() - move_start
break
except Exception as e:
print("You can't make that move. Try again!")
print(f"Reason: {e}")
print(f"Error: {e}")
continue
break

self.display_board()
self.check_mate(chess_board)
print()
print(self.color + "'s turn...")

def handle_game_full(self, gamefull):
# TODO Write this method
pass

def handle_draw_state(self, game_state):
# TODO Write this method
pass

def handle_chat_line(self, event):
# TODO Write this method
pass

def white_first_move(self):

global chess_board

self.display_board()
while(True):
try:
move = input("Make your move: ")
#move = input("Make your move: ")
move = "d4"
if move.lower() == "resign":
self.board.resign_game(self.game_id)
os._exit(0)
Expand Down Expand Up @@ -152,14 +149,21 @@ def check_mate(self, chess_board):
def display_board(self):
global chess_board

# display the chess board, if the the player's color is black then flip the board
if self.isWhite:
print(chess_board)
else:
print(chess_board.transform(chess.flip_vertical).transform(chess.flip_horizontal))
m = {'P': '♙', 'N': '♘', 'B': '♗', 'R': '♖', 'Q': '♕', 'K': '♔',
'p': '♟', 'n': '♞', 'b': '♝', 'r': '♜', 'q': '♛', 'k': '♚',
'.': '·', ' ': ' '}

# print clock
# Replace each piece in the board representation with its Unicode equivalent

# Display the chess board, if the the player's color is black then flip the board
if not self.isWhite:
chess_board = chess_board.transform(chess.flip_vertical).transform(chess.flip_horizontal)

board_str = str(chess_board)
for piece, char in m.items():
board_str = board_str.replace(piece, char)

print(board_str)
print("[%02d:%02d : %02d:%02d]" % (self.clock['white'].minute, self.clock['white'].second,
self.clock['black'].minute, self.clock['black'].second))
print()

print()
26 changes: 9 additions & 17 deletions lichs/__main__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import sys
import os
import berserk
import chess
from typing import Tuple
from pathlib import Path
from getpass import getpass

from Game import Game
from api_key import set_api

token_file = Path(__file__).parent.absolute() / "token.key"

Expand All @@ -17,9 +14,6 @@ def set_token(key):

def get_token():
return getpass("Please enter your token: ")
# def get_opt(opt):
# TODO Get option function


def get_game_type_input() -> Tuple[int, int]:
"""Gets the game time and give back time from the user."""
Expand All @@ -39,19 +33,16 @@ def get_game_type_input() -> Tuple[int, int]:
except:
print("Invalid input.")


def main():

if len(sys.argv) == 2:
set_token(sys.argv[1])
set_token(sys.argv[1])

if not token_file.exists():
print("Please provide a token key")
print("See the instructions in the Github README:")
print("https://github.com/Cqsi/lichs#how-to-generate-a-personal-api-token")
set_token(get_token())


token = token_file.read_text()
session = berserk.TokenSession(token)
client = berserk.clients.Client(session)
Expand All @@ -72,9 +63,10 @@ def main():
set_token(get_token())

# Welcome text
print("Welcome to Lichess in the Terminal (lichs)\n")
print("Welcome to LichessGPT\n")
print("Type either\nP to play\nH for help\nQ to quit ")
optFlag = True # Flag for options menu
optFlag = True # Flag for options menu

while optFlag == True:
choice = input("Choose your option: ")
if choice.lower() == "h":
Expand All @@ -83,12 +75,12 @@ def main():
elif choice.lower() == "q":
print("Quitting...")
sys.exit(0)
elif choice.lower() == "p":
elif choice.lower() == "p":
optFlag = False
else: print("Please choose from either P to play, H for help, or Q to quit")
else:
print("Please choose from either P to play, H for help, or Q to quit")

time, increment = get_game_type_input()

print("Searching after opponent...")
board.seek(time, increment)

Expand All @@ -97,7 +89,7 @@ def main():
print("An opponent was found!")

isWhite = True
color = "Black" # We set the color to the opposite color of the player
color = "Black" # We set the color to the opposite color of the player

if player_id != client.games.export(event['game']['id'])['players']['white']['user']['id']:
isWhite = False
Expand All @@ -110,4 +102,4 @@ def main():
game.start()

if __name__ == "__main__":
main()
main()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ requests==2.26.0
urllib3==1.26.5
wrapt==1.13.3
pytest==7.2.*
openai=0.27.2

0 comments on commit 01a4a49

Please sign in to comment.