Rock-Paper-Scissors AI



RPS.rbxl (19.6 KB)

BUT WAIT, hold that thought…

If my supersuperbot can beat your superbot, and your superbot can beat the original bot, haven’t we just changed the game from Rock-Paper-Scissors to Bot-Superbot-Supersuperbot?

So what if I created an entirely new game called BSbSsb.rbxl, where it simulates the winning bot from each of the bots that we made?

And then, what if I made an omegabot that could beat that game based on your choice of bot each round?

3 Likes

Is anyone interested in a bot war?
RPSbots.rbxl (10.5 KB)

RPS in ServerScriptService contains some example modules and requires another one. You can insert and run your own module in it and save it and post the ID here.

Example module: repetition bot; finds last instance of current chain of games where your move and the opponent’s responses have been the same and predicts that the opponent will play the same move as last time
Repetition bot module:

return{"Repetition",function(t)
	local t2={}
	return function()
		local r,len=math.random(3)-1,0
		for i=#t-1,1,-1 do
			local v=t[i+1]
			for j=0,i do
				if t[i-j]==t[#t-j]and t2[i-j]==t2[#t2-j]then
					if j>=len then
						r,len=(v+1)%3,j+1
					end
				else
					break
				end
			end
		end
		t2[#t2+1]=r
		return r
	end
end}

The structure of a module is that it returns a table containing the name and the function.
The main function stores variables and returns another function that returns a move, which is 0 (rock), 1 (paper) or 2 (scissors.)
The main function is given a table (t) which contains the opponent’s past moves; t[#t] is their last move (and on the very first move t[#t] does not exist.)

example module that beats the last move the opponent made:

return{"BeatLast",function(t)
	return function()
		return #t==0 and math.random(3)-1 or(t[#t]+1)%3
	end
end}

Post some module IDs here (especially if you can beat Chameleon, which actually lags within the first hundred games.)

Apparently you can’t require other peoples’ modules from studio. That is a big issue. There’s not really any convenient way around that. Anyone who wants to participate needs to create a physical place uploaded to roblox and then join it online. That’s stupid.

100 games with OP’s version.

I must be a very random person :frowning:

What would be the best way to have a bot war? Could closed-source be feasible? Would it have to be in an online place? How could somebody test their program against others offline? or would it be an offline simulation with some example programs and an online submittal thing?

In-game would be cool. Possible way of doing that:
Players go to the place, paste the body of their function, hit upload, and their code would play 100 rounds against the current best bot. Code would be run via loadstring in a sandbox. When a bot wins, its code is saved into a datastore as the champion.
Players should be able to play against the top-rated bot themselves & view its source.
For offline testing, have the place description include a link to the model we’re using to test it right now.

loadstring it is, definitely. I was only thinking about modules. loadstring is definitely the best thing. Input might be a concern, though. I wish I could just read whatever string is in the clipboard. TextBoxes have that whole text limit thing. What would be the best way to handle inputs?

Online-wise I’m thinking single-player servers: a player joins a server and is presented with a leaderboard of the top bots and an option to submit one. They input a bot and it plays some games against other bots on their server just to make sure it’s safe and won’t crash instantly, and then it’s submitted. Each server will be continually simulating games between random pairs of bots and storing the win/draw/lose rates against each opponent in one long string in a DataStore. The game will load this string and send it to the client to calculate and generate the leaderboard locally. When should the datastore be updated with new win/draw/lose values? How could bot crashes be detected so they can be taken offline?

1 Like

Could always have a “Load from Pastebin” thing.

1 Like

My thoughts exactly, and people should also have the option to play against bots.

Wrap them in a pcall. You shouldn’t be able to submit a bot to the leaderboards if there’s a syntax error or if it crashes when it’s playing against another bot. Bots that are already in the leaderboard and break for whatever reason should be removed.

I think textboxes are the only way to read the clipboard.
Now input has to be really complicated. Why is that limit even a thing?

By crashes I mean server crashes, i.e. while true do end, or if math.random(100000)==1 then while true do end end, something along those lines submitted inside a bot. If a bot crashes the server, how could/should I detect and handle that and remove it from the pool with a note to the author once they return?

Pastebin definitely looks like the best means of input since it just returns the raw text. A bot for example would look like this: http://pastebin.com/raw/BkAtr4CT where it returns the name and function.

They’re single-player servers, so I don’t see it becoming a big issue for accidental screw-ups.
If someone actually makes a really good AI that gets put on the leaderboard but crashes people for some reason, that probably needs to be manually moderated. I don’t know whether game.OnClose fires when the server crashes, but I doubt it. If it does, something like this would work:

game.OnClose = function()
	if collectgarbage'count' > some_high_number then
		--figure out which bot screwed up (i.e. ran last)
		--delete it from the DS
	end
end

One concern I’m having right now is that programs forget everything once a new server starts. They’re given the history of their opponent’s moves and their own moves at the start, sure, but any variables they created or statistics they recorded are all gone when the server ends. A program tracking how well different strategies are working against another program, for example, will lose track.

Check it out if you want

you can write a program and upload it to pastebin, then take the pastebin ID and submit it here and it will test and enter it into a pool.

The program looks like this: return name, function(table of opponent's past moves, table of bot's past moves) return function() return current decision 0 or 1 or 2 end end
e.g. repetition:

return"Repetition",function(t,t2)
	return function()
		local r,len=math.random(3)-1,0
		for i=#t-1,1,-1 do
			local v=t[i+1]
			for j=0,i do
				if t[i-j]==t[#t-j]and t2[i-j]==t2[#t2-j]then
					if j>=len then
						r,len=(v+1)%3,j+1
					end
				else
					break
				end
			end
		end
		return r
	end
end

I’ve submitted a bunch of bots to test against. Right now each server picks a pair of bots and plays them for 15 seconds before storing the new data. The leaderboard reloads when you click on the leaderboard button. The server picks the two bots that have played the least matches together to battle. A timeout is assigned to the pair so another server doesn’t also try to play the same pair and overwrite things. Win rates ignore draws.

1 Like

It would be nice if there were some simple sample code for developing my bot to make sure it works right in your game environment.

This is the code I have so far. Is this accurate? (Edit: according to waffle’s bots on the post below, it seems to be accurate).

local Bot1Moves, Bot2Moves = {}, {}
local Wins = {Bot1 = 0, Draw = 0, Bot2 = 0}
local Bot1Name, Bot1Function = require(script.ScissorsScissorsRockBot)()
local Bot2Name, Bot2Function = require(script.PaperPaperScissorsBot)()

Bot1MoveFunction, Bot2MoveFunction = Bot1Function(Bot2Moves, Bot1Moves), Bot2Function(Bot1Moves, Bot2Moves)

for i = 1, 2000 do
	local Bot1Move = Bot1MoveFunction()
	table.insert(Bot1Moves, Bot1Move)
	local Bot2Move = Bot2MoveFunction()
	table.insert(Bot2Moves, Bot2Move)
	if Bot1Move == Bot2Move then
		Wins.Draw = Wins.Draw + 1
	elseif Bot1Move == (Bot2Move + 1) % 3 then
		Wins.Bot1 = Wins.Bot1 + 1
	else
		Wins.Bot2 = Wins.Bot2 + 1
	end
end

print(Bot1Name .. ":", tostring(Wins.Bot1) .. ";", "Draw:", tostring(Wins.Draw) .. ";", Bot2Name .. ":", tostring(Wins.Bot2))\

The modules I have look like this:

script.ScissorsScissorsRockBot

return function()


return"ScissorsScissorsRockBot",function(t,t2)
	local i = 0
	return function()
		i = i % 3 + 1
		return (i == 3 and 0 or 2)
	end
end


end

script.PaperPaperScissorsBot

return function()


return"PaperPaperScissorsBot",function(t,t2)
	local i = 0
	return function()
		i = i % 3 + 1
		return (i == 3 and 2 or 1)
	end
end


end

I have the bot encased in a function because modules can only return exactly one value.

Here’s a testing script where you can just add the function into the code:

function rock()return function()return 0 end end
function paper()return function()return 1 end end
function scissors()return function()return 2 end end
function cycle(t)return function()return #t%3 end end
function rand()return function()return math.random(3)-1 end end
function RRPPSS(t)return function()return math.floor(#t/2)%3 end end
function copy(t)return function()return #t==0 and math.random(3)-1 or t[#t]end end
function count(t)
	local r,p,s=0,0,0
	local function f(x)
		if x==0 then r=r+1 elseif x==1 then p=p+1 else s=s+1 end
	end
	for i=1,#t do f(t[i])end
	return function()
		if #t~=0 then f(t[#t])else return math.random(3)-1 end
		if r>p and r>s then return 1
		elseif p>s and p>r then return 2
		elseif s>p and s>r then return 0
		elseif p>s and p==r then return math.random(2)-1
		elseif s>r and s==p then return math.random(2)
		elseif r>p and r==s then return(math.random(2)+1)%3
		else return math.random(3)-1
		end
	end
end
function hist(t)
	return function()
		return #t==0 and math.random(3)-1 or(t[math.random(#t)]+1)%3
	end
end
function beatlast(t)
	return function()
		return #t==0 and math.random(3)-1 or(t[#t]+1)%3
	end
end
function rockpaper(t)
	return function()return #t%2 end
end
function change()
	local last=math.random(3)-1
	return function()
		last=(last+math.random(2))%3
		return last
	end
end

local f,set,n={},{},{"rock","paper","scissors","cycle","rand","RRPPSS","copy","count","hist","beatlast","rockpaper","change"}
for i=1,#n do
	n[n[i]]=i
	f[i]=getfenv()[n[i]]
	set[n[i]]={0,0}
end

for i=1,#f do
	set[i]={}
	for j=i+1,#f do
		local hA,hB={},{}
		local a,b=f[i](hB,hA),f[j](hA,hB)
		set[i][j]={a,b,hA=hA,hB=hB,an=n[i],bn=n[j],a=0,b=0,d=0}
	end
end
math.randomseed(os.time())
local ct=os.clock()
for _=0,200 do
	for i=1,#f-1 do
		for j=i+1,#f do
			local t=set[i][j]
			local A,B=t[1](),t[2]()
			table.insert(t.hA,A)
			table.insert(t.hB,B)
			if _>0 then
				if A==B then
					t.d=t.d+1
				elseif A==(B+1)%3 then
					t.a=t.a+1
					set[t.an][1]=set[t.an][1]+1
					set[t.bn][2]=set[t.bn][2]+1
				else
					t.b=t.b+1
					set[t.bn][1]=set[t.bn][1]+1
					set[t.an][2]=set[t.an][2]+1
				end
			end
		end
	end
	if os.clock()-ct>1 then ct=os.clock()print(_)end
end
print("- clock: "..os.clock())
local N={}
local V={}
for i=1,#n do
	local x,y=set[n[i]][1],set[n[i]][2]
	if x==0 and y==0 then x,y=1,1 end
	N[i],V[i]=n[i],x/(x+y)
end
repeat
	local k=true
	for i=1,#V-1 do
		if V[i+1]>V[i]then
			V[i],V[i+1]=V[i+1],V[i]
			N[i],N[i+1]=N[i+1],N[i]
			k=false
		end
	end
until k
local mlen=1
local ratio={}
for i=1,#n do mlen=math.max(mlen,#N[i]+1)ratio[i]={}end
for i=1,#n do
	for j=1,#n do
		if i~=j then
			local x,r=set[n[n[i]]][n[n[j]]]
			if x then r=x.a/(x.a+x.b)else x=set[n[n[j]]][n[n[i]]]r=x.b/(x.a+x.b)end
			if x.a==0 and x.b==0 then r=.5 end
			ratio[i][j]=r
			ratio[j][i]=1-r
		end
	end
end
local score={}
for i=1,#n do score[n[i]]=1 end
function scoreiter()
	local m=0
	for i=1,#n do
		local v=0
		for j=1,#n do
			if i~=j then
				v=v+ratio[i][j]--*score[n[j]]
			end
		end
		if v>m then m=v end
		score[n[i]]=v
	end
	for i=1,#n do score[n[i]]=score[n[i]]/m end
end
for _=1,1 do scoreiter()end
local N,Ni={},{}for i=1,#n do N[i],Ni[i]=n[i],i end
repeat
	local k=true
	for i=1,#N-1 do
		if score[N[i]]<score[N[i+1]]then
			N[i],N[i+1]=N[i+1],N[i]
			Ni[i],Ni[i+1]=Ni[i+1],Ni[i]
			k=false
		end
	end
until k
for i=1,#n do
	print(string.format("%-"..mlen.."s%.05f",N[i],score[N[i]]))
end

for i=1,#N do
	local pr={}
	local tot=0
	local op,ov="",math.huge
	for j=1,#N do
		if i~=j then
			local x,r=set[n[N[i]]][n[N[j]]]
			if x then r=x.a/(x.a+x.b)else x=set[n[N[j]]][n[N[i]]]r=x.b/(x.a+x.b)end
			if x.a==0 and x.b==0 then r=.5 end
			if ov>r then op,ov=N[j],r end
			r=r
			pr[#pr+1]=string.format("- %-"..mlen.."s%.02f",N[j],r)tot=tot+r
		end
	end
	print(string.format("%-"..mlen.."s%.02f, %-"..mlen.."s%.05f",N[i],V[i]*100,op,ov))
	--for _,v in pairs(pr)do print(v)end
	--print("= "..tot)
end

You can change the number of games played per pair on line 64. List the names of all of the bot functions you want to test at once in the “n” table on line 47.

It would also be nice if the sample code were a little less obfuscated, and I could understand what the variable names meant :confused:

well “t” in all of the example bots is a table of all of the moves the opponent has made so far, where t[#t] is their previous move. if you mean the actual simulation code, well, I didn’t take the time to come up with meaningful variable names. “n” is the table of bot names, “f” is the table of bot functions that is populated by n, “set” is a table of win/lose values. The bot battling logic goes on in the for _ loop and then some scoring algorithm that I made up goes on to figure out who’s better than who and it prints some numbers.

This part
Bot1MoveFunction, Bot2MoveFunction = Bot1Function(Bot1Moves, Bot2Moves), Bot2Function(Bot1Moves, Bot2Moves)
should pass Bot2Moves, Bot1Moves to Bot1MoveFunction. The two tables are the opponent’s move history and then the bot’s own move history as the second parameter.

1 Like

you switched the wrong one in your edit, should be this
Bot1MoveFunction, Bot2MoveFunction = Bot1Function(Bot2Moves, Bot1Moves), Bot2Function(Bot1Moves, Bot2Moves)
other bot table, bot table

Just fixed that before you replied.