This commit is contained in:
2025-09-29 01:11:22 +02:00
parent 8d7bc7e096
commit 91ff9625ec
7 changed files with 135 additions and 11 deletions

7
rengobot/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
*.txt
*.sgf
*.png
*.out
*.sh
__pycache__/
rengobot_venv/

12
rengobot/README.md Normal file
View File

@@ -0,0 +1,12 @@
A fork of
https://github.com/ReneCareenium/rengobot
A discord bot for playing rengo games!
# Dependencies
- sgf-render
- python-discord
- python-sgfmill
Make sure to run the bot in an environment with read/write permissions

202
rengobot/rengobot.py Normal file
View File

@@ -0,0 +1,202 @@
import os
import ast
import time
from datetime import datetime, timedelta
import asyncio
import sgfengine
import discord
from discord.ext import commands
# We don't use fancy slash commands here. It seems there is this library for python but it looks a bit more involved.
# https://pypi.org/project/discord-py-slash-command/
intents = discord.Intents.default()
#intents.message_content = True
bot = commands.Bot(command_prefix='$', help_command=None, intents=intents)
# People who can start and resign games :O
admins=[]
awesome_server_id=
permitted_channel_id=
player_id= 0
start_time = 0
with open("token.txt") as f:
token = f.readlines()[0] # Get your own token and put it in token.txt
@bot.command()
async def help(ctx):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
await ctx.send(
'$help : shows this help\n\n'+
'$play <move>: play a move. For example, `$play Q16`. Passing is not implemented!\n'+
'$edit <move>: if you make a mistake in your move\n\n'+
'$sgf: get the sgf file of the game\n'+
'$board: shows the current board\n'+
'$newgame <handicap> <komi>: starts a game in this channel (admin only!)\n'+
'$resign <B/W>: <B/W> resigns the game in this channel. It returns its sgf file (admin only!)'
)
# ctx has guild, message, author, send, and channel (?)
@bot.command()
async def play(ctx, arg):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
channel_id= ctx.channel.id
user = ctx.author
guild= ctx.guild
global player_id
global start_time
if not os.path.exists(str(channel_id)+".sgf"):
await ctx.send("No active game in this channel!")
return
if (player_id == user.id):
await ctx.send("No two consecutive moves by the same player!")
return
elapsed_time = time.time() - start_time
if elapsed_time < 1:
await ctx.send("One second per move!")
return
legal_moves=[chr(col+ord('A')-1)+str(row) for col in range(1,21) if col!=9 for row in range(1,20)]
legal_moves+=[chr(col+ord('a')-1)+str(row) for col in range(1,21) if col!=9 for row in range(1,20)]
if arg not in legal_moves:
await ctx.send("I don't understand the move! Please input it in the format `$play Q16`")
return
try:
sgfengine.play_move(str(channel_id), arg, user.display_name)
except ValueError as e:
await ctx.send(str(e))
return
player_id=user.id
start_time = time.time()
file = discord.File(str(ctx.channel.id)+".png")
await ctx.send(file=file)
@bot.command()
async def edit(ctx, arg):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
channel_id= ctx.channel.id
user = ctx.author
guild= ctx.guild
global player_id
if (player_id == user.id):
await ctx.send("Not the player that made the mistake!")
return
if not os.path.exists(str(channel_id)+".sgf"):
await ctx.send("No active game in this channel!")
return
colour= sgfengine.next_colour(str(channel_id))
legal_moves=[chr(col+ord('A')-1)+str(row) for col in range(1,21) if col!=9 for row in range(1,20)]
legal_moves+=[chr(col+ord('a')-1)+str(row) for col in range(1,21) if col!=9 for row in range(1,20)]
if arg not in legal_moves:
await ctx.send("I don't understand the move! Please input it in the format `$play Q16`")
return
try:
sgfengine.play_move(str(channel_id), arg, user.display_name, True)
except ValueError as e:
await ctx.send(str(e))
return
file = discord.File(str(ctx.channel.id)+".png")
await ctx.send(file=file)
@bot.command()
async def board(ctx):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
channel_id= ctx.channel.id
user = ctx.author
guild= ctx.guild
if not os.path.exists(str(channel_id)+".sgf"):
await ctx.send("No active game in this channel!")
return
os.system("/home/nik/.cargo/bin/sgf-render -f png --style fancy --label-sides nesw -o "+str(channel_id)+".png -n last "+str(channel_id)+".sgf")
file = discord.File(str(ctx.channel.id)+".png")
await ctx.send(file=file)
@bot.command()
async def sgf(ctx):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
file = discord.File(str(ctx.channel.id)+".sgf")
await ctx.send(file=file)
@bot.command()
async def newgame(ctx, handicap=0, komi=6.5):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
channel_id= ctx.channel.id
user = ctx.author
if user.id not in admins:
await ctx.send("You don't have permissions for this!")
return
sgfengine.new_game(str(channel_id), handicap, komi)
os.system("/home/nik/.cargo/bin/sgf-render -f png --style fancy --label-sides nesw -o "+str(channel_id)+".png -n last "+str(channel_id)+".sgf")
file = discord.File(str(channel_id)+".png")
await ctx.send(file=file, content="A new game has started! Play with `$play <move>`")
@bot.command()
async def resign(ctx, arg):
if ctx.guild.id != awesome_server_id or ctx.channel.id != permitted_channel_id:
return
channel_id= ctx.channel.id
user = ctx.author
if user.id not in admins:
await ctx.send("You don't have permissions for this!")
return
if arg not in ["W","B"]:
await ctx.send("Unrecognized colour! Please try `$resign <B/W>` to resign as Black/White")
return
now=datetime.now()
file_name= "rengo_"+now.strftime("%Y_%m_%d_%H_%M_%S_")+ctx.channel.name+".sgf"
sgfengine.resign(str(channel_id), arg, file_name)
file = discord.File(file_name)
await ctx.send(file=file, content=("Black" if arg=="W" else "White")+" wins!")
print("Running")
bot.run(token)

105
rengobot/sgfengine.py Normal file
View File

@@ -0,0 +1,105 @@
# Needs sgf-render and sgfmill
# https://mjw.woodcraft.me.uk/sgfmill/doc/1.1.1/properties.html?highlight=list%20properties
import os
from sgfmill import sgf, boards, sgf_moves, ascii_boards
# This file only deals with the png and sgf side of things. To manage users etc go to the main file.
def new_game(channel_id, handicap=0, komi=6.5):
game= sgf.Sgf_game(19)
game.root.set("KM", komi)
if handicap>=2:
game.root.set("HA", handicap)
handicap_dict={
2: [(3,3), (15,15)],
3: [(3,3), (15,15), (15,3)],
4: [(3,3), (15,15), (15,3), (3,15)],
5: [(3,3), (15,15), (15,3), (3,15), (9,9)],
6: [(3,3), (15,15), (15,3), (3,15), (9,3), (9,15)],
7: [(3,3), (15,15), (15,3), (3,15), (9,3), (9,15), (9,9)],
8: [(3,3), (15,15), (15,3), (3,15), (9,3), (9,15), (3,9), (15,9)],
9: [(3,3), (15,15), (15,3), (3,15), (9,3), (9,15), (3,9), (15,9), (9,9)]}
game.root.set("AB",handicap_dict[handicap])
with open (channel_id+".sgf", "wb") as f:
f.write(game.serialise())
f.close()
os.system("/home/nik/.cargo/bin/sgf-render -f png --style fancy --label-sides nesw -o "+str(channel_id)+".png -n last "+str(channel_id)+".sgf")
#0 if black to play, 1 if white to play
def next_colour(channel_id):
with open(channel_id+".sgf","rb") as f:
game = sgf.Sgf_game.from_bytes(f.read())
f.close()
node= game.get_last_node()
return 1 if ("B" in node.properties() or "AB" in node.properties()) else 0
# Could be an illegal move, or maybe I don't understand the message
# outputs to <channel_id>.png
def play_move(channel_id, messagestr, player, overwrite=False):
thecol= ord(messagestr[0].lower()) - ord('a')
if thecol>8: thecol-=1 # Go boards don't have an I column!!
therow= int(messagestr[1:]) - 1
with open(channel_id+".sgf","rb") as f:
game = sgf.Sgf_game.from_bytes(f.read())
f.close()
koban=None
node= game.get_last_node()
board, moves= sgf_moves.get_setup_and_moves(game)
if overwrite:
node2= node.parent
node.delete()
node= node2
moves= moves[:-1]
for (colour, (row, col)) in moves:
koban=board.play(row,col,colour)
if (therow, thecol)==koban:
raise ValueError("Ko banned move!")
colour = "w" if ("B" in node.properties() or "AB" in node.properties()) else "b"
board2= board.copy()
try:
koban2=board2.play(therow, thecol, colour)
except ValueError as e:
raise ValueError("Illegal move! There is a stone there.")
if board2.get(therow,thecol) == None:
raise ValueError("Illegal move! No self-captures allowed.")
node2= node.new_child()
node2.set(("B" if colour =='b' else "W"), (therow,thecol))
if koban2 is not None: node2.set("SQ", [koban2])
node2.set("CR", [(therow, thecol)])
node2.set("C", player) # I think this would be fun for the review
if node.has_property("CR"): node.unset("CR")
if node.has_property("SQ"): node.unset("SQ")
with open (channel_id+".sgf", "wb") as f:
f.write(game.serialise())
f.close()
os.system("/home/nik/.cargo/bin/sgf-render -f png --style fancy --label-sides nesw -o "+str(channel_id)+".png -n last "+str(channel_id)+".sgf")
# colour is "B" if black resigns, "W" if white resigns
def resign(channel_id, colour, file_name):
with open(channel_id+".sgf","rb") as f:
game = sgf.Sgf_game.from_bytes(f.read())
f.close()
node= game.root
node.set("RE", ("B" if colour=="W" else "W")+"+R")
with open (file_name, "wb") as f:
f.write(game.serialise())
f.close()
os.remove(channel_id+".sgf")