forked from root/public
11 changed files with 1074 additions and 73 deletions
@ -1,27 +0,0 @@ |
|||||||
## Abs |
|
||||||
|
|
||||||
### Instructions |
|
||||||
|
|
||||||
Create a `isPositive` function that takes a number as |
|
||||||
parameter and return true if the given number is |
|
||||||
stricly positive, or false otherwise |
|
||||||
|
|
||||||
Create the `abs` function that takes one number argument |
|
||||||
and returns its absolute value. |
|
||||||
You are not allowed to use `Math.abs`, make your own. |
|
||||||
|
|
||||||
|
|
||||||
### Notions |
|
||||||
|
|
||||||
- [nan-academy.github.io/js-training/examples/functions.js](https://nan-academy.github.io/js-training/examples/functions.js) |
|
||||||
- [nan-academy.github.io/js-training/examples/ternary.js](https://nan-academy.github.io/js-training/examples/ternary.js) |
|
||||||
- [devdocs.io/javascript/global_objects/math/abs](https://devdocs.io/javascript/global_objects/math/abs) |
|
||||||
|
|
||||||
|
|
||||||
### Code provided |
|
||||||
|
|
||||||
> all code provided will be added to your solution and doesn't need to be submited. |
|
||||||
|
|
||||||
```js |
|
||||||
Math.abs = undefined |
|
||||||
``` |
|
@ -0,0 +1,171 @@ |
|||||||
|
/******************************* |
||||||
|
* functions given to the students |
||||||
|
********************************/ |
||||||
|
const SIZE = 100 |
||||||
|
const MAP = new Int8Array(SIZE * SIZE) |
||||||
|
const isFree = ({ x, y }) => MAP[y * SIZE + x] === 0 |
||||||
|
const isOccupied = ({ x, y }) => MAP[y * SIZE + x] === 1 |
||||||
|
const inBounds = (n) => n < SIZE && n >= 0 |
||||||
|
const isInBounds = ({ x, y }) => inBounds(x) && inBounds(y) |
||||||
|
const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)] |
||||||
|
|
||||||
|
/*********** |
||||||
|
* My functions |
||||||
|
************/ |
||||||
|
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) => { |
||||||
|
let arr = [] |
||||||
|
let car = state.player.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 }) |
||||||
|
) { |
||||||
|
let xad = state.player.x - 1 |
||||||
|
let yad = state.player.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(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] |
||||||
|
} |
||||||
|
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 }) |
||||||
|
) { |
||||||
|
let xad = state.player.x + 1 |
||||||
|
let yad = state.player.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(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] |
||||||
|
} |
||||||
|
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 }) |
||||||
|
) { |
||||||
|
let xad = state.player.x + 1 |
||||||
|
let yad = state.player.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(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] |
||||||
|
} |
||||||
|
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 }) |
||||||
|
) { |
||||||
|
let xad = state.player.x - 1 |
||||||
|
let yad = state.player.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(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] |
||||||
|
} |
||||||
|
|
||||||
|
for ({ x, y, cardinal } of state.player.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))] |
||||||
|
} |
||||||
|
|
||||||
|
// recursion
|
||||||
|
const calDistance = (x, y, car, count) => { |
||||||
|
if (car <= 0) { |
||||||
|
if ( |
||||||
|
isFree({ x, y }) && |
||||||
|
isAlley({ x: x + 1, y }) && |
||||||
|
isAlley({ x, y: y - 1 }) && |
||||||
|
isAlley({ x: x - 1, y }) |
||||||
|
) |
||||||
|
return -1 |
||||||
|
return !isFree({ x, y }) || !inBounds(y - 1) |
||||||
|
? count |
||||||
|
: calDistance(x, y - 1, car, count + 1) |
||||||
|
} |
||||||
|
if (car === 1) { |
||||||
|
if ( |
||||||
|
isFree({ x, y }) && |
||||||
|
isAlley({ x, y: y + 1 }) && |
||||||
|
isAlley({ x, y: y - 1 }) && |
||||||
|
isAlley({ x: x + 1, y }) |
||||||
|
) |
||||||
|
return -1 |
||||||
|
return !isFree({ x, y }) || !inBounds(x + 1) |
||||||
|
? count |
||||||
|
: calDistance(x + 1, y, car, count + 1) |
||||||
|
} |
||||||
|
if (car === 2) { |
||||||
|
if ( |
||||||
|
isFree({ x, y }) && |
||||||
|
isAlley({ x: x - 1, y }) && |
||||||
|
isAlley({ x, y: y + 1 }) && |
||||||
|
isAlley({ x: x + 1, y }) |
||||||
|
) |
||||||
|
return -1 |
||||||
|
return !isFree({ x, y }) || !inBounds(y + 1) |
||||||
|
? count |
||||||
|
: calDistance(x, y + 1, car, count + 1) |
||||||
|
} |
||||||
|
if (car === 3) { |
||||||
|
if ( |
||||||
|
isFree({ x, y }) && |
||||||
|
isAlley({ x, y: y - 1 }) && |
||||||
|
isAlley({ x: x - 1, y }) && |
||||||
|
isAlley({ x, y: y + 1 }) |
||||||
|
) |
||||||
|
return -1 |
||||||
|
return !isFree({ x, y }) || !inBounds(x - 1) |
||||||
|
? count |
||||||
|
: calDistance(x - 1, y, car, count + 1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const addToMap = ({ x, y }) => MAP[y * SIZE + x] = 1 |
||||||
|
const update = (state) => { |
||||||
|
state.players.forEach(addToMap) |
||||||
|
findBestPath(state) |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
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 inBounds = n => n < SIZE && n >= 0 |
||||||
|
const isInBounds = ({ x, y }) => inBounds(x) && inBounds(y) |
||||||
|
const pickRandom = arr => arr[Math.floor(Math.random() * arr.length)] |
||||||
|
|
||||||
|
// ok check if we can move on this block
|
||||||
|
const ok = (x = -1, y = -1) => { |
||||||
|
const coords = typeof x !== 'number' ? x : { x, y } |
||||||
|
return isFree(coords) && isInBounds(coords) |
||||||
|
} |
||||||
|
|
||||||
|
const isAlley = (card, x, y) => { |
||||||
|
switch (card) { |
||||||
|
case 0: |
||||||
|
if (ok(x, y - 1) && !ok(x + 1, y - 1) && !ok(x - 1, y - 1)) { |
||||||
|
while (ok(x, y - 1) && !hasLateralWalls(0, x, y)) { |
||||||
|
y-- |
||||||
|
if (hasLateralWalls(0, x, y)) return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
case 1: |
||||||
|
if (ok(x + 1, y) && !ok(x + 1, y + 1) && !ok(x + 1, y - 1)) { |
||||||
|
while (ok(x + 1, y) && !hasLateralWalls(1, x, y)) { |
||||||
|
x++ |
||||||
|
if (hasLateralWalls(1, x, y)) return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
case 2: |
||||||
|
if (ok(x, y + 1) && !ok(x + 1, y + 1) && !ok(x - 1, y + 1)) { |
||||||
|
while (ok(x, y + 1) && !hasLateralWalls(2, x, y)) { |
||||||
|
y++ |
||||||
|
if (hasLateralWalls(2, x, y)) return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
case 3: |
||||||
|
if (ok(x - 1, y) && !ok(x - 1, y + 1) && !ok(x - 1, y - 1)) { |
||||||
|
while (ok(x - 1, y) && !hasLateralWalls(3, x, y)) { |
||||||
|
x-- |
||||||
|
if (hasLateralWalls(3, x, y)) return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const hasLateralWalls = (card, x, y) => { |
||||||
|
switch (card) { |
||||||
|
case 0: return !(ok(x + 1, y) || ok(x - 1, y) || ok(x, y - 1)) |
||||||
|
case 1: return !(ok(x, y - 1) || ok(x, y + 1) || ok(x + 1, y)) |
||||||
|
case 2: return !(ok(x + 1, y) || ok(x - 1, y) || ok(x, y + 1)) |
||||||
|
case 3: return !(ok(x, y - 1) || ok(x, y + 1) || ok(x - 1, y)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const goDirection = (state, card) => |
||||||
|
ok(state.coords[card]) && |
||||||
|
!isAlley(card, state.x, state.y) && |
||||||
|
state.coords[card] |
||||||
|
|
||||||
|
const findEnemy = state => |
||||||
|
state.players.filter(p => state.player.name !== p.name)[0] |
||||||
|
|
||||||
|
const seekEnemy = state => { |
||||||
|
if (state.players.length === 1) return |
||||||
|
const enemy = findEnemy(state) |
||||||
|
const xPla = state.player.x |
||||||
|
const yPla = state.player.y |
||||||
|
const xOpo = enemy.x |
||||||
|
const yOpo = enemy.y |
||||||
|
const xDif = xPla - xOpo |
||||||
|
const yDif = yPla - yOpo |
||||||
|
|
||||||
|
return ( |
||||||
|
(Math.abs(xDif) > Math.abs(yDif) && |
||||||
|
goDirection(state.player, xPla < xOpo ? 1 : 3)) || |
||||||
|
goDirection(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) |
||||||
|
|
||||||
|
const addToMap = ({ x, y }) => (MAP[y * SIZE + x] = 1) |
||||||
|
const update = state => { |
||||||
|
state.players.forEach(addToMap) |
||||||
|
return walk(state) |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
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
|
||||||
|
|
||||||
|
// `inBounds` check if our coord (n) is an existing index in our MAP
|
||||||
|
const inBounds = n => n < SIZE && n >= 0 |
||||||
|
|
||||||
|
// `isInBounds` check that properties x and y of our argument are both in bounds
|
||||||
|
const isInBounds = ({ x, y }) => inBounds(x) && inBounds(y) |
||||||
|
|
||||||
|
// `pickRandom` get a random element from an array
|
||||||
|
const pickRandom = arr => arr[Math.floor(Math.random() * arr.length)] |
||||||
|
|
||||||
|
// `addToMap` save the new positions into the map
|
||||||
|
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
|
||||||
|
// 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
|
||||||
|
// cardinal: A number between 0 and 3 that represent the cardinal
|
||||||
|
// [ 0: NORTH, 1: EAST, 2: SOUTH, 3: WEST ]
|
||||||
|
// 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) |
||||||
|
|
||||||
|
// Actual AI logic:
|
||||||
|
// I filter my array of coords to keep only those that are in bounds
|
||||||
|
const coordsInBound = state.player.coords.filter(isInBounds) |
||||||
|
|
||||||
|
// I filter again to keep coords that are free
|
||||||
|
const available = coordsInBound.filter(isFree) |
||||||
|
|
||||||
|
// And I return a random available coord
|
||||||
|
return pickRandom(available) |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
const SIZE = 100 |
||||||
|
const [FREE, UNSAFE, FILLED] = Array(3).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 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 goForward = arr => arr.find(isForward) |
||||||
|
const goRight = arr => arr.find(isRight) |
||||||
|
const goLeft = arr => arr.find(isLeft) |
||||||
|
const isOtherPlayer = player => !player.isOwnPlayer |
||||||
|
|
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
return goLeft(arr) || goForward(arr) || goRight(arr) |
||||||
|
} |
||||||
|
|
||||||
|
const update = ({ player, players }) => { |
||||||
|
players.forEach(setFilled) |
||||||
|
players |
||||||
|
.filter(isOtherPlayer) |
||||||
|
.flatMap(({ coords }) => coords) |
||||||
|
.filter(isInBounds) |
||||||
|
.filter(isFree) |
||||||
|
.forEach(setUnsafe) |
||||||
|
|
||||||
|
const coordsInBound = player.coords |
||||||
|
.filter(isInBounds) |
||||||
|
.filter(isNotBackward) |
||||||
|
|
||||||
|
const available = pickForwardOrRightOrLeft(coordsInBound.filter(isFree)) |
||||||
|
const lastResort = pickForwardOrRightOrLeft(coordsInBound.filter(isUnsafe)) |
||||||
|
return available || lastResort |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
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 setFilled = ({ x, y }) => MAP[y * SIZE + x] = FILLED |
||||||
|
const inBounds = n => n < SIZE && n >= 0 |
||||||
|
const isInBounds = ({ x, y }) => inBounds(x) && inBounds(y) |
||||||
|
const isNotBackward = el => el.direction !== 2 |
||||||
|
const isForward = el => el.direction === 0 |
||||||
|
const isRight = el => el.direction === 1 |
||||||
|
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 |
||||||
|
|
||||||
|
// `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) |
||||||
|
.filter(isInBounds) |
||||||
|
.filter(isFree) |
||||||
|
.forEach(setUnsafe) |
||||||
|
|
||||||
|
const coordsInBound = player.coords |
||||||
|
.filter(isInBounds) |
||||||
|
.filter(isNotBackward) |
||||||
|
|
||||||
|
const available = snailIt(coordsInBound.filter(isFree)) |
||||||
|
const lastResort = snailIt(coordsInBound.filter(isUnsafe)) |
||||||
|
return available || lastResort |
||||||
|
} |
@ -0,0 +1,384 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>Tron</title> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAx42QAFP/FABXQkMAQC0uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAQBEREREREREAFEREREREQQAURERERERBABREREREREEAFEREREREQQAURERERERBABQERAAEREEAFCBEIiREQQAUQgRERERBABRAJEREREEAFAJEREREQQAUJERERERBABRERERERDEAEREREREREQQAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"> |
||||||
|
<style> |
||||||
|
body { |
||||||
|
background: #13191a; |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 1fr 300px 300px 1fr; |
||||||
|
font-family: monospace; |
||||||
|
color: #7cecff; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { letter-spacing: 6px } |
||||||
|
h2 { |
||||||
|
margin-bottom: 6px; |
||||||
|
text-overflow: ellipsis; |
||||||
|
max-width: calc(50vw - 300px); |
||||||
|
} |
||||||
|
|
||||||
|
h2:after { |
||||||
|
display: block; |
||||||
|
content: ' '; |
||||||
|
margin-top: 12px; |
||||||
|
height: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
h2 b { |
||||||
|
display: block; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#player-0 h2:after { |
||||||
|
background-image: linear-gradient(to left, #0000, currentColor 18px, #0000); |
||||||
|
margin-right: -10px; |
||||||
|
} |
||||||
|
|
||||||
|
#player-1 h2:after { |
||||||
|
margin-left: -10px; |
||||||
|
background-image: linear-gradient(to right, #0000, currentColor 18px, #0000); |
||||||
|
} |
||||||
|
|
||||||
|
canvas { |
||||||
|
display: block; |
||||||
|
width: 600px; |
||||||
|
height: 600px; |
||||||
|
grid-column: 2/4; |
||||||
|
box-shadow: 0 0 20px 1px #7cecff42; |
||||||
|
outline: 1px solid; |
||||||
|
outline-offset: -1px; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border: 1px solid; |
||||||
|
color: currentColor; |
||||||
|
background: transparent; |
||||||
|
padding: 0 2px; |
||||||
|
border-radius: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
svg { |
||||||
|
margin-top: 36px; |
||||||
|
width: 300px; |
||||||
|
} |
||||||
|
|
||||||
|
#notice { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
padding: 2px 0; |
||||||
|
user-select: none; |
||||||
|
} |
||||||
|
|
||||||
|
.hide { |
||||||
|
visibility: hidden; |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
|
||||||
|
#title { |
||||||
|
display: block; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
grid-column: 2/4; |
||||||
|
text-align: center; |
||||||
|
color: cyan; |
||||||
|
} |
||||||
|
|
||||||
|
#player-0, #player-1 { |
||||||
|
margin: 12px; |
||||||
|
padding: 6px; |
||||||
|
user-select: none; |
||||||
|
} |
||||||
|
|
||||||
|
#player-0 { |
||||||
|
grid-column: 1; |
||||||
|
text-align: right; |
||||||
|
} |
||||||
|
|
||||||
|
#player-1 { grid-column: 4 } |
||||||
|
|
||||||
|
|
||||||
|
#controls { |
||||||
|
grid-column: 2/4; |
||||||
|
height: 10px; |
||||||
|
background: #7cecff20; |
||||||
|
margin-top: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
#bar { |
||||||
|
overflow: hidden; |
||||||
|
position: relative; |
||||||
|
box-shadow: 0 0 20px 1px #7cecff30; |
||||||
|
outline: 1px solid; |
||||||
|
outline-offset: -1px; |
||||||
|
} |
||||||
|
|
||||||
|
#loading { background: #7cecff40 } |
||||||
|
#position { background: #7cecff } |
||||||
|
#loading, #position { |
||||||
|
position: absolute; |
||||||
|
transform: translate(-600px); |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
|
||||||
|
#bar, #loading, #position { |
||||||
|
height: 100%; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
@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 } |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="title"> |
||||||
|
<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"> |
||||||
|
<h2><b>AI-0</b></h2> |
||||||
|
<b>LOADING</b> |
||||||
|
<br> |
||||||
|
<b>0</b> |
||||||
|
</div> |
||||||
|
<canvas width="1200" height="1200"></canvas> |
||||||
|
<div id="player-1"> |
||||||
|
<h2><b>AI-1</b></h2> |
||||||
|
<b>LOADING</b> |
||||||
|
<br> |
||||||
|
<b>0</b> |
||||||
|
</div> |
||||||
|
<div id="controls"> |
||||||
|
<div id="bar"> |
||||||
|
<div id="loading"></div> |
||||||
|
<div id="position"></div> |
||||||
|
</div> |
||||||
|
<div id="notice" class="hide"> |
||||||
|
> scroll or keys to move step by step, click to jump. (shift = fast) |
||||||
|
<button>hide</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<script type="module"> |
||||||
|
import { move, update, colorize } from './lib/display.js' |
||||||
|
import { init, injectedCode } from './lib/state.js' |
||||||
|
|
||||||
|
const [canvas] = document.getElementsByTagName('canvas') |
||||||
|
const [hide] = document.getElementsByTagName('button') |
||||||
|
const bar = document.getElementById('bar') |
||||||
|
const loading = document.getElementById('loading') |
||||||
|
const position = document.getElementById('position') |
||||||
|
|
||||||
|
hide.onclick = () => { |
||||||
|
hide.parentElement.classList.add('hide') |
||||||
|
localStorage.hide = 1 |
||||||
|
} |
||||||
|
|
||||||
|
localStorage.hide || hide.parentElement.classList.remove('hide') |
||||||
|
|
||||||
|
const buildInfo = (player, i) => { |
||||||
|
const elem = document.getElementById(`player-${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)` |
||||||
|
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), |
||||||
|
status: text => status.data = text, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const notInBounds = n => n >= 100 || n < 0 |
||||||
|
|
||||||
|
const getShaUrl = login => |
||||||
|
`https://api.github.com/repos/${login}/tron/commits/master` |
||||||
|
|
||||||
|
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 toUrlObject = b => URL.createObjectURL(b, { type: 'text/javascript' }) |
||||||
|
const memo = {} |
||||||
|
const fetchBlob = url => memo[url] |
||||||
|
|| (memo[url] = fetch(url).then(toBlob).then(toUrlObject)) |
||||||
|
|
||||||
|
const getGithackUrl = async (login, sha) => { |
||||||
|
if (!sha || sha === 'master') { |
||||||
|
sha = localStorage[login] || (localStorage[login] = await getSha(login)) |
||||||
|
} |
||||||
|
return getAIUrl(login, sha) |
||||||
|
} |
||||||
|
|
||||||
|
const remoteURL = (login, sha) => location.host.startsWith('git.') |
||||||
|
? `${location.origin}/${login}/tron/raw/branch/${sha || 'master'}/ai.js` |
||||||
|
: getGithackUrl(login, sha) |
||||||
|
|
||||||
|
const formatURL = url => { |
||||||
|
if (url.startsWith('/')) return `${location.pathname}/ai/${url}` |
||||||
|
if (url.startsWith('https://')) return url |
||||||
|
if (url.includes(location.hostname)) return `https://${url}` |
||||||
|
return url.includes('@') |
||||||
|
? remoteURL(...url.split('@')) |
||||||
|
: `${location.origin}/${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 }) |
||||||
|
|
||||||
|
let turn = 1, maxTurn = 1, t = 1, cap = 10000, down |
||||||
|
const done = new Set(players) |
||||||
|
|
||||||
|
const setPosition = e => { |
||||||
|
const v = typeof e === 'number' |
||||||
|
? Math.max(e, 1) |
||||||
|
: Math.floor(Math.max(e.pageX - bar.offsetLeft, 1) / 600 * cap) |
||||||
|
|
||||||
|
t = Math.min(v, maxTurn) |
||||||
|
requested || (requested = requestAnimationFrame(refresh)) |
||||||
|
} |
||||||
|
|
||||||
|
window.onkeydown = e => { |
||||||
|
const step = e.shiftKey ? 10 : 1 |
||||||
|
switch (e.key) { |
||||||
|
case 'ArrowLeft': case 'a': case 'q': case 'l': return setPosition(t - step) |
||||||
|
case 'ArrowRight': case 'e': case 'd': case 'k': return setPosition(t + step) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
window.onmousemove = (e) => e.which ? (down && setPosition(e)) : (down = false) |
||||||
|
bar.onmousedown = (e) => (down = true) && setPosition(e) |
||||||
|
bar.onwheel = canvas.onwheel = (e) => |
||||||
|
setPosition(t + Math.sign(e.deltaY) * (e.shiftKey ? 10 : 1)) |
||||||
|
|
||||||
|
let requested, timeout |
||||||
|
const refresh = () => { |
||||||
|
requested = update(t) |
||||||
|
players[0].score(turn) |
||||||
|
players[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) |
||||||
|
requested || (requested = requestAnimationFrame(refresh)) |
||||||
|
|
||||||
|
// check if all AI are done |
||||||
|
if (done.size >= players.length) { |
||||||
|
turn++ |
||||||
|
const data = `[${players.join(',')}]` |
||||||
|
let allDead = true |
||||||
|
for (const p of players) { |
||||||
|
if (p.dead) continue |
||||||
|
allDead ? (allDead = false) : cap-- |
||||||
|
done.delete(p) |
||||||
|
p.worker.postMessage(data) |
||||||
|
p.timeout = setTimeout(p.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 |
||||||
|
|
||||||
|
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() |
||||||
|
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 }) |
||||||
|
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 })) |
||||||
|
}) |
||||||
|
|
||||||
|
// activate the AI |
||||||
|
info.status('ACTIVE') |
||||||
|
player.kill = cause => { |
||||||
|
kill(cause) |
||||||
|
next(player) |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
console.error(err) |
||||||
|
kill('FAILED-TO-LOAD') |
||||||
|
} |
||||||
|
|
||||||
|
move(player.x, player.y, player.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') |
||||||
|
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 ( |
||||||
|
!(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) |
||||||
|
if (failure === turn) { |
||||||
|
for (const p of players) { |
||||||
|
p.x === player.x && p.y === player.y && p.kill('MULTI-CRASH') |
||||||
|
} |
||||||
|
colorize(x, y, 0xffffff) |
||||||
|
return player.kill('MULTI-CRASH') |
||||||
|
} else if (failure) return player.kill('CRASH') |
||||||
|
next(player) |
||||||
|
} |
||||||
|
|
||||||
|
player.worker.onerror = () => player.kill('AI-ERROR') |
||||||
|
})) |
||||||
|
|
||||||
|
next(players[0]) |
||||||
|
} |
||||||
|
|
||||||
|
const params = new URLSearchParams(location.search) |
||||||
|
const { ai = '', seed } = Object.fromEntries(params) |
||||||
|
const search = () => String(params).replace(/%2F/g, '/').replace(/%40/g, '@') |
||||||
|
|
||||||
|
if (!seed) { |
||||||
|
params.set('seed', Math.abs(~~(Math.random()*0xffffffff))) |
||||||
|
location = `${location.origin}${location.pathname}?${search()}${location.hash}` |
||||||
|
} |
||||||
|
|
||||||
|
start({ urls: ai.split(' '), seed }).then(console.log, console.error) |
||||||
|
|
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,132 @@ |
|||||||
|
const vertexArray = new Float32Array(100 * 100 * 12) |
||||||
|
const colorArray = new Float32Array(100 * 100 * 6) |
||||||
|
const state = new Float32Array(100 * 100 * 2) |
||||||
|
const [canvas] = document.getElementsByTagName('canvas') |
||||||
|
const gl = canvas.getContext('webgl2', { antialias: false }) |
||||||
|
const S = 0.02 |
||||||
|
|
||||||
|
const applyState = (x, y, turn) => { |
||||||
|
const index = x * 100 + y |
||||||
|
const color = state[index * 2 + 0] > turn ? 0 : state[index * 2 + 1] |
||||||
|
colorArray[index * 6 + 0] = color |
||||||
|
colorArray[index * 6 + 1] = color |
||||||
|
colorArray[index * 6 + 2] = color |
||||||
|
colorArray[index * 6 + 3] = color |
||||||
|
colorArray[index * 6 + 4] = color |
||||||
|
colorArray[index * 6 + 5] = color |
||||||
|
} |
||||||
|
|
||||||
|
export const colorize = (x, y, color) => state[(x * 100 + y) * 2 + 1] = color |
||||||
|
export const move = (x, y, color, turn) => { |
||||||
|
const index = (x * 100 + y) * 2 |
||||||
|
if (state[index]) return state[index] |
||||||
|
state[index] = turn |
||||||
|
state[index + 1] = color |
||||||
|
} |
||||||
|
|
||||||
|
const loop = fn => { |
||||||
|
let x = -1, y = -1 |
||||||
|
while (++x < 100) { |
||||||
|
y = -1 |
||||||
|
while (++y < 100) fn(x, y) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const compileShader = (type, script) => { |
||||||
|
const shader = gl.createShader(type) |
||||||
|
gl.shaderSource(shader, script.trim()) |
||||||
|
gl.compileShader(shader) |
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { |
||||||
|
throw gl.getShaderInfoLog(shader) |
||||||
|
} |
||||||
|
return shader |
||||||
|
} |
||||||
|
|
||||||
|
// program
|
||||||
|
const program = gl.createProgram() |
||||||
|
gl.attachShader(program, compileShader(gl.VERTEX_SHADER, ` |
||||||
|
#version 300 es |
||||||
|
|
||||||
|
in vec2 a_position; |
||||||
|
in float a_color; |
||||||
|
out float v_color; |
||||||
|
|
||||||
|
void main() { |
||||||
|
gl_Position = vec4(a_position * vec2(1, -1), 0, 1); |
||||||
|
v_color = a_color; |
||||||
|
}`))
|
||||||
|
|
||||||
|
gl.attachShader(program, compileShader(gl.FRAGMENT_SHADER, ` |
||||||
|
#version 300 es |
||||||
|
|
||||||
|
precision mediump float; |
||||||
|
in float v_color; |
||||||
|
out vec4 outColor; |
||||||
|
|
||||||
|
vec4 unpackColor(float f) { |
||||||
|
vec4 color; |
||||||
|
color.r = floor(f / 65536.0); |
||||||
|
color.g = floor((f - color.r * 65536.0) / 256.0); |
||||||
|
color.b = floor(f - color.r * 65536.0 - color.g * 256.0); |
||||||
|
color.a = 256.0; |
||||||
|
return color / 256.0; |
||||||
|
} |
||||||
|
|
||||||
|
void main() { |
||||||
|
outColor = unpackColor(v_color); |
||||||
|
}`))
|
||||||
|
|
||||||
|
gl.linkProgram(program) |
||||||
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { |
||||||
|
throw gl.getProgramInfoLog(program) |
||||||
|
} |
||||||
|
|
||||||
|
gl.useProgram(program) |
||||||
|
|
||||||
|
// initialize state
|
||||||
|
loop((x, y) => { |
||||||
|
const x1 = ((x + 1) - 50) / 50 - S |
||||||
|
const y1 = ((y + 1) - 50) / 50 - S |
||||||
|
const x2 = x1 + S |
||||||
|
const y2 = y1 + S |
||||||
|
const index = (x * 100 + y) * 12 |
||||||
|
vertexArray[index + 0x0] = x1 |
||||||
|
vertexArray[index + 0x1] = y1 |
||||||
|
vertexArray[index + 0x2] = x2 |
||||||
|
vertexArray[index + 0x3] = y1 |
||||||
|
vertexArray[index + 0x4] = x1 |
||||||
|
vertexArray[index + 0x5] = y2 |
||||||
|
vertexArray[index + 0x6] = x1 |
||||||
|
vertexArray[index + 0x7] = y2 |
||||||
|
vertexArray[index + 0x8] = x2 |
||||||
|
vertexArray[index + 0x9] = y1 |
||||||
|
vertexArray[index + 0xa] = x2 |
||||||
|
vertexArray[index + 0xb] = y2 |
||||||
|
}) |
||||||
|
|
||||||
|
const vertexBuffer = gl.createBuffer() |
||||||
|
const a_position = gl.getAttribLocation(program, 'a_position') |
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) |
||||||
|
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0) |
||||||
|
gl.enableVertexAttribArray(a_position) |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW) |
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 60000) |
||||||
|
|
||||||
|
// color buffer
|
||||||
|
const colorBuffer = gl.createBuffer() |
||||||
|
const a_color = gl.getAttribLocation(program, 'a_color') |
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer) |
||||||
|
gl.vertexAttribPointer(a_color, 1, gl.FLOAT, false, 0, 0) |
||||||
|
gl.enableVertexAttribArray(a_color) |
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer) |
||||||
|
|
||||||
|
export const update = (turn) => { |
||||||
|
loop((x, y) => applyState(x, y, turn)) |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW) |
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 60000) |
||||||
|
} |
||||||
|
|
||||||
|
export const reset = () => { |
||||||
|
state.fill(0) |
||||||
|
update(0) |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
const SIZE = 100 |
||||||
|
const h = SIZE / 2 |
||||||
|
const m = h * 0.8 |
||||||
|
const max = m => n => n > m ? max1(n - m) : n |
||||||
|
const max1 = max(1) |
||||||
|
const max2PI = max(Math.PI * 2) |
||||||
|
const toInt = (r, g, b) => (r << 16) | (g << 8) | b |
||||||
|
const toRange = n => Math.round(n * 0xFF) |
||||||
|
const hue2rgb = (p, q, t) => { |
||||||
|
if (t < 0) t += 1 |
||||||
|
if (t > 1) t -= 1 |
||||||
|
if (t < 1/6) return p + (q - p) * 6 * t |
||||||
|
if (t < 1/2) return q |
||||||
|
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6 |
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
const hslToRgb = (h, s, l) => { |
||||||
|
if (!s) return toInt(toRange(l), toRange(l), toRange(l)) |
||||||
|
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s |
||||||
|
const p = 2 * l - q |
||||||
|
const r = hue2rgb(p, q, h + 1/3) |
||||||
|
const g = hue2rgb(p, q, h) |
||||||
|
const b = hue2rgb(p, q, h - 1/3) |
||||||
|
|
||||||
|
return toInt(toRange(r), toRange(g), toRange(b)) |
||||||
|
} |
||||||
|
|
||||||
|
export const init = ({ players, seed }) => { |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
const angle = (Math.PI * 2) / players.length |
||||||
|
const rate = (SIZE / players.length / SIZE) |
||||||
|
const shift = angle * rand() |
||||||
|
|
||||||
|
// shuffle using seeded random
|
||||||
|
players.sort((a, b) => a.name - b.name) |
||||||
|
let i = players.length, j, tmp |
||||||
|
while (--i > 0) { |
||||||
|
j = Math.floor(rand() * (i + 1)) |
||||||
|
tmp = players[j] |
||||||
|
players[j] = players[i] |
||||||
|
players[i] = tmp |
||||||
|
} |
||||||
|
|
||||||
|
return players.map((name, i) => { |
||||||
|
const jsonName = `"name":${JSON.stringify(name)}` |
||||||
|
const hue = max1(i * rate + 0.25) |
||||||
|
const p = { |
||||||
|
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}}]}`, |
||||||
|
} |
||||||
|
return p |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
removeEventListener('message', self.init) |
||||||
|
addEventListener('message', ({ data }) => { |
||||||
|
const players = JSON.parse(data) |
||||||
|
const player = players.find(isOwnPlayer) |
||||||
|
player.isOwnPlayer = true |
||||||
|
|
||||||
|
try { postMessage(JSON.stringify(update({ players, player }))) } |
||||||
|
catch (err) { |
||||||
|
console.error(err) |
||||||
|
throw err |
||||||
|
} |
||||||
|
}) |
||||||
|
postMessage('loaded') // Signal that the loading is over
|
||||||
|
}) |
||||||
|
` |
Loading…
Reference in new issue