JS-battle: how I wrote my eval ()

    You can remember Alexander Korotayev from the browser version of Heroes of Might and Magic: deciphering his report about her gathered a huge number of views on Habré. And now he has made a program-oriented game: you need to play it with JS-code.

    This time, the development took not years, but weeks, but still there were some interesting challenges. How to make the game comfortable even for developers who have never touched JavaScript before? How to protect yourself from simple ways to outsmart the game?



    As a result, Alexander again made a report on HolyJS, and we (the conference organizers) again prepared a text version for Habr.


    My name is Alexander Korotaev, I work at Tinkoff.ru, I work in the frontend. In addition, I am part of the Spb-frontend community, helping to organize meetings. I make a Drinkcast podcast, we invite interesting people and discuss various topics.

    What is the essence of toys? First, there must choose a unit from the proposed, it is such an RPG-system: each unit has its strengths and weaknesses. You see which units the opponent has chosen, and choose in retaliation to him. Then you need to write a script of your army's behavior in JavaScript — in other words, the script “what each unit should do on the battlefield.”

    This is done in debug mode: in fact, you debug the code, and then both opponents push their code, and the battle between the two sides begins.

    Thus, you can see how two scripts, two logic, two algorithms fight each other. I always wanted to do something like that, and now it is finally implemented.

    In action, it all looks like this:


    What did the work on it look like? A lot of work went on the documentation. When a player sits down at a laptop, he sees the documentation, in which everything is described in some detail.

    It took me a lot of time on its layout, refinement and questioning among people, whether it is understandable. The result was clear for sshnikov, javistov and other developers who do not know anything about JS at all. This toy can even be promoted by JavaScript: “It's not scary, see how you can write on it, even something fun turns out.”



    We had a big tournament in our company, in which virtually any programmers that we have participated.

    From technologies I used the most popular game engine from the JS world - Phaser. The largest and most popular frequently used Ace Editor. This is an editor on the web, very similar to Sublime or VSCode, it can be embedded into a web page. I also used RxJS to work with asynchronous interactions from different users and Preact to render html. Of the native technologies, I often worked with workers and websocket.





    Games for programmers


    What are games for programmers? In my opinion, these are games where you need to code, then get some fun result that you can compare with someone, this is a battle. From such accessible online games I know “Elevator Saga” - you write scripts for elevators according to certain parameters. “Screeps” - about biology, molecules, write scripts for them.

    There are still toys that are sometimes at conferences. The most popular of them is “Code in the Dark”, today it was also presented here. By the way, “Code in the dark” somehow inspired me to this.



    Why was this done? I received a task that I needed to come up with something cool with a booth for a conference, something unusual. Not so eichary were with questionnaires. It is clear that everyone wants to attract attention and gather contacts. We decided to go ahead and come up with something cool and fun for programmers. I realized that programmers want to fight, compete, and I need to give them that opportunity. It is necessary to create a stand on which they will come and will code.

    Gamification We did this not only among practicing programmers, but also among students. We conducted such matches at the institutes on Career Day. We needed to somehow see what kind of guys were there, whether we were good or not. We used gamification to lure people into the process, see how they act, what they do. They played and were distracted, but it gave us information. Some people shoved the code without even running it once, and it was immediately obvious that it was too early for development.



    How it looked in the first version. It was the main screen and two laptops for players. All this was connected with the server, the server kept the State and fumbled it between all the connected clients. Each screen was a connected client. Players' laptops were interactive screens from which this state could be changed. Screens are tightly bound to one server.

    The story of the lack of time


    The first story I encountered in this design is the story that I had very little time. Literally in five minutes, an idea was invented, in five seconds a name was invented when it took to create a repository on GitHub. I could spend it all just four hours in the evening, taking them even from my wife. As a result, I had only three weeks before the conference to realize it at least somehow. It all began in such a way that it was just necessary to invent an idea within the framework of a brainstorm, in five minutes the idea was born: “Let's write some kind of artificial intelligence for the RPG on JS”. It's cool, fun, I can do it.



    In the first implementation, there was a code editor and a battle screen on which the battle itself was. Phaser, Ace Editor and pure Node.js were used as a server without any frameworks. What I later regretted about is true, but then nothing much was required of the server.



    I managed to realize then Renderer, who painted the battle itself. The most difficult part was the sandbox for the JS code, that is, the sandbox, in which everything was to be performed separately for each player. There was also State sharing, which came from the server. Players somehow changed the state, threw it on the server, the server sent the rest of the web sockets. The server was the source of truth, and all connected clients trusted what came from the server.

    Sandbox


    What is so difficult in the implementation of the sandbox? The fact is that the sandbox is a whole world for the code in which the code must exist. That is, you create for him about the same world as around, but only consisting of some kind of conventions, an API, with which you can interact. How to implement it on JS? It seems that JS is not capable of this, it is so full of holes and it’s free that it’s just not possible to completely enclose the user code in a box without using a separate virtual machine with a separate OS.



    What should the sandbox do?

    First, as already said, isolate the code. It should be a world from which it is impossible to break out.

    Also there should be an API for managing units. Players must interact with the battlefield, moving units, directing them, giving them an attack vector.
    And any actions of units are asynchronous, that is, it somehow has to work with asynchronous code.



    What did I want to say about asynchrony? The fact is that in JS it is implemented with the help of promises. Everything is clear to everyone here, promises are a great thing, they work fine, we almost always had it. For many years everyone knows how to work with them, but this toy was not only for javascriptors. Can you imagine if I began to explain to javistams how to write the battle code using promises? How to do then-then-then, why sometimes it is not necessary ... What to do with conditions or cycles?



    You can, of course, go the best way and take the syntax async / await. [Slide 8:57] But imagine, too, how can programmers from outside the world of javascript explain that you should put await just before every line? As a result, the best way to work with asynchrony is not to use it at all.



    Make the most synchronous code and the most simple API, similar to almost any programming language. We make a toy not only for people writing in JS, we want to make it available to anyone who knows how to code.



    We all need to somehow run. The user writes the code, and we need to execute it, move the units on the map. The first thing that comes to mind is that we need eval () plus a deprecated with statement that is not recommended for use on MDN. This will work, but there are some problems.



    For example, we have a code that completely destroys our entire idea, which generally prevents us from doing anything further. This is the code that blocks execution. It is necessary to do something so that the user could not block the application. For example, an infinite loop can break everything. If alert () and prompt () can still be overridden, then we cannot override the infinite loop at all.

    eval () is evil


    So we get to the point that eval () is evil. No wonder it is called evil, because it is an insidious function that actually absorbs all the most free and open that is in JS, and leaves us completely defenseless. With one simple function, we make a huge hole in our application.



    But what if I tell you [in the voice of Steve Jobs] that we have reinvented eval ()?

    We have done eval () on other technologies, it works almost the same as the same eval () that we already have. In fact, in my code there is a function eval (), but implemented with the help of Workers, operator with and Proxy.



    Why workers? The fact is that they create a separate flow of execution, that is, JS is single-threaded, but thanks to the workers we can get another stream. This gives us many advantages. For example, within the framework of the same infinite cycles, we can cut off the flow created through the worker from the main flow, perhaps this is the main reason why I used workers. If the worker has managed to work faster than one second, we consider it successful, we get its result. If not, then we just cut it off. In fact, the user code for some reason did not work, there were some incomprehensible errors, or it slowed down due to an infinite loop. Many today tried to write while (true), I warned that this would not work.



    To write our worker, we just need to feed the script into the worker's constructor, which will be loaded via http. Inside the script, we need to make a message handler from the main thread. Using the postMessage () function inside the worker, we can send messages to the main thread. So we make communication between two threads. A fairly convenient simple API, but something is missing in it, namely the user code that we need to execute in this worker. We will not, each time, generate some kind of script file on the server and feed it to the worker.



    I found a way using URL.createObjectURL (). We make a block and feed it to the src worker. Thus, it unloads our code directly from the line. By the way, this path works with any objects in the DOM that have src - image works this way, for example, and even in an iframe you can load html, just by generating it from a string. Pretty cool and flexible, I think. We can also control the worker by simply passing it our specially generated object from the URL. We can also terminate it and it already works as we need, and we created the first sandbox.



    Asynchronous interactions go further, because any work with workers is asynchronous. We sent some message, and we cannot synchronously wait for the next message, the worker only returns an instance to us, and we can subscribe to the messages. We catch the message with RxJS, we create two threads: one for the successful message from the worker, the second to complete by timeout. Two threads that we then manage with their merge.



    RxJS has operators that allow us to work with threads. In fact, this is like lodash for synchronous operations. We can indicate some function and not think about how it is implemented inside; it removes a headache from us. We have to start thinking in flows, the operator merge merges our flows, reacts to any message. It will respond to both timeout and message. We need only the very first message, respectively, after the first message we terminate the worker. In case of an error, we derive this error, in case of success we make a resolve.



    It's all pretty simple. Our code becomes declarative, the complexity of asynchrony goes somewhere. The main thing is to learn these operators.



    This is how we work with the Unit API. I wanted the Unit API to be as simple as possible. Speaking about JS, many people think that it is difficult, you have to go somewhere, learn something. And I wanted to make it as simple as possible: everything in the global area, there is only the scope of the Unit API, nothing more. All for managing units, even autocomplete.



    [Slide 15:20] It begs the decision that all this can be thrust into the same forbidden operator with. Let's understand why it is forbidden.

    The fact is that with has its own problems. For example, with, unfortunately, it is full of holes outside of the scoop that we threw into it, because it tries to look deeper into the Unit API and looks into the global scop.


    This is where the last example is particularly cool, because even a foursome can be dangerous for our code, since all of these functions can be performed by custom code. The user can do anything. This is a game for programmers, and they love to explore the problems and ways of hacking something.



    As I said, the scopes are very leaky, so a global scop is always available. No matter how many scopes we send to our user code, no matter how many scopes we wrap it, the global scope will still be visible. And all because of with.

    In fact, he does not isolate anything, he simply adds a new layer of abstraction, a new global scop. But we can change this behavior with Proxy.



    The fact is that Proxy looks at all our calls to the object that are proxied through the new API, and we can control how the requests for new data in the object behave.



    In fact, with it works quite simply. When we feed him some variable, he checks under the hood whether this variable is in the object (that is, it executes the in operator), and if it does, it executes it in the object, and if not, it executes in the upper skuppe, our case in the global. It's pretty simple here. The main thing that helps us Proxy - we can override this behavior.



    In Proxy there is such a thing as hooks. A wonderful thing that allows us to proxy any requests to the object. We can change the attribute request behavior, change the attribute assignment behavior, and most importantly, we can change the behavior of this in operator. There is a has has hook, which we can return only true. Thus, we will take and completely deceive our with statement, making our API already safer than it was before.



    If we try to run eval (), he will first ask if this eval () is in unitApi, he will be answered “is”, and we get “undefined is not a function”. It seems that this is the first time that I am glad about this error! This mistake is exactly what we should have received. We took and told the user: "Sorry, forget about everything that you knew about the window object, this is no longer there." We have already left part of the problems, but this is not all.



    The fact is that the with statement from JS, JS is dynamic and a bit weird. The odd thing is that not everything works as we would like, without looking at the specification. The fact is that with works also with the properties of the prototype. That is, we can simply feed him an array, execute this incomprehensible code. All array functions are available as global in this scope, which looks a bit strange.



    This is not important for us, it is important for us that the user can execute valueOf () and get our entire sandbox. Just take it and pick it up, see what's in it. It didn’t want that either, so they brought an interesting piece in the specification: Symbol.unscopables. That is, in the new specification on symbols, Symbol.unscopables was introduced specifically for the with operator, which is prohibited. Because they believe that someone else uses it. For example me!



    Thus, we will make another interceptor, where we specifically check whether this symbol is in the list of all unscopables attributes. If not, then return it, but if yes - then sorry, we do not return. We do not use it either. And thus, with with, we cannot even get a prototype of our sandbox.



    We still have the Worker environment. This is something that hangs in a global area and is still available. The fact is that if you simply override this, it will be available in the prototype. Through the prototype in JS, almost everything can be pulled out. Surprisingly, all these methods are still available through the prototype.



    I had to just take and clean all this. We go through all the keys and clean it all.



    And then we leave a little Easter egg for the user, who still try to call this. We take the usual function, the main thing is that it is not an arrow switch, which has a scop, and we change it to our object, in which we leave a small easter egg for a particularly curious user who wants to bring some this or self to the console. I think that Easter eggs are great, and you need to leave them in the code.



    Further, it turns out that there are only with our Unit API. We completely blocked everything - in fact, left a whitelist. We need to add those APIs that are useful and needed. For example, the Math API, which has a useful random function that many use when writing code for units.

    We also need the console and many other utilitarian functions that do not carry any destructive function. We create a while list for our APIs. This is good, because if we created a blacklist, we would be dependent on any browser update that occurs without our knowledge.



    Having created a whitelist, we can start using try-catch in our code. Our wrapped code already catches errors and can send them to the user, which is very important for debag.



    But the fact is that the console methods from Worker do not manifest themselves in user code. That is, if you open the console and go into the environment of the worker, they will be available, but telling the user "open the console and see what happened to you" would be wrong. I decided to make a friendly console for the player, where a player without even a JavaScript experience could see in a friendly form what happened.

    They write to him that in the code an error, something went wrong. All this is in our console, so we need to catch errors. In any case, you need to filter the messages in order not to show the user paths to files from the webpack and other things.



    For this, I use the magic patchMethod (), which simply patches the console methods, replacing them with the usual postMessage (). In fact, we send postMessage () to each console log, error, warn, info. We patch all console methods and we know when the user calls the console. It is necessary to display all this in a normal <div>, which will show all this beautifully, when the user has an error, when the code has been executed and when not, to always give the user feedback on what happened with the game at a specific point in time.



    I talked a lot about asynchrony, that all user actions are asynchronous. All actions at a unit are also asynchronous: some passage to some cell is necessarily animation, this is passage through several cells, after some time the next action needs to be performed. I, say, do not use promises at all. The fact is that within these functions I have trite actions, which in the form of objects transfer data further.



    After executing all the custom code, I get an array of actions. Why so, why I have not real-time battle? The fact is that the real-time battle with the participation of workers gives problems with the need to set up communication between the client and the worker, and I only had three weeks. I realized this as quickly as possible and more or less qualitatively so that nothing falls off. The main part of this toy, on which everything keeps, worked. Therefore, all the code you write on the player's screen is executed before the battle. And then the whole battle is already going on the script.



    I get isolated workers who make scripts for each specific unit. These workers are isolated so that the error in the code of each specific unit does not hit the others. If one unit code has fallen off, the rest continue to walk. It is necessary that if a player wrote a code that is not executed (as some students who tested the game did), the opponent still executed his code and could win this battle. The moral is simple: write bad code - lose.


    Destructive Math.random ()


    Everything was fine, I did everything, I had literally one evening left before the first conference at which we launched the toy. And then I remembered Math.random ().

    It seems to work as it should, but I remembered that the toy is performed only on the client, and we can have several clients and the same battle can be launched on several clients. This means that Math.random () will each time produce something different.



    That is, if a player tries to write a code that shoots units in a random order (and this is absolutely normal), JS issues different numbers, but the same numbers are issued on different clients. That is, different players have a different outcome of the battle.



    And if we consider that we organized the tournament in our company and gave prizes for all this, you can imagine how many people with forks and torches would run after me. I could not prove anything to them, I needed to find something.

    And in the end, I literally got a lot of problems in one evening, which could seriously knock down my game. Of course, it was possible to make a crutch in the form of a random () ban, but I found the best way.



    I know that random () is not completely random - for the “fair random” they are still looking for a reasonably inexpensive solution suitable for use in personal computers. In the end, I realized that I needed to find some kind of unique salt that would make this random () pseudo-random, but for the players it should remain random. As soon as the player changes something and restarts the code, he should produce a random number. But in the case when the same battle is launched on different clients, this random () should work exactly the same.



    In the end, I used the linear congruential method. This is the simplest function I found to create random () with salt. We throw some seed and by simple calculations we make a random number from it (in the sense that we cannot predict it without the calculations that we perform).


    Salt turns out from the user code, still I throw units there. We summarize all the character indices in the user's code and get some salt, from which then we make the function random (). This allows you to work transparently for me and opaque to users and saves us a lot of problems with the fact that random () is executed differently on different clients.

    By referenceAll code that is sent to the worker is needed to make JS completely safe. The orange “insert” is the same user code. which is injected there. That's how much code you need to just make JS safe. In the same place random () and unit API are injected. By injecting, I get even more code that is sent to the worker.



    State sharing: RxJS, server and clients


    So we made out what we have with the client. Now let's talk about state sharing, why it is needed and how it was organized. At us state is stored on the server, the server should fumble it to the connected clients. [Slide 28:48]

    We have four roles of different clients that can connect to the server: “left user”, “right user”, a viewer who looks at the main screen, and an administrator who can do anything.

    The left screen cannot change the state of the right player, the viewer cannot change anything, and the admin can do everything.



    Why was this a problem? Everything is pretty simple. Any connected client can throw a session, the server accepts it and a merjit with a state that is inside the server and then distributes to all clients. He fumbles for any changes that come to him. It was necessary to filter it somehow.



    For a start, I will say why RxJS is also on the server. All interactions with two or more connected users become asynchronous. We must wait for the results from both users. For example, both users clicked the “Done” button, you have to wait until both click, and then perform the action. It's all pretty easy to resolve to RxJS in this way:



    We again operate with threads, there is a stream from the socket, which is called socket. To make one thread that monitors only the left player, we simply take and filter messages from this socket by the left player (and similarly with the right one). Then we can combine them with the help of the forkJoin () operator, which works like Promise.all () and is its counterpart. We wait for both of these actions and call the setState () method, which sets our state as “ready”. It turns out that we are waiting for both players and changing the state of the server. On RxJS, this is obtained as declarative as possible, so I used it.

    The problem remains that players can change the state of each other. We must forbid them to do it. Still, they are programmers, there were precedents that someone tried. Create separate classes for them that are inherited from Client.



    They will have the basic logic of the player’s communication with the server, and in each particular class there will be his custom logic for filtering data.

    Client is actually a pool of connections, connections to clients.



    It simply stores them and it has an onUnsafeMessage stream that is completely unsafe: it cannot be trusted, it’s just raw messages from the user that it accepts. We write these raw messages to the stream.

    Further, when implementing a specific player, we take this onUnsafeMessage and filter it.



    We need to filter only the data that we can get from this player that we can trust. The left player can only change the state of the left player, so we take from all the data that he could send, only the state of the left player. If not sent - okay. If sent - take. Thus, we receive messages from a completely unsafe message that we can trust when working in a room.



    We have game rooms that unite players. Inside the room, we can write the very functions that can change state directly, simply by subscribing to these streams, which we can already trust. We abstract from heaps of checks. We did the cast checks and called them separate classes. We divided the code in such a way that inside the controller, where important functions of changing the state are performed, the code became as simple and declarative as possible.

    Also, RxJS is used on the client, it is connected to the socket on the reverse side, emittit events and redirects them in every possible way.

    In this case, I would like to analyze an example when I need to change the army of the right opponent.



    To subscribe to it, we create a stream from the same socket and filter it. We are convinced that this is really the right player, and we take from him a message about what kind of army he has. If there is no such message, the flow will not return anything, there will be no messages in it, it will be silent until the player changes the army. We immediately declaratively solve the problem of filtering events, we simply do not have it.

    And when something has already come from the stream, we call the setState () function. It's quite simple, the very approach that allows us to do everything transparently and declaratively. The very thing for which I took on the RxJS project and in which he helped me a lot.



    I create threads that I have quite clearly named, with which it is clear how to work, everything is declarative, the necessary functions are called, there is no messing around with a lot of if and filtering events, RxJS does it all.

    Story: from single player to multiplayer



    So, the first version of my toy was written. We can say that it was a single player, because only two players could play it, the number of connected clients was fixed. We had the left player, the right player and the screen behind, it all connected directly to the server. Everything was tough, after all it was done in three weeks.

    I received a new proposal: expand the toy on all programmers in the company so that they can open and play it on their computers. So that we have a leaderboard, multiplayer, so that they can play together. Then I realized that I had a big refactoring.



    As a result, it was not so difficult. I simply combined all the entities that I had into separate rooms. I had the essence of “Room”, which could unite all the roles. Now not the players themselves communicate directly with the server, but the rooms. The rooms had already proxied the requests directly to the server, replacing the state, and the state became for each room separately.



    I took and rewrote everything, added a list of leaders, the best ones were awarded with prizes. It was necessary just a large number of users, it was impossible to keep an eye on everyone, it was necessary to write something where to collect all the data.

    JS Gamedev and his problems



    Thus, I have already become seriously acquainted with JS game dev. I did the last project for about three years, occasionally resting. And then I had both weeks for three weeks. I sat every day and did something in the evenings.

    What problems are there in the development of games on JS? Everything is different from our business applications, where it’s not a problem to write something from scratch. Moreover, there are many places where it is even more welcome: we’ll do our best, remember the stories with NPM and left-pad yourself.



    It is impossible to do this in JS Gamedev, because all technologies for displaying graphics are so low-level that it is economically unprofitable to write something on them. If I took this toy and started writing it from scratch on WebGL, I would have spent about six months on it, just trying to figure out some strange bugs. The most popular game engine Phaser removed these problems from me ...



    ... and added new ones to me: 5 megabytes in a bundle. And with this nothing could be done, he does not even know what treeshaking is. Moreover, only the latest version of Phaser can work with webpack and bundles. Before that, Phaser was connected only in the script html-tag, it was strange for me.

    I come from any kind of web scripts, and in JS game deve almost nothing is able to do that. Any modules have extremely poor typing or do not have it at all, or in principle do not know how to webpack, it was necessary to find ways to wrap. As it turned out, even Ace Editor in its pure form does not work at all with a webpack. To start working, you need to download a separate package, where it is already wrapped (brace).

    It was about the same with Phaser before, but in the new version they did it more or less normally. I continued to write on the Phaser and found how to make everything work with the webpack as we used to: so that there was typing and tests could be screwed to all this. I found that you can take PixiJS separately , which is a webpack renderer, and find for it a lot of modules that are ready to work with it.



    PixiJS is a great library that can render either on WebGL or on Canvas. Moreover, you can even write code like for Canvas, and it will be rendered in WebGL. This library is able to render 2D very quickly. The main thing is to know how it works with memory in order not to fall into a position when the memory is over.

    I recommend separately on the awesome-pixijs gitHub repository where you can find different modules. Most of all I liked React-pixi. We can simply abstract away from solving problems with a view, when we write imperative functions directly in the controller to draw geometric shapes, sprites, animation, and so on. We can mark everything in JSX. We come from the world of JSX with our business application and can use them further. The very thing for which I love abstraction. React-pixi gives us this familiar abstraction.

    I also advise you to take tween.js - that famous Phaser animation engine, which allows you to create declarative animations that are somewhat similar to CSS animations: make the transition between states, and tween.js decides for us exactly how to move the object.

    Types of players: who they are and how to make friends with them


    I ran into different players, and I would like to tell you more about toy testing. I collected colleagues in one closed room and did not let them out until they finished playing. Unfortunately, not everyone could finish the game, because at the very beginning I had a lot of bugs. Fortunately, I started testing as soon as at least some working prototype appeared. Honestly, the first testing failed, because a couple of players didn’t get anything. It was a shame, but it gave me a kick that allowed me to move on.

    When your toy is ready, you can be received very well, or with a fork and torch. All people are waiting for the games of a fan, waiting for the happiness that you give them. And you give them something that does not work at all, although it seems to have worked for you. When you have an online toy, there are even more such bugs.

    As a result, the most pleasant people of those whom I encountered are “researchers” who always find more in your toy than it really is. They can pleasantly supplement it with all sorts of trifles, prompting you to add something. But, unfortunately, communication with these people did not give the important thing - the stability of the toy.

    There are ordinary players who come exclusively for fun. They may sometimes not even notice bugs, somehow skipping through them on their way to pleasure.

    Another category is bug collectors for which almost everything does not work. You need to be friends with such people, although they will talk a lot of negativity. We need to establish strange relationships with them: they hurt you, and you try to take something useful for yourself from them, "let's sit at your computer, we'll see." We need to work with them, because in the end it is these people who will make your game a quality one.

    You need to test only on living people. Your eye becomes blurred, and testing will definitely show what is hidden. You develop a toy and plunge deeper, cut some features, and they may not even be needed. You go directly to your consumers, show them and watch how they play, what keys they press. It gives you an incentive to do exactly what you need. You see that some people constantly press Ctrl + S, because they are used to save the code - well, at least run the code by Ctrl + S, the player will feel more comfortable. You need to create a comfortable environment for the player, for this you need to love him and follow him.

    The 80/20 rule works: you make a demo 20% of the time of the entire game development, and for the player it looks like 80% completed game. The perception works in such a way that the basic mechanics is ready, everything moves and works, which means that the game is almost ready, and the developer will soon finish. But in fact, the developer still has a way out of 80%. As I said before, I had to work on the documentation for quite a long time so that it was understandable for everyone. I showed it to many people who spoke their comments, I filtered them, trying to understand the essence of the statements. And it took me a lot of time to search for bugs.

    So I would advise you to do only demos in game devs: they please everyone, they don't take much time, and nobody really wants anything from demos. Finishing games is a boring process, but starting is a great one.

    Finally, I leave the links to you:

    May 24-25, St. Petersburg will host HolyJS 2019 Piter , a conference for JavaScript developers. The site already has the first speakers .
    You can also apply for a report, Call for Papers is open until March 11.
    Ticket prices will rise on February 1.

    Also popular now: