JS Битва

как я написал свой eval()

JS Битва
как я написал свой eval()

Коротаев Александр

@mamu_eval
github.com/lekzd

  • Фронтенд разработчик в Tinkoff.ru
  • Spb-frontend
  • подкаст Drinkcast

Выберите юнита

Напишите код

Создайте алгоритм

Технологии

Игры
для программистов

Мотивация

  1. Все хотят сражаться
  2. Крутой стенд
  3. Геймификация

Геймификация

Как это выглядит

История
о нехватке времени

Немного цифр

  1. 5 минут на идею
  2. 4 часа в день
  3. всего 3 недели

Первая реализация

  1. Phaser
  2. Ace editor
  3. Node.js

Основные модули

  1. Renderer
  2. JS Sandbox
  3. State sharing
  4. Server – source of truth
Песочница
для JS кода

Итак, песочница

  1. Изоляция
  2. API юнитов
  3. Асинхронный код

Асинхронность:☹️

            goTo(5, 0)
              .then(() => attack())
              .then(() => goTo(5, 0))
              .then(() => attack())
        

Асинхронность:😀

            await goTo(5, 0)
            await attack()
            await goTo(5, 0)
            await attack()
        

Асинхронность:❤️️

            goTo(5, 0)
            attack()
            goTo(5, 0)
            attack()
        

Интерпретация

eval() + with?

  1. исполняет код из строки
  2. в текущем контексте

Harmful code

  1. while (true) {}
  2. for (i++) {}
  3. alert()
  4. prompt()
eval()
* is evil
eval()
Workers + with + Proxy

Workers!

Hello Worker

            const worker = new Worker('script.js')
            setTimeout(() => worker.close(), 1000)
        

script.js

            onmessage = (message) => {
              postMessage(message)
            }
        

Код из строки

            const customCode = 'attack()'
            const blob = new Blob([customCode], {...})
            const codeUrl = URL.createObjectURL(blob)
             
            const worker = new Worker(codeUrl)
            setTimeout(() => worker.terminate(), 1000)
        

Добавим RxJS

                const message$ = fromEvent(worker, 'message')
                  .pipe(map(e => JSON.parse(e.data)))
                 
                const timeoutClose$ = timer(1000).pipe(
                  takeUntil(message$),
                  switchMap(() => throwError(`to much time!`))
                );
                  
                merge(timeoutClose$, message$)
                  .pipe(
                      map(e => e.data),
                      finalize(() => worker.terminate()),
                      catchError(error => {
                          reject(error)
                          return []
                      })
                  )
                  .subscribe(success => {
                      resolve(success)
                  })
            

Unit Api

            goTo(5, 0)
             
            for(i = 0; i < 10; i++) {
              shoot(unitId)
            }
        

with?

            with(unitApi) {
              goTo(5, 0)
             
              for(i = 0; i < 10; i++) {
                  shoot(unitId)
              }
            }
        
eval()
  1. eval()
  2. setTimeout()
  3. new Function()

JS Scopes

Proxy + scopes

Как работает with

            with({foo: 'bar'}) {
              foo
            }
        
            'foo' in obj
              ? obj.foo
              : global.foo
        

with + Proxy

            const safeApi = new Proxy(unitApi, {
              has: () => true
            })
             
            with(safeApi) {
              ${userCode}
            }
        

with + Proxy = ❤️

  1. > eval()
  2. >> eval in unitApi?
  3. << true!

А так можно 0_о

            with ([1, 2, 3]) {
              filter(i => i !== 3)
              .some(i => i === 3)
            }
            // false
        

И даже так

            with ({hello: 'HolyJS'}) {
              valueOf()
            }
            // {hello: 'HolyJS'}
        

Symbol.unscopables

            const safeApi = new Proxy(unitApi, {
              has: () => true,
              get: (target, key) =>
                  key === Symbol.unscopables ? undefined : target[key]
            })
        

Окружение Worker

  1. this
  2. self
  3. postMessage()
  4. onmessage
  5. fetch()
  6. FileApi
  7. ...

Окружение Worker

                const nativePostMessage = this.postMessage;
                 
                Object.keys(this).forEach(key => {
                  delete this[key]
                }
                 
                with (sandboxProxy) {
                  (function() {
                      ${USER_CODE}
                  }).call({"hi": '=)'})
                }
            

console, Math?

                const apis = {console, Math, parseInt, ...}
                const unitApi = Object.assign(unitApi, apis)
                const sandboxProxy = new Proxy(unitApi, {has, get})
                 
                with (sandboxProxy) {
                  try {
                      ${USER_CODE}
                  } catch (e) {
                      console.error(e)
                  }
                }
            

Custom console

                ['log', 'info', 'warn', 'error'].forEach(patchMethod);
                 
                with (sandboxProxy) {
                  try {
                      ${USER_CODE}
                  } catch (e) {
                      console.error(e)
                  }
                }
                 
                function patchMethod(name) {
                  const nativeMethod = console[name].bind(console);
                  
                  console[name] = (...data) => {
                      attributes = attributes.map(parseParams)
                      
                      postMessage(JSON.stringify({type: name, data}))
                  }
                }
            

Асинхронность

Unit api:

            goTo(x, y) {
              return {type: 'goTo', x, y}
            }
        

Результат Worker

            actions = [
              {type: 'goTo', x, y},
              {type: 'shoot', id},
              {type: 'goTo', x, y}
            ]
        

Изолированные Workers

  1. Каждый юнит имеет изолированный поток
  2. Ели код юнита упал, остальные продолжают работать
История
разрушительный Math.random()

Пример кода

            const ids = ['IE', 'DART', 'EVAL'];
             
            for(i = 0; i < 10; i++) {
                shoot(ids[(ids.length * Math.random())|0])
            }
        

Проблема random()

            (ids.length * Math.random())|0
            1
            (ids.length * Math.random())|0
            3
            (ids.length * Math.random())|0
            2
        

Проблема random()

100 / 200
200 / 100
300 / 10

Проблема random()

Подсолим random()

Функция

            // LCG – Линейный конгруэнтный метод
            function LCG(seed) {
              return function() {
                  seed = Math.imul(16807, seed) | 0 % 2147483647
                  return (seed & 2147483647) / 2147483648
              }
            }
        

Соль?

            const getRandomSalt = (seed) =>
              [...seed].reduce((acc, curr) =>
                  acc + curr.charCodeAt(0), 0) % 2147483647
             
            const salt = getRandomSalt(userCode)
            Math.random = LCG(salt)
        
State sharing
RxJS, сервер и клиенты

Есть 4 роли

  1. Left player
  2. Right player
  3. Watcher
  4. Admin

Источник истины

RxJS на сервере

RxJS на сервере

            leftIsReady$ = socket$.pipe(filter(...))
            rightIsReady$ = socket$.pipe(filter(...))
             
            forkJoin(leftIsReady$, rightIsReady$)
              .subscribe(() => {
                this.setState({mode: 'ready'})
              })
        

Фильтрация стейта

Роли

            class LeftPlayer extends Client {}
            class RightPlayer extends Client {}
            class Watcher extends Client {}
            class Admin extends Client {}
        

Connections Pool

            class Client {
              connections = new Set()
              
              onUnsafeMessage$ = new Subject()
            }
        

Игрок

            class LeftPlayer extends Client {
              get onMessage$() {
                  return this.onUnsafeMessage$
                      .pipe(
                          filter(query => query.type === 'state'),
                          map(state => ({left: state.left}))
                      )
              }
        

Комната

            class Room {
              onState$ = this.clients.onMessage$
                  .pipe(filter(query => query.type === 'state'))
              
              constructor() {
                  this.onState$.subscribe(state => 
                      this.setState(state))
              }
        

RxJS на клиенте

RxJS на клиенте

            rightArmyChanges$ = socket$.pipe(
                  filter(state => !!state.right),
                  pluck('right', 'army')
              )
              .subscribe(enemyArmy => {
                  this.setState({right: {army: enemyArmy}})
              })
        

RxJS на клиенте

            rightArmyChanges$
              .subscribe(army => this.setRightArmy(army))
              
            rightNameChanges$
              .subscribe(name => this.setRightName(name))
        
История
из синглплеера в мультиплеер
и снова за 3 недели

Взять и переписать

Список лидеров

JS Gamedev
и его проблемы

В обычном вебе

Не беда, в течение рабочего дня можно написать все свое

В JS gamedev

Но есть решение

cursedcoder/awesome-pixijs

  • React-pixi
  • tween.js
  • ...
Типы игроков
кто они и как с ними подружиться

Итак, почти готово

Исследователи

Рядовые игроки

Собиратели багов

Тестируем
только на живых людях

Тестирование

Итоги
правило 80 / 20

Ссылки

???