Browse Source

fix tron ai

content-update
Clement Denis 4 years ago
parent
commit
94ff55a50c
  1. 82
      subjects/tron/ai/hard.js
  2. 30
      subjects/tron/ai/license-to-kill.js
  3. 37
      subjects/tron/ai/random.js
  4. 82
      subjects/tron/ai/right.js
  5. 32
      subjects/tron/ai/snail.js
  6. 139
      subjects/tron/index.html
  7. 84
      subjects/tron/lib/state.js
  8. 4
      subjects/tron/tron.en.md

82
subjects/tron/ai/hard.js

@ -16,100 +16,100 @@ const isAlley = ({ x, y }) => !isFree({ x, y }) || !isInBounds({ x, y })
// this functions will find the best path, so the path that has more empty spaces
// so use `isFree`,
const findBestPath = (state) => {
const findBestPath = ({ ai }) => {
let arr = []
let car = state.player.cardinal
let car = ai.cardinal
// if it as a block on the symmetric position it must
// simulate the symmetric position and see witch path is the best
if (
(car === 3 || car === 0) &&
!isFree({ x: state.player.x - 1, y: state.player.y - 1 }) &&
isFree({ x: state.player.x, y: state.player.y - 1 }) &&
isFree({ x: state.player.x - 1, y: state.player.y })
!isFree({ x: ai.x - 1, y: ai.y - 1 }) &&
isFree({ x: ai.x, y: ai.y - 1 }) &&
isFree({ x: ai.x - 1, y: ai.y })
) {
let xad = state.player.x - 1
let yad = state.player.y - 1
let xad = ai.x - 1
let yad = ai.y - 1
let choose = [
calDistance(xad + 1, yad, 1, 0),
calDistance(state.player.x, state.player.y - 1, car, 0),
calDistance(state.player.x - 1, state.player.y, car, 0),
calDistance(ai.x, ai.y - 1, car, 0),
calDistance(ai.x - 1, ai.y, car, 0),
calDistance(xad, yad + 1, 2, 0),
]
let index = choose.indexOf(Math.max(...choose))
return index === 0 || index === 1
? state.player.coords[0]
: state.player.coords[3]
? ai.coords[0]
: ai.coords[3]
}
if (
(car === 1 || car === 0) &&
!isFree({ x: state.player.x + 1, y: state.player.y - 1 }) &&
isFree({ x: state.player.x, y: state.player.y - 1 }) &&
isFree({ x: state.player.x + 1, y: state.player.y })
!isFree({ x: ai.x + 1, y: ai.y - 1 }) &&
isFree({ x: ai.x, y: ai.y - 1 }) &&
isFree({ x: ai.x + 1, y: ai.y })
) {
let xad = state.player.x + 1
let yad = state.player.y - 1
let xad = ai.x + 1
let yad = ai.y - 1
// choose will save the biggest path to be chosen
// [ down, line1, line0, left ]
let choose = [
calDistance(xad, yad + 1, 2, 0),
calDistance(state.player.x + 1, state.player.y, car, 0),
calDistance(state.player.x, state.player.y - 1, car, 0),
calDistance(ai.x + 1, ai.y, car, 0),
calDistance(ai.x, ai.y - 1, car, 0),
calDistance(xad - 1, yad, 3, 0),
]
let index = choose.indexOf(Math.max(...choose))
return index === 0 || index === 1
? state.player.coords[1]
: state.player.coords[0]
? ai.coords[1]
: ai.coords[0]
}
if (
(car === 2 || car === 1) &&
!isFree({ x: state.player.x + 1, y: state.player.y + 1 }) &&
isFree({ x: state.player.x, y: state.player.y + 1 }) &&
isFree({ x: state.player.x + 1, y: state.player.y })
!isFree({ x: ai.x + 1, y: ai.y + 1 }) &&
isFree({ x: ai.x, y: ai.y + 1 }) &&
isFree({ x: ai.x + 1, y: ai.y })
) {
let xad = state.player.x + 1
let yad = state.player.y + 1
let xad = ai.x + 1
let yad = ai.y + 1
// choose will save the biggest path to be chosen
// [ left, line2, line1, up ]
let choose = [
calDistance(xad - 1, yad, 3, 0),
calDistance(state.player.x, state.player.y + 1, car, 0),
calDistance(state.player.x + 1, state.player.y, car, 0),
calDistance(ai.x, ai.y + 1, car, 0),
calDistance(ai.x + 1, ai.y, car, 0),
calDistance(xad, yad - 1, 0, 0),
]
let index = choose.indexOf(Math.max(...choose))
return index === 0 || index === 1
? state.player.coords[2]
: state.player.coords[1]
? ai.coords[2]
: ai.coords[1]
}
if (
(car === 2 || car === 3) &&
!isFree({ x: state.player.x - 1, y: state.player.y + 1 }) &&
isFree({ x: state.player.x, y: state.player.y + 1 }) &&
isFree({ x: state.player.x - 1, y: state.player.y })
!isFree({ x: ai.x - 1, y: ai.y + 1 }) &&
isFree({ x: ai.x, y: ai.y + 1 }) &&
isFree({ x: ai.x - 1, y: ai.y })
) {
let xad = state.player.x - 1
let yad = state.player.y + 1
let xad = ai.x - 1
let yad = ai.y + 1
// choose will save the biggest path to be chosen
// [ right, line2, line3, up ]
let choose = [
calDistance(xad + 1, yad, 1, 0),
calDistance(state.player.x - 1, state.player.y, car, 0),
calDistance(state.player.x, state.player.y + 1, car, 0),
calDistance(ai.x - 1, ai.y, car, 0),
calDistance(ai.x, ai.y + 1, car, 0),
calDistance(xad, yad - 1, 0, 0),
]
let index = choose.indexOf(Math.max(...choose))
return index === 0 || index === 1
? state.player.coords[3]
: state.player.coords[2]
? ai.coords[3]
: ai.coords[2]
}
for ({ x, y, cardinal } of state.player.coords) {
for ({ x, y, cardinal } of ai.coords) {
// if everything is ok it must continue with the best path
arr.push(calDistance(x, y, cardinal, 0))
}
return state.player.coords[arr.indexOf(Math.max(...arr))]
return ai.coords[arr.indexOf(Math.max(...arr))]
}
// recursion
@ -166,6 +166,6 @@ const calDistance = (x, y, car, count) => {
const addToMap = ({ x, y }) => MAP[y * SIZE + x] = 1
const update = (state) => {
state.players.forEach(addToMap)
state.ais.forEach(addToMap)
findBestPath(state)
}

30
subjects/tron/ai/license-to-kill.js

@ -58,19 +58,19 @@ const hasLateralWalls = (card, x, y) => {
}
}
const goDirection = (state, card) =>
ok(state.coords[card]) &&
!isAlley(card, state.x, state.y) &&
state.coords[card]
const goDirection = (ai, card) =>
ok(ai.coords[card]) &&
!isAlley(card, ai.x, ai.y) &&
ai.coords[card]
const findEnemy = state =>
state.players.filter(p => state.player.name !== p.name)[0]
state.ais.filter(p => state.ai.name !== p.name)[0]
const seekEnemy = state => {
if (state.players.length === 1) return
if (state.ais.length === 1) return
const enemy = findEnemy(state)
const xPla = state.player.x
const yPla = state.player.y
const xPla = state.ai.x
const yPla = state.ai.y
const xOpo = enemy.x
const yOpo = enemy.y
const xDif = xPla - xOpo
@ -78,20 +78,20 @@ const seekEnemy = state => {
return (
(Math.abs(xDif) > Math.abs(yDif) &&
goDirection(state.player, xPla < xOpo ? 1 : 3)) ||
goDirection(yPla < yOpo ? 2 : 0)
goDirection(state.ai, xPla < xOpo ? 1 : 3)) ||
goDirection(state.ai, yPla < yOpo ? 2 : 0)
)
}
const walk = state =>
seekEnemy(state) ||
goDirection(state.player, 0) ||
goDirection(state.player, 1) ||
goDirection(state.player, 2) ||
goDirection(state.player, 3)
goDirection(state.ai, 0) ||
goDirection(state.ai, 1) ||
goDirection(state.ai, 2) ||
goDirection(state.ai, 3)
const addToMap = ({ x, y }) => (MAP[y * SIZE + x] = 1)
const update = state => {
state.players.forEach(addToMap)
state.ais.forEach(addToMap)
return walk(state)
}

37
subjects/tron/ai/random.js

@ -2,6 +2,7 @@ const SIZE = 100
const MAP = new Int8Array(SIZE * SIZE) // State of the Map
const isFree = ({ x, y }) => MAP[y * SIZE + x] === 0 // 0 = block free
const isOccupied = ({ x, y }) => MAP[y * SIZE + x] === 1 // 1 = block occupied
const isMe = ai => ai.me // is true if this AI is you
// `inBounds` check if our coord (n) is an existing index in our MAP
const inBounds = n => n < SIZE && n >= 0
@ -17,37 +18,39 @@ const addToMap = ({ x, y }) => MAP[y * SIZE + x] = 1
// `update` this function is called at each turn
const update = state => {
// update is called with a state argument that has 2 properties:
// players: an array of all the players
// player: the player for this AI
// Each players contains:
// color: A number that represent the color of a player
// name: A string of the player name
// score: A number of the total block collected by this player
// x: The horizontal position of the player
// y: The vertical position of the player
// update is called with a state argument that has 3 properties:
// ais: an array of all the AIs
// ai: the current AI
// Each AIs contains:
// color: A number that represent the color of a AI
// name: A string of the AI name
// score: A number of the total block collected by this AI
// x: The horizontal position of the AI
// y: The vertical position of the AI
// index: The computed index of the coord (x * 100 + y)
// coords: An array of 4 coordinates of the nearest blocks
// [ NORTH, EAST, SOUTH, WEST ]
// N
// W + E
// S
// Each coordinate contains:
// x: The horizontal position
// y: The vertical position
// index: The computed index of the coord (x * 100 + y)
// cardinal: A number between 0 and 3 that represent the cardinal
// [ 0: NORTH, 1: EAST, 2: SOUTH, 3: WEST ]
// N
// W + E
// S
//
// direction: A number between 0 and 3 that represent the direction
// [ 0: FORWARD, 1: RIGHT, 2: BACKWARD, 3: LEFT ]
// Saving state between each updates:
// I update the MAP with the new position of each players
state.players.forEach(addToMap)
// I update the MAP with the new position of each AIs
state.ais.forEach(addToMap)
// Actual AI logic:
// I filter my array of coords to keep only those that are in bounds
const coordsInBound = state.player.coords.filter(isInBounds)
const coordsInBound = state.ai.coords.filter(isInBounds)
// I filter again to keep coords that are free
const available = coordsInBound.filter(isFree)

82
subjects/tron/ai/right.js

@ -1,48 +1,68 @@
const SIZE = 100
const [FREE, UNSAFE, FILLED] = Array(3).keys()
const [FREE, FILLED] = Array(3).keys()
const [FORWARD, RIGHT, BACKWARD, LEFT] = Array(4).keys()
const [NORTH, EAST, SOUTH, WEST] = Array(4).keys()
const MAP = new Int8Array(SIZE * SIZE)
const isFree = ({ x, y }) => MAP[y * SIZE + x] < FILLED
const isUnsafe = ({ x, y }) => MAP[y * SIZE + x] === UNSAFE
const setUnsafe = ({ x, y }) => MAP[y * SIZE + x] = UNSAFE
const isFree = ({ x, y }) => MAP[y * SIZE + x] === FREE
const setFilled = ({ x, y }) => MAP[y * SIZE + x] = FILLED
const inBounds = n => n < SIZE && n >= 0
const isInBounds = ({ x, y }) => inBounds(x) && inBounds(y)
const isForward = el => el.direction === 0
const isRight = el => el.direction === 1
const isLeft = el => el.direction === 3
const isNotBackward = el => el.direction !== BACKWARD
const isForward = el => el.direction === FORWARD
const isRight = el => el.direction === RIGHT
const isLeft = el => el.direction === LEFT
const goForward = arr => arr.find(isForward)
const goRight = arr => arr.find(isRight)
const goLeft = arr => arr.find(isLeft)
const isOtherPlayer = player => !player.isOwnPlayer
const isOtherAI = ai => !ai.me
const isAlive = ai => !ai.dead
let wallReached = false
const pickForwardOrRightOrLeft = arr => {
if (arr.length === 3 && !wallReached) {
return goForward(arr) || goRight(arr) || goLeft(arr)
}
if (arr.length <= 2 && !wallReached) {
wallReached = true
return goRight(arr) || goLeft(arr) || goForward(arr)
}
const pickDirection = arr => goLeft(arr) || goForward(arr) || goRight(arr)
return goLeft(arr) || goForward(arr) || goRight(arr)
const getNearestWallCardinal = ai => {
const n = ai.y
const e = 99 - ai.x
const s = 99 - ai.y
const w = ai.x
const smallest = Math.min(n, e, s, w)
if (smallest === n) return NORTH
if (smallest === e) return EAST
if (smallest === s) return SOUTH
if (smallest === w) return WEST
}
const update = ({ player, players }) => {
players.forEach(setFilled)
players
.filter(isOtherPlayer)
.flatMap(({ coords }) => coords)
let reachForWall = true
const update = ({ ai, ais }) => {
ais.forEach(setFilled)
const enemyCoordsIndices = ais
.filter(isOtherAI)
.filter(isAlive)
.flatMap(ai => ai.coords)
.map(coord => coord.index)
const possibleCoords = ai.coords
.filter(isInBounds)
.filter(isFree)
.forEach(setUnsafe)
const coordsInBound = player.coords
.filter(isInBounds)
.filter(isNotBackward)
const safeCoords = possibleCoords.filter(c => !enemyCoordsIndices.includes(c.index))
if (reachForWall) {
const cardinal = getNearestWallCardinal(ai)
const coord = ai.coords.find(c => c.cardinal === cardinal)
if (safeCoords.includes(coord)) return coord
}
const available = pickForwardOrRightOrLeft(coordsInBound.filter(isFree))
const lastResort = pickForwardOrRightOrLeft(coordsInBound.filter(isUnsafe))
return available || lastResort
reachForWall = false
return pickDirection(safeCoords) || pickDirection(possibleCoords)
}
// hej

32
subjects/tron/ai/snail.js

@ -1,11 +1,8 @@
const SIZE = 100
const FREE = 0
const UNSAFE = -1
const FILLED = 1
const MAP = new Int8Array(SIZE * SIZE)
const isFree = ({ x, y }) => MAP[y * SIZE + x] < FILLED
const isUnsafe = ({ x, y }) => MAP[y * SIZE + x] === UNSAFE
const setUnsafe = ({ x, y }) => MAP[y * SIZE + x] = UNSAFE
const isFree = ({ x, y }) => MAP[y * SIZE + x] === FREE
const setFilled = ({ x, y }) => MAP[y * SIZE + x] = FILLED
const inBounds = n => n < SIZE && n >= 0
const isInBounds = ({ x, y }) => inBounds(x) && inBounds(y)
@ -16,25 +13,30 @@ const isLeft = el => el.direction === 3
const goForward = arr => arr.find(isForward)
const goRight = arr => arr.find(isRight)
const goLeft = arr => arr.find(isLeft)
const isOtherPlayer = player => !player.isOwnPlayer
const isAlive = ai => !ai.dead
const isOtherAI = ai => !ai.me
// `snailIt` goes with the form of a snail
const snailIt = arr => goRight(arr) || goForward(arr) || goLeft(arr)
const update = ({ player, players }) => {
players.forEach(setFilled)
players
.filter(isOtherPlayer)
.flatMap(({ coords }) => coords)
const update = ({ ai, ais }) => {
ais.forEach(setFilled)
const enemyCoordsIndices = ais
.filter(isOtherAI)
.filter(isAlive)
.flatMap(ai => ai.coords)
.map(coord => coord.index)
const possibleCoords = ai.coords
.filter(isInBounds)
.filter(isFree)
.forEach(setUnsafe)
const coordsInBound = player.coords
const safeCoords = possibleCoords.filter(c => !enemyCoordsIndices.includes(c.index))
const coordsInBound = ai.coords
.filter(isInBounds)
.filter(isNotBackward)
const available = snailIt(coordsInBound.filter(isFree))
const lastResort = snailIt(coordsInBound.filter(isUnsafe))
return available || lastResort
return snailIt(safeCoords) || snailIt(possibleCoords)
}

139
subjects/tron/index.html

@ -37,12 +37,12 @@ h2 b {
}
#player-0 h2:after {
#ai-0 h2:after {
background-image: linear-gradient(to left, #0000, currentColor 18px, #0000);
margin-right: -10px;
}
#player-1 h2:after {
#ai-1 h2:after {
margin-left: -10px;
background-image: linear-gradient(to right, #0000, currentColor 18px, #0000);
}
@ -91,18 +91,18 @@ svg {
color: cyan;
}
#player-0, #player-1 {
#ai-0, #ai-1 {
margin: 12px;
padding: 6px;
user-select: none;
}
#player-0 {
#ai-0 {
grid-column: 1;
text-align: right;
}
#player-1 { grid-column: 4 }
#ai-1 { grid-column: 4 }
#controls {
@ -136,8 +136,8 @@ svg {
@media (orientation: portrait) {
h2 { max-width: 300px }
#controls { grid-row: 3 }
#player-0 { grid-column: 2; grid-row: 4 }
#player-1 { grid-column: 3; grid-row: 4 }
#ai-0 { grid-column: 2; grid-row: 4 }
#ai-1 { grid-column: 3; grid-row: 4 }
}
</style>
@ -147,14 +147,14 @@ svg {
<svg fill="cyan" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 449.9 184.1">
<defs><filter id="g"><feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="cyan"/></filter></defs>
<path style="filter:url(#g)" d="M36 62.6H5.2v-19H36v19zM38.4 39H3l-3 2.8v23l3 2.5h35.4l3.1-2.7V41.7L38.4 39z"/><g clip-path="url(#clipPath4354)" transform="matrix(1.25 0 0 -1.25 -135 647)"><path style="filter:url(#g)" d="M333.3 426.4a18.9 18.9 0 00-19.1 18.5c0 10.3 8.6 18.6 19.1 18.6a18.9 18.9 0 0019.2-19.2c-.4-10-8.8-18-19.2-18m0 41.2c-12.8 0-23.2-10.1-23.2-22.6a22.9 22.9 0 0123.2-22.5c12.9 0 23.2 10 23.2 22.5a22.9 22.9 0 01-23.2 22.6"/><path style="filter:url(#g)" d="M333.3 406.6c-21.8 0-39.4 17.2-39.4 38.3s17.6 38.3 39.4 38.3c21.8 0 39.5-17.1 39.5-38.3s-17.7-38.3-39.5-38.3m0 80.6a42.9 42.9 0 01-43.4-42.9c.3-23 19.6-41.6 43.4-41.6a42.9 42.9 0 0143.5 42.2 42.9 42.9 0 01-43.5 42.3M463.7 407H457l-28.4 31.8v9.5H448v34.5h15.6V407zm1.5 79.5H447l-2.7-3.2v-31.5h-17.1c-2.2 0-2.2-1.2-2.2-2.1v-12.2l30.4-34.4h9.8l2.6 2.4v78.1l-2.6 3zM419.2 441.1h-19.6v-33.9h-15.7v75.6h6.9l28.4-32.1V441zm-26.9 45.3h-9.8l-2.6-2.5v-78.1l2.6-2.7h18.2l2.7 3v31.6h17.1c2.2 0 2.2 1.1 2.2 2V452l-30.4 34.5zM283.2 407h-18.8l-31.8 32.5v13.1h52a22.5 22.5 0 00-22-15.5h-9.2l29.8-30m-20 26.2c13.8.3 24.6 9.7 26.2 23h-58l-2.7-2.6V439c0-.5 0-1 .4-1.5l33.4-34.3h26.3v4.1l-26.2 26h.6zM220.2 407h-15.7v45.6h15.7V407zm1.3 49.4h-18.7l-2.5-2.7v-48l2.7-2.6h18.6l2.6 3v47.6l-2.7 2.7z"/><path style="filter:url(#g)" d="M172.5 467.4c-3.7 0-6.8-3.1-6.8-6.8v-53.5l-15.6-.1v54.8a20.7 20.7 0 0021.2 20.8h91.2a23 23 0 0022-15.2h-112zm90 19.1h-91.1a24.6 24.6 0 01-25.6-25v-55.9l3-2.4h18.5l2.4 2.9v54.4a3 3 0 003.2 3h116.4a27 27 0 01-26.7 23"/></g></svg></div>
<div id="player-0">
<div id="ai-0">
<h2><b>AI-0</b></h2>
<b>LOADING</b>
<br>
<b>0</b>
</div>
<canvas width="1200" height="1200"></canvas>
<div id="player-1">
<div id="ai-1">
<h2><b>AI-1</b></h2>
<b>LOADING</b>
<br>
@ -187,18 +187,18 @@ hide.onclick = () => {
localStorage.hide || hide.parentElement.classList.remove('hide')
const buildInfo = (player, i) => {
const elem = document.getElementById(`player-${i}`)
const buildInfo = (ai, i) => {
const elem = document.getElementById(`ai-${i}`)
if (!elem) return { score: () => {}, status: () => {} }
const [name, status, score] = [...elem.children].map(e => e.firstChild).filter(Boolean)
name.firstChild.data = player.name
elem.style.color = `hsl(${player.hue*360}, 100%, 70%)`
elem.style.textShadow = `0 0 6px hsla(${player.hue*360}, 100%, 70%, 0.4)`
name.firstChild.data = ai.name
elem.style.color = `hsl(${ai.hue*360}, 100%, 70%)`
elem.style.textShadow = `0 0 6px hsla(${ai.hue*360}, 100%, 70%, 0.4)`
elem.style.width = 'calc(100% - 36px)' // force width force redraw
// this fix a bug on chrome, not re-applying `currentColor` to gradients
return {
score: text => player.dead || (score.data = text),
score: text => ai.dead || (score.data = text),
status: text => status.data = text,
}
}
@ -212,8 +212,11 @@ const getAIUrl = (login, sha, ai) =>
`https://rawcdn.githack.com/${login}/tron/${sha}/ai/${ai || login}.js`
const getSha = async login => (await (await fetch(getShaUrl(login))).json()).sha
const toBlob = async r =>
new Blob([`${await r.text()}${injectedCode}`], { type : 'text/javascript' })
const toBlob = async r => {
if (!r.ok) throw Error(`${r.status}: ${r.statusText}`)
const code = await r.text()
return new Blob([`${code}${injectedCode}`], { type : 'text/javascript' })
}
const toUrlObject = b => URL.createObjectURL(b, { type: 'text/javascript' })
const memo = {}
const fetchBlob = url => memo[url]
@ -241,10 +244,10 @@ const formatURL = url => {
const start = async ({ urls, seed }) => {
if (urls.length < 2) throw Error('2 AI urls are required to play')
const players = init({ players: urls, seed })
const ais = init({ ais: urls, seed })
let turn = 1, maxTurn = 1, t = 1, cap = 10000, down
const done = new Set(players)
const done = new Set(ais)
const setPosition = e => {
const v = typeof e === 'number'
@ -271,101 +274,101 @@ const start = async ({ urls, seed }) => {
let requested, timeout
const refresh = () => {
requested = update(t)
players[0].score(turn)
players[1].score(turn)
ais[0].score(turn)
ais[1].score(turn)
loading.style.transform = `translate(${((turn / cap)*600)-600}px)`
position.style.transform = `translate(${((t / cap)*600)-600}px)`
}
const next = (player) => {
clearTimeout(player.timeout)
done.add(player)
const next = (ai) => {
clearTimeout(ai.timeout)
done.add(ai)
requested || (requested = requestAnimationFrame(refresh))
// check if all AI are done
if (done.size >= players.length) {
if (done.size >= ais.length) {
turn++
const data = `[${players.join(',')}]`
const data = `[${ais.join(',')}]`
let allDead = true
for (const p of players) {
if (p.dead) continue
for (const a of ais) {
if (a.dead) continue
allDead ? (allDead = false) : cap--
done.delete(p)
p.worker.postMessage(data)
p.timeout = setTimeout(p.kill, 50, 'TIMEOUT')
done.delete(a)
a.worker.postMessage(data)
a.timeout = setTimeout(a.kill, 50, 'TIMEOUT')
}
allDead && (cap = turn)
t === maxTurn ? (t = maxTurn = turn) : (maxTurn = turn)
}
}
await Promise.all(players.map(async (player, i) => {
const info = buildInfo(player, i)
player.score = info.score
await Promise.all(ais.map(async (ai, i) => {
const info = buildInfo(ai, i)
ai.score = info.score
const kill = (cause) => {
if (player.dead) return
console.log(`${player.name} died because he ${cause} at ${player.x} ${player.y}`)
player.cause = cause
player.dead = true
player.worker && player.worker.terminate()
if (ai.dead) return
console.log(`${ai.name} died because he ${cause} at ${ai.x} ${ai.y}`)
ai.cause = cause
ai.dead = true
ai.worker && ai.worker.terminate()
info.status(cause)
}
// init the worker
try {
const url = await fetchBlob(await formatURL(player.name))
player.worker = new Worker(url, { type: 'module', name: player.name })
const url = await fetchBlob(await formatURL(ai.name))
ai.worker = new Worker(url, { type: 'module', name: ai.name })
await new Promise((s, f) => {
player.worker.onmessage = e => e.data === 'loaded' ? s() : f(Error(e.data))
player.worker.onerror = f
player.worker.postMessage(JSON.stringify({ id: player.name, seed }))
ai.worker.onmessage = e => e.data === 'loaded' ? s() : f(Error(e.data))
ai.worker.onerror = f
ai.worker.postMessage(JSON.stringify({ id: ai.name, seed }))
})
// activate the AI
info.status('ACTIVE')
player.kill = cause => {
ai.kill = cause => {
kill(cause)
next(player)
next(ai)
}
} catch (err) {
console.error(err)
kill('FAILED-TO-LOAD')
}
move(player.x, player.y, player.color, turn)
move(ai.x, ai.y, ai.color, turn)
// handle each response from the AI
player.worker.onmessage = ({ data }) => {
if (done.has(player)) return player.kill('UNEXPECTED-MESSAGE')
if (!data) return player.kill('STUCK')
ai.worker.onmessage = ({ data }) => {
if (done.has(ai)) return ai.kill('UNEXPECTED-MESSAGE')
if (!data) return ai.kill('STUCK')
const { x, y } = JSON.parse(data)
if (typeof x !== 'number' || typeof y !== 'number') return player.kill('INVALID_INPUT')
if (notInBounds(x) || notInBounds(y)) return player.kill('OUT_OF_BOUNDS')
if (typeof x !== 'number' || typeof y !== 'number') return ai.kill('INVALID_INPUT')
if (notInBounds(x) || notInBounds(y)) return ai.kill('OUT_OF_BOUNDS')
if (
!(x === player.x - 1 && y === player.y) &&
!(x === player.x + 1 && y === player.y) &&
!(x === player.x && player.y === y + 1) &&
!(x === player.x && player.y === y - 1)
) return player.kill('IMPOSSIBLE_MOVE')
player.x = x
player.y = y
const failure = move(x, y, player.color, turn)
!(x === ai.x - 1 && y === ai.y) &&
!(x === ai.x + 1 && y === ai.y) &&
!(x === ai.x && ai.y === y + 1) &&
!(x === ai.x && ai.y === y - 1)
) return ai.kill('IMPOSSIBLE_MOVE')
ai.x = x
ai.y = y
const failure = move(x, y, ai.color, turn)
if (failure === turn) {
for (const p of players) {
p.x === player.x && p.y === player.y && p.kill('MULTI-CRASH')
for (const a of ais) {
a.x === ai.x && a.y === ai.y && a.kill('MULTI-CRASH')
}
colorize(x, y, 0xffffff)
return player.kill('MULTI-CRASH')
} else if (failure) return player.kill('CRASH')
next(player)
return ai.kill('MULTI-CRASH')
} else if (failure) return ai.kill('CRASH')
next(ai)
}
player.worker.onerror = () => player.kill('AI-ERROR')
ai.worker.onerror = () => ai.kill('AI-ERROR')
}))
next(players[0])
next(ais[0])
}
const params = new URLSearchParams(location.search)

84
subjects/tron/lib/state.js

@ -27,67 +27,93 @@ const hslToRgb = (h, s, l) => {
return toInt(toRange(r), toRange(g), toRange(b))
}
export const init = ({ players, seed }) => {
export const init = ({ ais, seed }) => {
let w = (123456789 + seed) & 0xffffffff
let z = (987654321 - seed) & 0xffffffff
const rand = () => {
let t = seed += 0x6D2B79F5
t = Math.imul(t ^ t >>> 15, t | 1)
t ^= t + Math.imul(t ^ t >>> 7, t | 61)
return ((t ^ t >>> 14) >>> 0) / 4294967296
z = (36969 * (z & 65535) + (z >>> 16)) & 0xffffffff
w = (18000 * (w & 65535) + (w >>> 16)) & 0xffffffff
return (((z << 16) + (w & 65535)) >>> 0) / 4294967296
}
const angle = (Math.PI * 2) / players.length
const rate = (SIZE / players.length / SIZE)
const angle = (Math.PI * 2) / ais.length
const rate = (SIZE / ais.length / SIZE)
const shift = angle * rand()
// shuffle using seeded random
players.sort((a, b) => a.name - b.name)
let i = players.length, j, tmp
ais.sort((a, b) => a.name - b.name)
let i = ais.length, j, tmp
while (--i > 0) {
j = Math.floor(rand() * (i + 1))
tmp = players[j]
players[j] = players[i]
players[i] = tmp
tmp = ais[j]
ais[j] = ais[i]
ais[i] = tmp
}
return players.map((name, i) => {
return ais.map((name, i) => {
const jsonName = `"name":${JSON.stringify(name)}`
const hue = max1(i * rate + 0.25)
const p = {
const ai = {
hue,
name,
x: Math.round(max2PI(Math.cos(angle * i + shift)) * m + h),
y: Math.round(max2PI(Math.sin(angle * i + shift)) * m + h),
cardinal: 0,
direction: 0,
color: hslToRgb(hue, 1, 0.5),
toString: () => `{${jsonName},"dead":${!!p.dead},"cardinal":${p.cardinal},"direction":${p.direction},"color":${p.color},"x":${p.x},"y":${p.y},"coords":[{"x":${p.x},"y":${p.y - 1},"cardinal":0,"direction":${(4 - p.cardinal) % 4}},{"x":${p.x + 1},"y":${p.y},"cardinal":1,"direction":${(5 - p.cardinal) % 4}},{"x":${p.x},"y":${p.y + 1},"cardinal":2,"direction":${(6 - p.cardinal) % 4}},{"x":${p.x - 1},"y":${p.y},"cardinal":3,"direction":${(7 - p.cardinal) % 4}}]}`,
toString: () => `{${jsonName},"dead":${!!ai.dead},"color":${ai.color},"x":${ai.x},"y":${ai.y}}`,
}
return p
return ai
})
}
export const injectedCode = `
if (typeof update !== 'function') throw Error('Update function not defined')
addEventListener('message', self.init = initEvent => {
const { seed, id } = JSON.parse(initEvent.data)
const isOwnPlayer = p => p.name === id
let { seed, id } = JSON.parse(initEvent.data)
const r4 = () => Math.floor(Math.random() * 4)
let w = (123456789 + seed) & 0xffffffff
let z = (987654321 - seed) & 0xffffffff
Math.random = () => {
let t = seed += 0x6D2B79F5
t = Math.imul(t ^ t >>> 15, t | 1)
t ^= t + Math.imul(t ^ t >>> 7, t | 61)
return ((t ^ t >>> 14) >>> 0) / 4294967296
z = (36969 * (z & 65535) + (z >>> 16)) & 0xffffffff
w = (18000 * (w & 65535) + (w >>> 16)) & 0xffffffff
return (((z << 16) + (w & 65535)) >>> 0) / 4294967296
}
const prev = {}
let me
removeEventListener('message', self.init)
addEventListener('message', ({ data }) => {
const players = JSON.parse(data)
const player = players.find(isOwnPlayer)
player.isOwnPlayer = true
const ais = JSON.parse(data)
me || (ais
.sort((a, b) => a.name - b.name)
.forEach(a => prev[a.name] = [{...a, cardinal: r4(), direction: r4() }]))
for (const ai of ais) {
const { x, y, name, dead } = ai
if (dead) {
ai.coords = []
continue
}
name === id && (me = ai)
const { cardinal, direction } = prev[name].find(c => c.x === ai.x && c.y === ai.y)
ai.index = ai.x * 100 + ai.y
ai.direction = direction
ai.cardinal = cardinal
ai.coords = prev[name] = [
{ index: x*100+(y-1), x, y: y - 1, cardinal: 0, direction: (4 - cardinal) % 4 },
{ index: (x+1)*100+y, x: x + 1, y, cardinal: 1, direction: (5 - cardinal) % 4 },
{ index: x*100+(y+1), x, y: y + 1, cardinal: 2, direction: (6 - cardinal) % 4 },
{ index: (x-1)*100+y, x: x - 1, y, cardinal: 3, direction: (7 - cardinal) % 4 },
]
}
me.me = true
try { postMessage(JSON.stringify(update({ players, player }))) }
try { postMessage(JSON.stringify(update({ ais, ai: me }))) }
catch (err) {
console.error(err)
console.error(id, err)
throw err
}
})

4
subjects/tron/tron.en.md

@ -41,7 +41,9 @@ You will need to create a public repository with the name `tron`. Next you need
- You may use this link [tron](/public/subjects/tron?ai=&seed=1653547275), to test your AI
- You need to add your AI as a user in that link
> Example: if your git login is **Frenchris** and you want to test against **LEEDASILVA** the link becomes: `/public/subjects/tron?AI=Frenchris@master+LEEDASILVA@master&seed=1653547275`
> Example:
- if your git login is **Frenchris** and you want to test against **LEEDASILVA** the link becomes: `/public/subjects/tron?AI=Frenchris@master+LEEDASILVA@master`
- if you want to test against the default `/random.js` AI the link becomes: `/public/subjects/tron?AI=Frenchris@master+/random.js`
- Open the inspector of the browser used and **disable the cache**
- let's change the update function so that your AI only goes forward.

Loading…
Cancel
Save