{"id":445565,"date":"2025-01-21T15:09:10","date_gmt":"2025-01-21T15:09:10","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=445565"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=445565","title":{"rendered":"<span>\u041f\u0438\u0448\u0435\u043c \u043c\u043e\u0440\u0441\u043a\u043e\u0439 \u0431\u043e\u0439 \u043d\u0430 VueJS \u0438 Python<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440!<\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043f\u043e\u043f\u0443\u043b\u044f\u0440\u043d\u043e\u0439 \u0438\u0433\u0440\u044b. \u041c\u043e\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0438\u0433\u0440\u0430\u0442\u044c \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0438 \u0441\u0438\u043b\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c.<\/p>\n<p><a href=\"http:\/\/91.245.227.98\/\" rel=\"noopener noreferrer nofollow\">\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u0438\u0433\u0440\u0443<\/a><\/p>\n<p>\u0418\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0438 \u0432 \u043a\u043e\u043d\u0446\u0435 \u0441\u0442\u0430\u0442\u044c\u0438.<\/p>\n<p>\u0422\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0441\u0442\u044d\u043a:<\/p>\n<ul>\n<li>\n<p>Frontend: VueJS [Options API] (TypeScript, Vuex, Canvas, Bootstrap, Nginx)<\/p>\n<\/li>\n<li>\n<p>Backend: Python (FastAPI, Postgres, SQLAlchemy, Websockets)<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u0432\u0435\u0440\u043d\u0443\u0442\u043e \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 docker-compose.<\/p>\n<p>\u0420\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e \u0441\u0430\u043c\u0443 \u0438\u0433\u0440\u0443 \u0432 \u043c\u043e\u0440\u0441\u043a\u043e\u0439 \u0431\u043e\u0439 \u044f \u043d\u0435 \u0431\u0443\u0434\u0443. \u041a\u0430\u0436\u0434\u044b\u0439 \u0438\u0437 \u043d\u0430\u0441 \u043d\u0430\u0432\u0435\u0440\u043d\u044f\u043a\u0430 \u0438\u0433\u0440\u0430\u043b \u0432 \u043d\u0435\u0435 \u0432 \u0442\u043e\u043c \u0438\u043b\u0438 \u0438\u043d\u043e\u043c \u0432\u043e\u0437\u0440\u0430\u0441\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u0436\u0435 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e \u043f\u0440\u043e \u043c\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043e \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0434\u0435\u0442\u0430\u043b\u044f\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0442.\u0434.<\/p>\n<p>\u041e\u0442 \u0432\u0430\u0441 \u0436\u0434\u0443 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043a\u0440\u0438\u0442\u0438\u043a\u0438 \u0438 \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0445 \u0441\u043e\u0432\u0435\u0442\u043e\u0432.<\/p>\n<h2>\u041e\u0431 \u0438\u0433\u0440\u0435<\/h2>\n<p>\u0418\u0433\u0440\u0430 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 \u043e\u0434\u043d\u043e\u0441\u0442\u0440\u0430\u043d\u0438\u0447\u043d\u043e\u0435 SPA-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u043e\u0435 \u043d\u0430 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0435 VueJS \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432 \u044f\u0437\u044b\u043a\u0430 TypeScript (\u0434\u0430\u043b\u0435\u0435 &#8212; TS). \u0414\u0430, \u0432 \u0438\u0433\u0440\u0435 \u0435\u0441\u0442\u044c \u043a\u0435\u0439\u0441\u044b, \u043a\u043e\u0433\u0434\u0430 \u043e\u043d\u0430 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0434\u043b\u044f &#171;\u043f\u043e\u043b\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438&#187; \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0442 \u0438\u0433\u0440\u044b \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0438\u0433\u0440\u043e\u043a\u043e\u0432), \u043d\u043e \u0432 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u0433\u0440\u0443 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u043c SPA-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043c.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0438\u0433\u0440\u0430\u0442\u044c \u0431\u044b\u043b\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430, \u043d\u043e \u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u2014 \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 Responsive Design, \u0432 \u0447\u0435\u043c \u043c\u043d\u0435 \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u043c\u043e\u0433\u043b\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 <a href=\"https:\/\/getbootstrap.com\/\" rel=\"noopener noreferrer nofollow\">Bootstrap [v5.3.3]<\/a>. \u0412\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u044f \u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u043d\u0430 \u043a\u0430\u043d\u0432\u0430\u0441\u0435 \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u043d\u044b \u043a \u0442\u0430\u0447\u0430\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u043e\u0436\u043d\u043e \u043b\u0435\u0433\u043a\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0438\u043c\u0438 \u0438 \u0441 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.<\/p>\n<h2>\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0433\u0440\u044b<\/h2>\n<p>\u041d\u0430 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0434\u0432\u0430 \u0440\u0435\u0436\u0438\u043c\u0430 \u0438\u0433\u0440\u044b\u00a0\u2014 \u00ab\u0418\u0433\u0440\u0430 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c\u00bb \u0438 \u00ab\u0418\u0433\u0440\u0430 \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u043c\u00bb. \u0412 \u00ab\u0431\u044d\u043a\u043b\u043e\u0433\u0435\u00bb \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u00ab\u0418\u0433\u0440\u044b \u0441\u00a0\u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u043e\u043c\u00bb.<\/p>\n<p>\u041f\u0440\u0438\u00a0\u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 UI \u0438\u0433\u0440\u044b \u0438 \u0435\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u0430 \u2014 \u044f \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0435\u0435 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e user-\u0444\u0440\u0435\u043d\u0434\u043b\u0438 \u0438 \u0438\u043d\u0442\u0443\u0438\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043d\u044f\u0442\u043d\u043e\u0439, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0440\u0430\u0441\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u0430. \u0410 \u0442\u0430\u043a\u0436\u0435 \u0434\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0438\u0433\u0440\u043e\u043a \u043d\u0435 \u043a\u043b\u0438\u043a\u043d\u0443\u043b \u0442\u0443\u0434\u0430, \u043a\u0443\u0434\u0430 \u043a\u043b\u0438\u043a\u0430\u0442\u044c \u043d\u0435 \u043d\u0430\u0434\u043e, \u0430 \u0435\u0441\u043b\u0438 \u0438 \u043a\u043b\u0438\u043a\u043d\u0443\u043b, \u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u043b \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0432\u043d\u044b\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f, \u0442\u0438\u043f\u0430:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/7c7\/4e2\/3e3\/7c74e23e36b09bc0c952bb869c96353e.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 1. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0438\u0433\u0440\u044b.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 1. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0438\u0433\u0440\u044b.\" width=\"817\" height=\"118\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7c7\/4e2\/3e3\/7c74e23e36b09bc0c952bb869c96353e.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 1. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0438\u0433\u0440\u044b.<\/figcaption><\/div>\n<\/figure>\n<p>\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432 \u043b\u0435\u0432\u043e\u043c \u043d\u0438\u0436\u043d\u0435\u043c \u0443\u0433\u043b\u0443 \u043e\u043a\u043d\u0430.<\/p>\n<p>\u041d\u043e \u043a\u0440\u0430\u0442\u043a\u043e \u0432\u0441\u0435-\u0442\u0430\u043a\u0438 \u0440\u0430\u0441\u043f\u0438\u0448\u0443 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0433\u0440\u044b.<\/p>\n<p>\u0414\u043b\u044f \u0438\u0433\u0440\u044b \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0438\u043a \u0438 \u043f\u0440\u043e\u0439\u0442\u0438 \u043a\u0430\u043f\u0447\u0443 (\u043e\u043d\u0430 \u043d\u0435 \u0441\u043b\u043e\u0436\u043d\u0430\u044f, \u0432\u0441\u0435\u0433\u043e 4 \u0445\u043e\u0440\u043e\u0448\u043e \u0447\u0438\u0442\u0430\u0435\u043c\u044b\u0445 \u0441\u0438\u043c\u0432\u043e\u043b\u0430).<\/p>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0440\u0435\u0436\u0438\u043c \u0438\u0433\u0440\u044b: \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u043c \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u043a\u043d\u043e\u043f\u043a\u0443.<\/p>\n<h3>\u0418\u0433\u0440\u0430 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c<\/h3>\n<p>\u041f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0438\u0433\u0440\u044b \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u2014 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043c\u0438\u0433\u0430\u044e\u0449\u0430\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c \u00ab\u041f\u043e\u0438\u0441\u043a \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430\u00bb \u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0431\u0443\u0434\u0435\u0442 \u0436\u0434\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0433\u0440\u043e\u043a\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0437\u0430\u0439\u0434\u0435\u0442 \u0432 \u0438\u0433\u0440\u0443 \u0438 \u0442\u043e\u0436\u0435 \u0432\u044b\u0431\u0435\u0440\u0435\u0442 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u044b\u0439 \u0442\u0438\u043f \u0438\u0433\u0440\u044b. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0438\u0433\u0440\u0430 \u00ab\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u00bb \u0438\u0445 \u0432\u043c\u0435\u0441\u0442\u0435 \u0438 \u0432\u044b\u0432\u0435\u0434\u0435\u0442 \u0443 \u043e\u0431\u043e\u0438\u0445 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d \u043d\u0430\u0434\u043f\u0438\u0441\u044c:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f5e\/dfa\/e1c\/f5edfae1cc0319a24920329946fdf666.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 2. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0435 \u0434\u043b\u044f \u0438\u0433\u0440\u044b \u0438 \u0435\u0433\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u0435.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 2. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0435 \u0434\u043b\u044f \u0438\u0433\u0440\u044b \u0438 \u0435\u0433\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u0435.\" width=\"778\" height=\"196\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/f5e\/dfa\/e1c\/f5edfae1cc0319a24920329946fdf666.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 2. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0435 \u0434\u043b\u044f \u0438\u0433\u0440\u044b \u0438 \u0435\u0433\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u0435.<\/figcaption><\/div>\n<\/figure>\n<p>\u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u0440\u0438\u0441\u0442\u0443\u043f\u0430\u0435\u043c \u043a \u0440\u0430\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.<\/p>\n<p>\u041d\u0430 \u043b\u0435\u0432\u043e\u043c \u0433\u0440\u0438\u0434\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0412\u0430\u0448\u0430 \u0444\u043b\u043e\u0442\u0438\u043b\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u043f\u043e\u043b\u0435 \u0431\u043e\u044f. \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u2014 10 \u0448\u0442\u0443\u043a. \u041e\u0434\u0438\u043d 4-\u0445 \u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0439, \u0434\u0432\u0430 3-\u0445 \u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0445, \u0442\u0440\u0438 2-\u0445 \u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0445 \u0438 \u0447\u0435\u0442\u044b\u0440\u0435 \u043e\u0434\u043d\u043e\u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0445 . \u041f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0442\u044c \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u043c\u043e\u0436\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043f\u0435\u0440\u0432\u0443\u044e \u043a\u043b\u0435\u0442\u043a\u0443. \u0415\u0441\u043b\u0438 \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c (\u0442\u0430\u043f\u043d\u0443\u0442\u044c) \u043f\u043e \u043d\u0435\u0439 \u0434\u0432\u043e\u0439\u043d\u044b\u043c \u043a\u043b\u0438\u043a\u043e\u043c \u2014 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u043e\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 (\u0441 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442). \u041f\u0440\u0438 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u043d\u0430 \u0433\u0440\u0438\u0434\u0435 \u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438<\/p>\n<div class=\"floating-image\">\n<figure class=\"float\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1eb\/482\/1e9\/1eb4821e99120b03007b68f7ab294115.gif\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 3. \u041a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u043c\u0435\u0441\u0442\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u044f\/\u043a\u0430\u0441\u0430\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 3. \u041a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u043c\u0435\u0441\u0442\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u044f\/\u043a\u0430\u0441\u0430\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.\" width=\"342\" height=\"202\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1eb\/482\/1e9\/1eb4821e99120b03007b68f7ab294115.gif\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 3. \u041a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u043c\u0435\u0441\u0442\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u044f\/\u043a\u0430\u0441\u0430\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.<\/figcaption><\/div>\n<\/figure>\n<p>\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u2014 \u0438\u0433\u0440\u0430 \u0441\u043b\u0435\u0434\u0438\u0442, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u044b\u043b\u043e \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u0439, \u0430 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0438\u0445 \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u2014 \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u043c\u0435\u0441\u0442\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u0439 (\u043a\u0430\u0441\u0430\u043d\u0438\u0439) \u043a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c. \u041f\u0440\u0438 \u0442\u0430\u043a\u043e\u0439 \u0440\u0430\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0438\u0433\u0440\u043e\u043a\u0443 \u043d\u0430\u0447\u0430\u0442\u044c \u0438\u0433\u0440\u0443 \u0438 \u0432\u044b\u0432\u0435\u0434\u0435\u0442 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435. \u041f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u2014 \u0435\u0441\u043b\u0438 \u043e\u043d \u0441\u0442\u043e\u0438\u0442 \u0440\u044f\u0434\u043e\u043c \u0441 \u0433\u0440\u0430\u043d\u0438\u0446\u0435\u0439 \u0438 \u043f\u0440\u0438 \u043d\u043e\u0432\u043e\u043c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u0435\u0435 \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0442\u044c \u2014 \u0438\u0433\u0440\u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0434\u0438\u0442 \u043e\u0431 \u044d\u0442\u043e\u043c. \u0422.\u0435. \u0435\u0441\u043b\u0438, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e\u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0445\u043f\u0430\u043b\u0443\u0431\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u043d\u0430 \u0440\u0438\u0441\u0443\u043d\u043a\u0435 3 \u0441 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/475\/e3b\/1c4\/475e3b1c45f3041480029d87f8a2c6d6.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 4. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u0432\u044b\u0445\u043e\u0434\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u044b \u0441\u0435\u0442\u043a\u0438.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 4. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u0432\u044b\u0445\u043e\u0434\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u044b \u0441\u0435\u0442\u043a\u0438.\" width=\"944\" height=\"114\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/475\/e3b\/1c4\/475e3b1c45f3041480029d87f8a2c6d6.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 4. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u0432\u044b\u0445\u043e\u0434\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u044b \u0441\u0435\u0442\u043a\u0438.<\/figcaption><\/div>\n<\/figure>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0442\u043e\u0433\u043e \u043a\u0430\u043a \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u2014 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u043c \u043a\u043d\u043e\u043f\u043a\u0443 &#171;\u0418\u0433\u0440\u0430\u0442\u044c&#187;, \u044d\u0442\u043e \u043f\u0440\u043e\u0438\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u0412\u044b \u0433\u043e\u0442\u043e\u0432\u044b \u043a \u0438\u0433\u0440\u0435:<\/p>\n<\/div>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e3f\/55d\/7ee\/e3f55d7ee2e215fd543f9766792b263b.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 5. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u0433\u043e\u0442\u043e\u0432\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043a \u0438\u0433\u0440\u0435.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 5. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u0433\u043e\u0442\u043e\u0432\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043a \u0438\u0433\u0440\u0435.\" width=\"762\" height=\"185\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e3f\/55d\/7ee\/e3f55d7ee2e215fd543f9766792b263b.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 5. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u0433\u043e\u0442\u043e\u0432\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043a \u0438\u0433\u0440\u0435.<\/figcaption><\/div>\n<\/figure>\n<p>\u0415\u0441\u043b\u0438 \u0412\u044b \u0438 \u0412\u0430\u0448 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u043e\u0431\u0430 \u043d\u0430\u0436\u0430\u043b\u0438 \u00ab\u0418\u0433\u0440\u0430\u0442\u044c\u00bb\u00a0\u2014 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c \u00ab\u0418\u0433\u0440\u0430 \u043d\u0430\u0447\u0430\u043b\u0430\u0441\u044c\u00bb \u0438 \u0438\u0433\u0440\u0430 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u0432\u043e\u00a0\u0432\u0440\u0435\u043c\u044f \u0438\u0433\u0440\u044b \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f. \u041f\u0435\u0440\u0432\u0435\u043d\u0441\u0442\u0432\u043e \u0445\u043e\u0434\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c. \u0423\u00a0\u0438\u0433\u0440\u043e\u043a\u0430, \u0447\u044c\u044f \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u0441\u0442\u0440\u0435\u043b\u044f\u0442\u044c\u00a0\u2014 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0438\u0433\u0430\u044e\u0449\u0430\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c \u00ab\u0432\u0430\u0448 \u0445\u043e\u0434\u00bb:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b01\/74a\/ea7\/b0174aea7cf1f1315ad0d35fd79e0862.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 6. \u041d\u0430\u0447\u0430\u043b\u043e \u0438\u0433\u0440\u044b.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 6. \u041d\u0430\u0447\u0430\u043b\u043e \u0438\u0433\u0440\u044b.\" width=\"718\" height=\"240\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/b01\/74a\/ea7\/b0174aea7cf1f1315ad0d35fd79e0862.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 6. \u041d\u0430\u0447\u0430\u043b\u043e \u0438\u0433\u0440\u044b.<\/figcaption><\/div>\n<\/figure>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0438\u0433\u0440\u043e\u043a\u0438 \u0434\u0435\u043b\u0430\u044e\u0442 \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u044b \u043f\u0443\u0442\u0435\u043c \u043a\u043b\u0438\u043a\u0430 \u043f\u043e \u0442\u043e\u0439 \u0438\u043b\u0438 \u0438\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u0446\u0438\u0438 (\u043a\u043b\u0435\u0442\u043a\u0435 \u0441 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438) \u043d\u0430 \u0433\u0440\u0438\u0434\u0435 \u0441\u043f\u0440\u0430\u0432\u0430. \u041f\u0440\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u043d\u0438\u0438 \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u2014 \u0437\u0430 \u0438\u0433\u0440\u043e\u043a\u043e\u043c \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0441\u0442\u044c \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u0430; \u0435\u0441\u043b\u0438 \u0436\u0435 \u0438\u0433\u0440\u043e\u043a \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u0438\u043b \u043c\u0438\u043c\u043e, \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442 \u043a \u0435\u0433\u043e \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0443. <\/p>\n<p>\u041f\u0440\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u043d\u0438\u0438\/\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u2014 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0433\u0440\u0430\u043d\u0438\u0447\u043d\u044b\u0435 (\u0443\u0433\u043b\u043e\u0432\u044b\u0435) \u043a\u043b\u0435\u0442\u043a\u0438 \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0430 \u043e\u0431\u043e\u0438\u0445 \u0433\u0440\u0438\u0434\u0430\u0445:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/da1\/87e\/284\/da187e28471df8725f92b6e00ab10645.gif\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 7. \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0430 \u043b\u043e\u043a\u0430\u0446\u0438\u0439.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 7. \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0430 \u043b\u043e\u043a\u0430\u0446\u0438\u0439.\" width=\"230\" height=\"194\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/da1\/87e\/284\/da187e28471df8725f92b6e00ab10645.gif\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 7. \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0430 \u043b\u043e\u043a\u0430\u0446\u0438\u0439.<\/figcaption><\/div>\n<\/figure>\n<p>\u0418\u0433\u0440\u0430 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442\u0441\u044f \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440 \u043f\u043e\u043a\u0430 \u043a\u0442\u043e-\u043d\u0438\u0431\u0443\u0434\u044c \u0438\u0437 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u0432 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043d\u0435 \u043f\u043e\u0442\u043e\u043f\u0438\u0442 \u0432\u0440\u0430\u0436\u0435\u0441\u043a\u0443\u044e \u0444\u043b\u043e\u0442\u0438\u043b\u0438\u044e. \u041f\u043e\u0441\u043b\u0435 \u043f\u043e\u0431\u0435\u0434\u044b\/\u043f\u043e\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0443 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u044b, \u0430 \u0443 \u043f\u0440\u043e\u0438\u0433\u0440\u0430\u0432\u0448\u0435\u0433\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0442\u0441\u044f \u043a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/191\/a5f\/5fd\/191a5f5fdf03c46fdff37c2bf163e354.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 8. \u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 8. \u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.\" width=\"387\" height=\"409\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/191\/a5f\/5fd\/191a5f5fdf03c46fdff37c2bf163e354.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 8. \u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.<\/figcaption><\/div>\n<\/figure>\n<p>\u041f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u2014 \u043c\u043e\u0436\u043d\u043e \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0435\u0449\u0435 \u0440\u0430\u0437, \u043d\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f \u0437\u0430\u043d\u043e\u0432\u043e \u0432\u044b\u0448\u0435\u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u0432\u0432\u043e\u0434\u0443 \u043d\u0438\u043a\u0430, \u043a\u0430\u043f\u0447\u0438, \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u0438\u0433\u0440\u044b, \u043f\u043e\u0438\u0441\u043a\u0443 \u0438 \u0442.\u043f. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0441\u043b\u0435 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f \u0438\u0433\u0440\u044b \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043a\u043d\u043e\u043f\u043a\u0430:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/308\/00a\/30c\/30800a30c74bb0a3f645588952d3acb6.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 9. \u041a\u043d\u043e\u043f\u043a\u0430 \u0434\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0438\u0433\u0440\u044b \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 9. \u041a\u043d\u043e\u043f\u043a\u0430 \u0434\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0438\u0433\u0440\u044b \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c.\" width=\"441\" height=\"104\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/308\/00a\/30c\/30800a30c74bb0a3f645588952d3acb6.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 9. \u041a\u043d\u043e\u043f\u043a\u0430 \u0434\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0438\u0433\u0440\u044b \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c.<\/figcaption><\/div>\n<\/figure>\n<p>\u041d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043d\u0435\u0435 \u2014 \u0443 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/688\/cf8\/3cb\/688cf83cbff67713c397e7f01b3dce4a.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 10. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u0438\u043a\u0430 \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0435\u0449\u0435 \u043e\u0434\u043d\u0443 \u0438\u0433\u0440\u0443.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 10. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u0438\u043a\u0430 \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0435\u0449\u0435 \u043e\u0434\u043d\u0443 \u0438\u0433\u0440\u0443.\" width=\"399\" height=\"128\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/688\/cf8\/3cb\/688cf83cbff67713c397e7f01b3dce4a.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 10. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u0438\u043a\u0430 \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0435\u0449\u0435 \u043e\u0434\u043d\u0443 \u0438\u0433\u0440\u0443.<\/figcaption><\/div>\n<\/figure>\n<p>\u0415\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u0442\u043e\u0436\u0435 \u043d\u0430\u0436\u043c\u0435\u0442 \u044d\u0442\u0443 \u043a\u043d\u043e\u043f\u043a\u0443\u00a0\u2014 \u0443\u00a0\u043e\u0431\u043e\u0438\u0445 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0437\u0430\u043d\u043e\u0432\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0438 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0438\u0433\u0440\u0443. \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439 \u043d\u0430\u00a0\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0433\u0440 \u043d\u0435\u0442. \u0414\u043b\u044f\u00a0\u0432\u044b\u0445\u043e\u0434\u0430 \u0438\u0437\u00a0\u0438\u0433\u0440\u044b \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u043e\u0434\u0438\u043d \u0438\u0437\u00a0\u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u0432\u044b\u0445\u043e\u0434\u0438\u0442 \u0438\u0437\u00a0\u0438\u0433\u0440\u044b (\u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443,\u00a0\u043b\u0438\u0431\u043e \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0443) \u0432\u00a0\u0435\u0435 \u00ab\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u0444\u0430\u0437\u0435\u00bb\u00a0\u2014 \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e\u00a0\u0440\u0430\u0437\u0440\u044b\u0432\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ffb\/15b\/cbd\/ffb15bcbd049236a55f163b493878fc7.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 11. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0440\u0430\u0437\u0440\u044b\u0432\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0438 \u0432\u044b\u0445\u043e\u0434\u0435 \u0438\u0437 \u0438\u0433\u0440\u044b.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 11. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0440\u0430\u0437\u0440\u044b\u0432\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0438 \u0432\u044b\u0445\u043e\u0434\u0435 \u0438\u0437 \u0438\u0433\u0440\u044b.\" width=\"891\" height=\"117\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/ffb\/15b\/cbd\/ffb15bcbd049236a55f163b493878fc7.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 11. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0440\u0430\u0437\u0440\u044b\u0432\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0438 \u0432\u044b\u0445\u043e\u0434\u0435 \u0438\u0437 \u0438\u0433\u0440\u044b.<\/figcaption><\/div>\n<\/figure>\n<h3>\u0418\u0433\u0440\u0430 \u0441 \u0434\u0440\u0443\u0433\u043e\u043c<\/h3>\n<p>\u041f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c \u2014 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0444\u043e\u0440\u043c\u0430 \u0441 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043c \u0412\u0430\u0448\u0435\u0433\u043e id \u0438 \u043f\u043e\u043b\u0435 \u0432\u0432\u043e\u0434\u0430 \u0434\u043b\u044f id \u0434\u0440\u0443\u0433\u0430 \u0441 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438:<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/65a\/cff\/5c1\/65acff5c1cc20e7d0c745246a70b13fa.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 12. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 12. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c.\" width=\"453\" height=\"296\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/65a\/cff\/5c1\/65acff5c1cc20e7d0c745246a70b13fa.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 12. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c.<\/figcaption><\/div>\n<\/figure>\n<p>\u0421\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u2014 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043c\u0435\u043d\u044f\u0442\u044c\u0441\u044f \u0441 \u0434\u0440\u0443\u0433\u043e\u043c id-\u0448\u043d\u0438\u043a\u0430\u043c\u0438, \u0432\u0432\u0435\u0441\u0442\u0438 id \u0434\u0440\u0443\u0433\u0430 \u0432 \u043d\u0443\u0436\u043d\u043e\u0435 \u043f\u043e\u043b\u0435 \u0438 \u043d\u0430\u0436\u0430\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0433\u0440\u0443\u00bb. \u0414\u0430\u043b\u0435\u0435\u00a0\u2014 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u0438\u0433\u0440\u0435 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c\u00a0\u2014 \u043d\u0443\u0436\u043d\u043e \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0438 \u043d\u0430\u0436\u0430\u0442\u044c \u00ab\u0418\u0433\u0440\u0430\u0442\u044c\u00bb. \u041f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0438\u0433\u0440\u044b \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u043c\u00a0\u2014 \u0443\u00a0\u0432\u0430\u0441 \u0442\u0430\u043a\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0441 \u043d\u0438\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0431\u0435\u0437 \u043d\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438\u0433\u0440\u044b.<\/p>\n<h2>Backend<\/h2>\n<p>\u0411\u044d\u043a \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u043d\u0430 \u044f\u0437\u044b\u043a\u0435 Python, \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a FastAPI. \u0414\u043b\u044f \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0434\u043e\u043a\u0435\u0440\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d \u043e\u0434\u0438\u043d \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 uvicorn-\u0432\u043e\u0440\u043a\u0435\u0440. \u0424\u0440\u043e\u043d\u0442 \u043e\u0431\u0449\u0430\u0435\u0442\u0441\u044f \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c WebSocket-\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u0411\u0414 \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437 \u0442\u0440\u0435\u0445 \u0442\u0430\u0431\u043b\u0438\u0447\u0435\u043a:<\/p>\n<details class=\"spoiler\">\n<summary>\u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/77b\/a17\/916\/77ba17916ad1f1240b430c72a1ebaf49.png\" alt=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 13. \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414.\" title=\"\u0420\u0438\u0441\u0443\u043d\u043e\u043a 13. \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414.\" width=\"681\" height=\"821\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/77b\/a17\/916\/77ba17916ad1f1240b430c72a1ebaf49.png\"\/><\/p>\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 13. \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414.<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<p>\u0414\u0432\u0435 \u0438\u0437 \u043d\u0438\u0445 \u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u044b\u0435 \u2014 \u0441 \u0442\u0438\u043f\u043e\u043c \u0438\u0433\u0440\u044b \u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u0438\u0433\u0440\u043e\u043a\u0430, \u0430 \u0442\u0440\u0435\u0442\u044c\u044f (TRivalCouple &#8212; <em>\u0441\u043e\u043f\u0435\u0440\u043d\u0438\u0447\u0430\u044e\u0449\u0430\u044f \u043f\u0430\u0440\u0430<\/em>) \u2014 \u0431\u0443\u0444\u0435\u0440\u043d\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0438\u0433\u0440\u043e\u043a\u0438 &#171;\u0441\u043e\u0435\u0434\u0438\u043d\u044f\u044e\u0442\u0441\u044f&#187; \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0431\u043e\u0439 \u0434\u043b\u044f \u0438\u0433\u0440\u044b, \u0430 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0438\u0433\u0440\u044b \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0434\u0430\u043b\u044f\u0435\u0442\u0441\u044f.<\/p>\n<p>\u0414\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0441\u043e\u043a\u0435\u0442\u043e\u0432 \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043c\u0438 \u0431\u044b\u043b \u0441\u043e\u0437\u0434\u0430\u043d \u043a\u043b\u0430\u0441\u0441 ConnectionManager, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0445\u0440\u0430\u043d\u0438\u0442 uuid \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u0435\u0433\u043e WS-\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435:<\/p>\n<details class=\"spoiler\">\n<summary>class ConnectionManager<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">class ConnectionManager:     def __init__(self):         self.active_connections: dict[uuid.UUID, WebSocket] = {}      async def connect(self, client_uuid: uuid.UUID, websocket: WebSocket):         await websocket.accept()         self.active_connections[client_uuid] = websocket      async def disconnect(self, client_uuid: uuid.UUID):         self.active_connections.pop(client_uuid)      async def send_personal_message(self, client_uuid: uuid.UUID, message: dict):         if client_uuid in self.active_connections:             await self.active_connections[client_uuid].send_json(message)      async def send_structured_data(self, client_uuid: uuid.UUID, msg_type: str, data: dict, is_status_ok: bool = True):         message_to_send = {'msg_type': msg_type, 'data': data, 'is_status_ok': is_status_ok}         await self.send_personal_message(client_uuid, message_to_send)      async def broadcast(self, message: str):  # \u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0443 \u043f\u043e\u043a\u0430 \u043d\u0435 \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043b         for connection in self.active_connections.values():             await connection.send_text(message)      async def print_number_of_clients(self):         print('Number of clients: ', len(self.active_connections)) <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0412\u0445\u043e\u0434\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u043e\u0439 \u0441\u043b\u0443\u0436\u0438\u0442 ws-\u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442, \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0449\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441\u044b:<\/p>\n<pre><code class=\"python\">manager = ConnectionManager()  @router.websocket(\"\/client\/{client_uuid}\/ws\") async def websocket_endpoint(ws: WebSocket, client_uuid: uuid.UUID):     await manager.connect(client_uuid, ws)     try:         while True:             data_from_client = await ws.receive_json()             await process_data(client_uuid, data_from_client, manager)     except WebSocketDisconnect:         await manager.disconnect(client_uuid)         await delete_rival_couple_and_notify(client_uuid, manager)         await manager.print_number_of_clients()<\/code><\/pre>\n<p>\u041c\u0435\u0442\u043e\u0434 <code>process_data()<\/code> \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 \u043e\u0442\u0432\u0435\u0442 (\u0441\u043c. controller.py):<\/p>\n<details class=\"spoiler\">\n<summary>controller.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">import uuid from datetime import datetime from random import randint  from sqlalchemy import select, func, or_, and_, ColumnElement, asc  import db from entities.model import TRivalCouple, TGameType from helper.game_state import GameState from helper.game_type import GameType from helper.message_type import MessageType from websocket.common import notify_enemy_about_game_creation from websocket.connection_manager import ConnectionManager from websocket.friend_game_controller import process_friend_game_creation  player_available_condition: ColumnElement[bool] = or_(     and_(TRivalCouple.dfplayer1.__ne__(None), TRivalCouple.dfplayer2.__eq__(None)),     and_(TRivalCouple.dfplayer1.__eq__(None), TRivalCouple.dfplayer2.__ne__(None)), )  random_game_clause: ColumnElement[bool] = TGameType.id.__eq__(GameType.RANDOM.value)   def find_by_client_id_clause(client_uuid: uuid.UUID) -&gt; ColumnElement[bool]:     clause: ColumnElement[bool] = or_(         TRivalCouple.dfplayer1.__eq__(client_uuid),         TRivalCouple.dfplayer2.__eq__(client_uuid)     )     return clause   def available_random_couple_exists() -&gt; bool:     with db.session_scope() as s_:         stmt = select(func.count(TRivalCouple.id)).join(TGameType, TRivalCouple.dfgame_type.__eq__(TGameType.id),                                                         isouter=True).where(             and_(player_available_condition, random_game_clause))         count = s_.execute(stmt).scalar()          if count == 0:             return False         return True   async def create_random_couple(client_uuid, data_from_client):     with db.session_scope() as s_:         rival_couple_player: TRivalCouple = TRivalCouple(             id=uuid.uuid4(),             dfplayer1=client_uuid,             dfplayer1_nickname=data_from_client['nickName'],             dfplayer1_state=GameState.SEARCHING_FOR_OPPONENT.value,             dfcreated_on=datetime.now(),             dfgame_type=GameType.RANDOM.value         )          s_.add(rival_couple_player)   def find_available_random_couple() -&gt; TRivalCouple:     with db.session_scope() as s_:         stmt = select(TRivalCouple).join(TGameType, TRivalCouple.dfgame_type.__eq__(TGameType.id),                                          isouter=True).where(player_available_condition, random_game_clause).order_by(             asc(TRivalCouple.dfcreated_on)).limit(1)         return s_.execute(stmt).scalar()   async def add_player_to_rival_couple(rc: TRivalCouple, client_uuid: uuid.UUID, data_from_client: dict):     with db.session_scope() as s_:         if not rc.dfplayer1:             rc.dfplayer1 = client_uuid             rc.dfplayer1_nickname = data_from_client['nickName']             rc.dfplayer1_state = GameState.SHIPS_POSITIONING.value             rc.dfplayer2_state = GameState.SHIPS_POSITIONING.value         else:             rc.dfplayer2 = client_uuid             rc.dfplayer2_nickname = data_from_client['nickName']             rc.dfplayer2_state = GameState.SHIPS_POSITIONING.value             rc.dfplayer1_state = GameState.SHIPS_POSITIONING.value          s_.add(rc)  # add to s_.dirty for subsequent commit to DB   def find_rival_couple_by_id(game_id: uuid.UUID) -&gt; TRivalCouple:     with db.session_scope() as s_:         return s_.get(TRivalCouple, game_id)   def find_rival_couple_by_client_id(client_uuid: uuid.UUID) -&gt; TRivalCouple:     \"\"\"     \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 \u0442\u0438\u043f\u0430 TRivalCouple \u043f\u043e client_uuid     :param client_uuid: \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (websocket-\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f)     :return: \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u0449\u0430\u044e\u0449\u0430\u044f \u043f\u0430\u0440\u0430 (TRivalCouple)     \"\"\"     with db.session_scope() as s_:         # \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0411\u0414 \u0441 \u0442\u0430\u043a\u0438\u043c \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u043c \u043f\u043e-\u0445\u043e\u0440\u043e\u0448\u0435\u043c\u0443 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430         stmt = select(TRivalCouple).where(find_by_client_id_clause(client_uuid)).limit(1)         return s_.scalar(stmt)   async def delete_rival_couple_and_notify(client_uuid: uuid.UUID, manager: ConnectionManager):     couple = find_rival_couple_by_client_id(client_uuid)      if not couple:         return      with db.session_scope() as s_:         if couple.dfplayer1 and couple.dfplayer2:             if couple.dfplayer1 == client_uuid:  # \u0435\u0441\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f player1, \u0442\u043e \u043e\u043f\u043e\u0432\u0435\u0449\u0430\u0435\u043c player2                 await manager.send_structured_data(couple.dfplayer2, MessageType.DISCONNECTION.value,                                                    {'enemy_nickname': couple.dfplayer1_nickname})             else:  # \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442                 await manager.send_structured_data(couple.dfplayer1, MessageType.DISCONNECTION.value,                                                    {'enemy_nickname': couple.dfplayer2_nickname})          s_.delete(couple)   async def process_data(client_uuid: uuid.UUID, data_from_client: dict, manager: ConnectionManager):     msg_type = data_from_client['msg_type']      if msg_type == MessageType.GAME_CREATION.value:         if data_from_client['game_type'] == GameType.RANDOM.value:              if not available_random_couple_exists():                 await create_random_couple(client_uuid, data_from_client)             else:                 rc: TRivalCouple = find_available_random_couple()                 await add_player_to_rival_couple(rc, client_uuid, data_from_client)                 await notify_enemy_about_game_creation(rc, manager)          else:  # game_type == GameType.FRIEND.value:             await process_friend_game_creation(client_uuid, data_from_client, manager)      if msg_type == MessageType.SHIPS_ARE_ARRANGED.value:         rc = find_rival_couple_by_id(data_from_client['game_id'])          if not rc:             return          with db.session_scope() as s_:             if rc.dfplayer1 == client_uuid:  # \u0435\u0441\u043b\u0438 \u043f\u0435\u0440\u0432\u044b\u0439 \u0438\u0433\u0440\u043e\u043a \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u0442 \"\u0418\u0433\u0440\u0430\u0442\u044c\"                 rc.dfplayer1_state = GameState.PLAYING.value                 await manager.send_structured_data(rc.dfplayer2, msg_type, {'enemy_client_id': str(rc.dfplayer1)})             else:                 rc.dfplayer2_state = GameState.PLAYING.value                 await manager.send_structured_data(rc.dfplayer1, msg_type, {'enemy_client_id': str(rc.dfplayer2)})              #  \u0415\u0441\u043b\u0438 \u043e\u0431\u0430 \u0438\u0433\u0440\u043e\u043a\u0430 \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u043b\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0438 \u043d\u0430\u0436\u0430\u043b\u0438 \"\u0418\u0433\u0440\u0430\u0442\u044c\"             #  \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441 msg_type = PLAY, \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u044e\u0449\u0435\u0435 \u043e \u043d\u0430\u0447\u0430\u043b\u0435 \u0438\u0433\u0440\u044b             if rc.dfplayer1_state == GameState.PLAYING.value and rc.dfplayer2_state == GameState.PLAYING.value:                 #  \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043a\u0442\u043e \u0438\u0437 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u0445\u043e\u0434\u0438\u0442 \u043f\u0440\u0432\u044b\u043c \u0438 \u043e\u043f\u043e\u0432\u0435\u0449\u0430\u0435\u043c \u0438\u0433\u0440\u043e\u043a\u043e\u0432                 turn = randint(1, 2)                 if turn == 1:                     await manager.send_structured_data(rc.dfplayer1, MessageType.PLAY.value, {'turn_to_shoot': True})                     await manager.send_structured_data(rc.dfplayer2, MessageType.PLAY.value, {'turn_to_shoot': False})                 else:                     await manager.send_structured_data(rc.dfplayer1, MessageType.PLAY.value, {'turn_to_shoot': False})                     await manager.send_structured_data(rc.dfplayer2, MessageType.PLAY.value, {'turn_to_shoot': True})              s_.add(rc)  # \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0411\u0414      if msg_type == MessageType.FIRE_REQUEST.value:         await manager.send_structured_data(uuid.UUID(data_from_client['enemy_client_id']), msg_type,                                            {'shot_location': data_from_client['shot_location']})      if msg_type == MessageType.FIRE_RESPONSE.value:         data_for_sending = {'shot_result': data_from_client['shot_result'],                             'shot_location': data_from_client['shot_location']}          if 'sunkShip' in data_from_client:             data_for_sending['sunkShip'] = data_from_client['sunkShip']          await manager.send_structured_data(uuid.UUID(data_from_client['enemy_client_id']), msg_type,                                            data_for_sending)      if msg_type == MessageType.UNSUNK_SHIPS.value:         await manager.send_structured_data(uuid.UUID(data_from_client['enemy_client_id']), msg_type,                                            {'unSunkShips': data_from_client['unSunkShips']})      if msg_type == MessageType.GAME_OVER.value:         await manager.send_structured_data(uuid.UUID(data_from_client['enemy_client_id']), msg_type, data={})      if msg_type == MessageType.PLAY_AGAIN.value:         # \u043e\u043f\u043e\u0432\u0435\u0441\u0442\u0438\u043c \u0438\u0433\u0440\u043e\u043a\u0430 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u0438\u043a \u0445\u043e\u0447\u0435\u0442 \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0441 \u043d\u0438\u043c \u0435\u0449\u0435 \u0440\u0430\u0437         await manager.send_structured_data(uuid.UUID(data_from_client['enemy_client_id']), msg_type, {})         rc = find_rival_couple_by_id(data_from_client['game_id'])          if not rc:             return          with db.session_scope() as s_:             sp: int = GameState.SHIPS_POSITIONING.value             if rc.dfplayer1 == client_uuid:                 rc.dfplayer1_state = sp             else:                 rc.dfplayer2_state = sp              s_.add(rc)  # \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0411\u0414 <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0422\u0438\u043f\u044b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439:<\/p>\n<details class=\"spoiler\">\n<summary>message_type.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">from enum import Enum   class MessageType(Enum):     GAME_CREATION = 0     SHIPS_ARE_ARRANGED = 1     PLAY = 2     FIRE_REQUEST = 3     FIRE_RESPONSE = 4     UNSUNK_SHIPS = 5     GAME_OVER = 6     DISCONNECTION = 7     PLAY_AGAIN = 8 <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u041c\u043e\u0434\u0435\u043b\u044c \u0434\u0430\u043d\u043d\u044b\u0445:<\/p>\n<details class=\"spoiler\">\n<summary>model.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">from sqlalchemy import Uuid, DateTime, ForeignKey, String, SmallInteger from sqlalchemy.orm import DeclarativeBase, mapped_column   class Base(DeclarativeBase):     pass   class TRivalCouple(Base):     __tablename__ = \"trival_couple\"      id = mapped_column(Uuid, primary_key=True)     dfplayer1 = mapped_column(Uuid, index=True)     dfplayer1_nickname = mapped_column(String(25))     dfplayer1_state = mapped_column(ForeignKey(\"tplayer_state.id\"), index=True)     dfplayer2 = mapped_column(Uuid, index=True)     dfplayer2_nickname = mapped_column(String(25))     dfplayer2_state = mapped_column(ForeignKey(\"tplayer_state.id\"), index=True)     dfgame_type = mapped_column(ForeignKey(\"tgame_type.id\"), index=True)     dfcreated_on = mapped_column(DateTime)     dffinished_on = mapped_column(DateTime)   class TGameType(Base):     __tablename__ = \"tgame_type\"      id = mapped_column(SmallInteger, primary_key=True)     dfname_en = mapped_column(String(30), unique=True)     dfname = mapped_column(String(30))   class TPlayerState(Base):     __tablename__ = \"tplayer_state\"      id = mapped_column(SmallInteger, primary_key=True)     dfname_en = mapped_column(String(30), unique=True)     dfname = mapped_column(String(30)) <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0412 \u0442\u0435\u043b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0438\u043f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f <code>MessageType<\/code> \u0438 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u043d\u0435\u0433\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f \u0442\u0435 \u0438\u043b\u0438 \u0438\u043d\u044b\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f (\u0441\u043c. \u0444\u0430\u0439\u043b controller.py \u0432\u044b\u0448\u0435, \u0432 \u043d\u0435\u043c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u043b\u043e\u0433\u0438\u043a\u0430). <\/p>\n<p>\u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0440\u043e\u043b\u044c \u0431\u044d\u043a\u0430\u2014 \u0432 \u00ab\u0441\u0432\u044f\u0437\u044b\u0432\u0430\u043d\u0438\u0438\u00bb \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u0432 \u0434\u0440\u0443\u0433 \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u043c \u0432\u043e\u00a0\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 TRivalCouple \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043c\u0435\u0436\u0434\u0443 \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c\u0438. \u0410\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u0442\u043e\u043c\u0443, \u043a\u0430\u043a\u00a0\u0435\u0441\u043b\u0438\u00a0\u0431\u044b \u0434\u0432\u0430 \u0438\u0433\u0440\u043e\u043a\u0430 \u0438\u0433\u0440\u0430\u043b\u0438 \u0432\u00a0\u043c\u043e\u0440\u0441\u043a\u043e\u0439 \u0431\u043e\u0439 \u00ab\u043d\u0430 \u0431\u0443\u043c\u0430\u0433\u0435\u00bb\u00a0\u2014 \u043e\u0434\u0438\u043d \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b, \u0430\u00a0\u0432\u0442\u043e\u0440\u043e\u0439 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u043c (\u043c\u0438\u043c\u043e\/\u0440\u0430\u043d\u0438\u043b\/\u0443\u0431\u0438\u043b).<\/p>\n<h3>\u041f\u043e\u0438\u0441\u043a \u0438 \u0441\u0432\u044f\u0437\u044b\u0432\u0430\u043d\u0438\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u0432<\/h3>\n<p>\u041f\u0440\u0438 \u043d\u0430\u0436\u0430\u0442\u0438\u0438 \u043d\u0430\u00a0\u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0418\u0433\u0440\u0430 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c\u00bb\u00a0\u2014 \u0444\u0440\u043e\u043d\u0442 \u043f\u043e\u0441\u044b\u043b\u0430\u0435\u0442 \u043d\u0430\u00a0\u0431\u044d\u043a \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441\u00a0\u0442\u0438\u043f\u043e\u043c <code>MessageType.GAME_CREATION<\/code>. \u0411\u044d\u043a \u043f\u044b\u0442\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0439\u0442\u0438 \u0437\u0430\u043f\u0438\u0441\u044c \u0432\u00a0\u0442\u0430\u0431\u043b\u0438\u0446\u0435 TRivalCouple, \u0443\u00a0\u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043e\u0434\u0438\u043d \u0438\u0433\u0440\u043e\u043a \u0443\u0436\u0435 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u0430\u00a0\u0434\u043b\u044f\u00a0\u0432\u0442\u043e\u0440\u043e\u0433\u043e \u0435\u0441\u0442\u044c \u00ab\u0441\u0432\u043e\u0431\u043e\u0434\u043d\u0430\u044f \u044f\u0447\u0435\u0439\u043a\u0430\u00bb (= null). \u0415\u0441\u043b\u0438 \u043e\u043d \u043d\u0430\u0445\u043e\u0434\u0438\u0442 \u0442\u0430\u043a\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c\u00a0\u2014 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u0433\u0440\u043e\u043a\u0430 \u0432\u00a0\u043d\u0435\u0435 \u0438 \u043e\u043f\u043e\u0432\u0435\u0449\u0430\u0435\u0442 \u0435\u0433\u043e \u043e\u043f\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u043e\u00a0\u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0435. \u0415\u0441\u043b\u0438 \u0442\u0430\u043a\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442\u00a0\u2014 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u0435\u0435 \u0438 \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0432 \u043d\u0435\u0439 \u0438\u0433\u0440\u043e\u043a\u0430:<\/p>\n<pre><code class=\"python\">if msg_type == MessageType.GAME_CREATION.value:     if data_from_client['game_type'] == GameType.RANDOM.value:          if not available_random_couple_exists():             await create_random_couple(client_uuid, data_from_client)         else:             rc: TRivalCouple = find_available_random_couple()             await add_player_to_rival_couple(rc, client_uuid, data_from_client)             await notify_enemy_about_game_creation(rc, manager)      else:  # game_type == GameType.FRIEND.value:         await process_friend_game_creation(client_uuid, data_from_client, manager)<\/code><\/pre>\n<p>\u0412 \u0441\u043b\u0443\u0447\u0430\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f \u043f\u043b\u044e\u0441-\u043c\u0438\u043d\u0443\u0441 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u0430\u044f, \u0441\u043c. \u0442\u0435\u043b\u043e \u043c\u0435\u0442\u043e\u0434\u0430 <code>process_friend_game_creation()<\/code>:<\/p>\n<details class=\"spoiler\">\n<summary>friend_game_controller.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">import uuid from datetime import datetime  from sqlalchemy import select, func, and_  import db from entities.model import TRivalCouple, TGameType from helper.game_state import GameState from helper.game_type import GameType from websocket.common import notify_enemy_about_game_creation from websocket.connection_manager import ConnectionManager   def friend_couple_exists(client_uuid: uuid.UUID, friend_uuid: uuid.UUID) -&gt; bool:     with db.session_scope() as s_:         stmt = select(func.count(TRivalCouple.id)).join(TGameType, TRivalCouple.dfgame_type.__eq__(TGameType.id),                                                         isouter=True).where(             and_(TGameType.id.__eq__(GameType.FRIEND.value), TRivalCouple.dfplayer1 == friend_uuid,                  TRivalCouple.dfplayer2 == client_uuid))         count = s_.execute(stmt).scalar()          if count == 0:             return False         return True   async def create_friend_couple(client_uuid: uuid.UUID, data_from_client: dict):     with db.session_scope() as s_:         rival_couple: TRivalCouple = TRivalCouple(             id=uuid.uuid4(),             dfplayer1=client_uuid,             dfplayer1_nickname=data_from_client['nickName'],             dfplayer1_state=GameState.SEARCHING_FOR_OPPONENT.value,             dfplayer2=data_from_client['enemy_client_id'],             dfcreated_on=datetime.now(),             dfgame_type=GameType.FRIEND.value         )          s_.add(rival_couple)   def find_friend_couple(client_uuid: uuid.UUID, friend_uuid: uuid.UUID) -&gt; TRivalCouple:     with db.session_scope() as s_:         stmt = select(TRivalCouple).join(TGameType, TRivalCouple.dfgame_type.__eq__(TGameType.id),                                          isouter=True).where(             and_(                 TGameType.id.__eq__(GameType.FRIEND.value),                 TRivalCouple.dfplayer1.__eq__(friend_uuid),                 TRivalCouple.dfplayer2.__eq__(client_uuid)             )         ).limit(1)  # \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0411\u0414 \u0441 \u0442\u0430\u043a\u0438\u043c \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u043c \u043f\u043e-\u0445\u043e\u0440\u043e\u0448\u0435\u043c\u0443 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430          return s_.scalar(stmt)   async def join_friend_couple(rc, nickname: str):     with db.session_scope() as s_:         rc.dfplayer2_nickname = nickname         rc.dfplayer1_state = GameState.SHIPS_POSITIONING.value         rc.dfplayer2_state = GameState.SHIPS_POSITIONING.value          s_.add(rc)  # add to s_.dirty for subsequent commit to DB   async def process_friend_game_creation(client_uuid: uuid.UUID, data_from_client: dict, manager: ConnectionManager):     friend_uuid: uuid.UUID = data_from_client['enemy_client_id']      if not friend_couple_exists(client_uuid, friend_uuid):         await create_friend_couple(client_uuid, data_from_client)     else:         rc: TRivalCouple = find_friend_couple(client_uuid, friend_uuid)         await join_friend_couple(rc, data_from_client['nickName'])         await notify_enemy_about_game_creation(rc, manager) <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h2>Frontend<\/h2>\n<p>\u041c\u043e\u0434\u0435\u043b\u044c \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0430 \u0442\u0440\u0435\u043c\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u043c\u0438 \u043a\u043b\u0430\u0441\u0441\u0430\u043c\u0438:<\/p>\n<details class=\"spoiler\">\n<summary>Ship.ts<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">import GameStore from \"@\/store\/index\"; import Location from \".\/Location\"; import ShipOrientation from \".\/enums\/ShipOrientation\";   export default class Ship {     private _length: number;     private _type: ShipOrientation;     private _location: Location;     private _hitsNumber: number;      constructor(_length: number, _type: ShipOrientation, _location: Location) {         this._length = _length;         this._type = _type;         this._location = _location;         this._hitsNumber = 0;     }      public get length(): number {         return this._length;     }     public set length(v: number) {         this._length = v;     }      public get type(): ShipOrientation {         return this._type;     }     public set type(v: ShipOrientation) {         this._type = v;     }      public get location(): Location {         return this._location;     }     public set location(v: Location) {         this._location = v;     }      public get hitsNumber(): number {         return this._hitsNumber;     }     public set hitsNumber(v: number) {         this._hitsNumber = v;     }      \/**      * \u0420\u0438\u0441\u0443\u0435\u0442 \u043a\u043e\u0440\u0430\u0431\u043b\u044c      *\/     public draw(ctx: CanvasRenderingContext2D | null, strokeColor: string = \"black\") {          let rectangleWidth, rectangleHeight: number;          if (ctx) {             ctx.save();             ctx.strokeStyle = strokeColor;             ctx.lineWidth = 2;              let gcw: number = GameStore.getters.getGridCellWidth;             let gch: number = GameStore.getters.getGridCellHeight;              if (this._type === ShipOrientation.Horizontal) {                 rectangleWidth = this._length * gcw;                 rectangleHeight = gch;              } else {                 rectangleWidth = gcw;                 rectangleHeight = this._length * gch;             };              this.drawBulkhead(ctx);              ctx.strokeRect(this._location.x * gcw + 1, this._location.y * gch + 1, rectangleWidth - 2, rectangleHeight - 2);             ctx.restore();         }     }     \/**      * \u0420\u0438\u0441\u0443\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u044b\u0447\u043a\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f      *\/     private drawBulkhead(ctx: CanvasRenderingContext2D | null) {         if (ctx) {             ctx.beginPath();             for (let i = 1; i &lt; this._length; i++) {                  let gcw: number = GameStore.getters.getGridCellWidth;                 let gch: number = GameStore.getters.getGridCellHeight;                  let x0 = this._location.x * gcw;                 let y0 = this._location.y * gch;                  if (this._type === ShipOrientation.Horizontal) {                     ctx.moveTo(x0 + i * gcw, y0);                     ctx.lineTo(x0 + i * gcw, y0 + gch);                 }                 else \/* if (this._type === ShipType.Vertical) *\/ {                     ctx.moveTo(x0, y0 + i * gch);                     ctx.lineTo(x0 + gcw, y0 + i * gch);                 }              };             ctx.stroke();         }     }     \/**      * \u041c\u0435\u043d\u044f\u0435\u0442 \u0442\u0438\u043f \u043a\u043e\u0440\u0430\u0431\u043b\u044f      *\/     public changeOrientation() {         if (this._type === ShipOrientation.Horizontal)             this._type = ShipOrientation.Vertical;         else             this._type = ShipOrientation.Horizontal;     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 (\u043b\u043e\u043a\u0430\u0446\u0438\u0439), \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u043e\u043c\u0443 \u043a\u043e\u0440\u0430\u0431\u043b\u044e      *\/     public getLocations(): Location[] {         \/\/ \u0421\u0440\u0430\u0437\u0443 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0443 \u043f\u0435\u0440\u0432\u043e\u0439 (\u0433\u043e\u043b\u043e\u0432\u043d\u043e\u0439) \u043a\u043b\u0435\u0442\u043a\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f         \/\/ \u0442.\u043a. \u043e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0449\u0435\u0439 \u043a\u0430\u043a \u0434\u043b\u044f \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0433\u043e, \u0442\u0430\u043a \u0438 \u0434\u043b\u044f \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u044f         let locations: Location[] = [this._location];          if (this._type === ShipOrientation.Horizontal)             for (let i = 1; i &lt; this._length; i++)                 locations.push(new Location(this._location.x + i, this._location.y));         else             for (let i = 1; i &lt; this._length; i++)                 locations.push(new Location(this._location.x, this._location.y + i));          return locations;     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0442\u043e\u0440\u0446\u0435\u0432\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f (\u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u0438\u0445 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u0438\u0442\u044c)      *\/     public static async getFrontAndBackLocations(length: number, loc_x: number, loc_y: number, shipType: number) {         let locs: Location[] = [];          if (length === 1) {             \/\/ \u0435\u0441\u043b\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043e\u0434\u043d\u043e\u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0439, \u0442\u043e \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u0441\u043c\u0435\u0436\u043d\u044b\u0435 (\u043d\u0435\u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435) \u043b\u043e\u043a\u0430\u0446\u0438\u0438             for (let i = -1; i &lt;= 1; i++)                 for (let j = -1; j &lt;= 1; j++) {                      let neighborX = loc_x + i;                     let neighborY = loc_y + j;                      \/\/ \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0447\u0442\u043e \u043b\u043e\u043a\u0430\u0446\u0438\u0438 \u041d\u0415\u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0438 \u043d\u0435 \u0432\u044b\u0445\u043e\u0434\u044f\u0442 \u0437\u0430 \u0440\u0430\u043c\u043a\u0438 \u0433\u0440\u0438\u0434\u0430                     if ((i * i + j * j !== 1) || neighborX &lt; 0 || neighborY &lt; 0 || neighborX &gt; 9 || neighborY &gt; 9)                         continue;                      locs.push(new Location(neighborX, neighborY));                 }         }         else {              if (shipType === ShipOrientation.Horizontal) {                 let leftLoc = new Location(loc_x - 1, loc_y);                 let rightLoc = new Location(loc_x + length, loc_y);                  if (leftLoc.x &gt;= 0) locs.push(leftLoc);                 if (rightLoc.x &lt;= 9) locs.push(rightLoc);             }             else {                 let topLoc = new Location(loc_x, loc_y - 1);                 let bottomLoc = new Location(loc_x, loc_y + length);                  if (topLoc.y &gt;= 0) locs.push(topLoc);                 if (bottomLoc.y &lt;= 9) locs.push(bottomLoc);             }         }         return locs;     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true \u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u0439 \u043e\u0431\u043e\u0438\u0445 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439, \u0435\u0441\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0435\u0442\u0441\u044f \u0441 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u043c ship, \u0438\u043d\u0430\u0447\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 false      *\/     public isIntersect(ship: Ship): [boolean, Location | undefined, Location | undefined] {         for (const loc1 of this.getLocations())             for (const loc2 of ship.getLocations())                 if (Math.abs(loc1.x - loc2.x) &lt;= 1 &amp;&amp; Math.abs(loc1.y - loc2.y) &lt;= 1)                     return [true, loc1, loc2];          return [false, undefined, undefined];     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u041a\u043e\u0440\u0430\u0431\u043b\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0441\u0435\u0442\u043a\u0435 \u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0442\u0440\u0435\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432: <code>_location<\/code> \u2014 \u043b\u043e\u043a\u0430\u0446\u0438\u044f \u043f\u0435\u0440\u0432\u043e\u0439 \u043a\u043b\u0435\u0442\u043a\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0430-2),  <code>_type<\/code> \u2014 \u0442\u0438\u043f \u043a\u043e\u0440\u0430\u0431\u043b\u044f (\u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439\/\u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439) \u0438 <code>_length<\/code> \u2014 \u0434\u043b\u0438\u043d\u0430;<\/p>\n<p>\u041a\u043b\u0430\u0441\u0441 \u043b\u043e\u043a\u0430\u0446\u0438\u044f:<\/p>\n<details class=\"spoiler\">\n<summary>Location.ts<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">import { GameStore } from \"@\/store\/modules\/GameStore\"; import HighlightType from \".\/enums\/HighlightType\"; import { letterDict } from \"@\/helpers\/LetterDict\";   export default class Location {     private _x: number;     private _y: number;      constructor(_x: number, _y: number) {         this._x = _x;         this._y = _y;     }      public get x(): number {         return this._x;     }     public set x(v: number) {         this._x = v;     }     public get y(): number {         return this._y;     }     public set y(v: number) {         this._y = v;     }      \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043b\u043e\u043a\u0430\u0446\u0438\u044e \u043f\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0435 \u0438\u0433\u0440\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044f      *\/     public static getLocationByOffsetXY(offsetX: number, offsetY: number): Location {          const st = GameStore.state;         let gcw: number = GameStore.getters.getGridCellWidth(st);         let gch: number = GameStore.getters.getGridCellHeight(st);          let currentX = Math.floor(offsetX \/ gcw);         let currentY = Math.floor(offsetY \/ gch);          return new Location(currentX, currentY);     }     \/**      * \u041f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430 \u043a\u0430\u043d\u0432\u0430\u0441\u0435 \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u0446\u0438\u0438      *\/     public async highlight(ctx: CanvasRenderingContext2D | null, highlightType: HighlightType = HighlightType.CIRCLE) {         if (ctx) {              const st = GameStore.state;             let gcw: number = GameStore.getters.getGridCellWidth(st);             let gch: number = GameStore.getters.getGridCellHeight(st);              ctx.save();              if (highlightType === HighlightType.SQUARE) {                 ctx.fillStyle = \"rgb(229 22 35)\";                 ctx.fillRect(this._x * gcw + 3, this._y * gch + 3, gcw - 6, gch - 6);             }             else if (highlightType === HighlightType.CIRCLE) {                 ctx.fillStyle = \"rgb(33 22 235)\";                  const circle = new Path2D();                 let sp = GameStore.state.scaleParameter;                  circle.arc(this._x * gcw + 0.5 * gcw, this._y * gch + 0.5 * gch, 5 * sp, 0, 2 * Math.PI);                 ctx.fill(circle);             } else {                 \/\/ \u0420\u0438\u0441\u0443\u0435\u043c \u043a\u0440\u0435\u0441\u0442\u0438\u043a                 ctx.strokeStyle = \"rgb(229 22 35)\";                 ctx.lineWidth = 3;                  ctx.beginPath();                 ctx.moveTo(this._x * gcw + 5, this._y * gch + 5);                 ctx.lineTo((this._x + 1) * gcw - 5, (this._y + 1) * gch - 5);                  ctx.moveTo((this._x + 1) * gcw - 5, this._y * gch + 5);                 ctx.lineTo(this._x * gcw + 5, (this._y + 1) * gch - 5);                  ctx.stroke();             }              ctx.restore();         }     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true, \u0435\u0441\u043b\u0438 \u043b\u043e\u043a\u0430\u0446\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u043c\u0438 \u0441\u0435\u0442\u043a\u0438, \u0438\u043d\u0430\u0447\u0435 false      *\/     public outsideTheGrid(): boolean {         return this._x &lt; 0 || this._x &gt; 9 || this._y &lt; 0 || this._y &gt; 9     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true, \u0435\u0441\u043b\u0438 \u043b\u043e\u043a\u0430\u0446\u0438\u044f \u0432\u0430\u043b\u0438\u0434\u043d\u0430, \u0438\u043d\u0430\u0447\u0435 false      *\/     public isValid(): boolean {         \/\/ \u0411\u044b\u0432\u0430\u044e\u0442 \u0441\u043b\u0443\u0447\u0430\u0438, \u043a\u043e\u0433\u0434\u0430 \u0438\u0433\u0440\u043e\u043a \u0434\u0435\u043b\u0430\u0435\u0442 \u0432\u044b\u0441\u0442\u0440\u0435\u043b \u043f\u043e \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u0446\u0438\u0438          \/\/ (\u0432\u043d\u0435 \u0433\u0440\u0430\u043d\u0438\u0446 \u0441\u0435\u0442\u043a\u0438, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0435 \u0430-0)         return (this._x &gt;= 0 &amp;&amp; this._x &lt;= 9 &amp;&amp; this._y &gt;= 0 &amp;&amp; this._y &lt;= 9)     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0443 \u0432 \u0435\u0435 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u043c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0438, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 (\u043a-7)      *\/     public toString = () =&gt; `(${letterDict[this._x]}-${this._y + 1})`; }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u041a\u043b\u0430\u0441\u0441 Game:<\/p>\n<details class=\"spoiler\">\n<summary>Game.ts<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">import Ship from \".\/Ship\"; import ShipOrientation from \".\/enums\/ShipOrientation\"; import Location from \".\/Location\"; import { GameStore } from \"@\/store\/modules\/GameStore\";  export default class Game {      private static ships: Ship[];     private static shotHistory: Location[] = [];       \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0440\u0430\u0431\u043b\u0438      *\/     public static getShips(): Ship[] {         return Game.ships;     }     \/**      * createDefaultShips     *\/     public static createInitialShips() {         Game.ships = [             new Ship(1, ShipOrientation.Horizontal, new Location(8, 2)),             new Ship(1, ShipOrientation.Horizontal, new Location(0, 9)),             new Ship(1, ShipOrientation.Horizontal, new Location(7, 5)),             new Ship(1, ShipOrientation.Horizontal, new Location(8, 8)),              new Ship(2, ShipOrientation.Horizontal, new Location(0, 0)),             new Ship(2, ShipOrientation.Vertical, new Location(4, 3)),             new Ship(2, ShipOrientation.Vertical, new Location(1, 2)),              new Ship(3, ShipOrientation.Horizontal, new Location(5, 0)),             new Ship(3, ShipOrientation.Horizontal, new Location(4, 9)),              new Ship(4, ShipOrientation.Vertical, new Location(2, 6))         ]     }     \/**      * \u041e\u0447\u0438\u0449\u0430\u0435\u0442 \u043a\u0430\u043d\u0432\u0430\u0441, \u0437\u0430\u043d\u043e\u0432\u043e \u0440\u0438\u0441\u0443\u0435\u0442 \u0441\u0435\u0442\u043a\u0443 \u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0438     *\/     public static refreshGridAndShips() {         const st = GameStore.state;          const ctx: CanvasRenderingContext2D = GameStore.getters.getContext2D(st);         const hostileCtx: CanvasRenderingContext2D = GameStore.getters.getHostileContext2D(st);          Game.clearGrid(ctx);         Game.clearGrid(hostileCtx);          Game.makeGrid(ctx);         Game.makeGrid(hostileCtx);          Game.createInitialShips();          Game.ships.forEach(s =&gt; { s.draw(ctx); });     }     \/**      * \u041e\u0447\u0438\u0449\u0430\u0435\u0442 \u043a\u0430\u043d\u0432\u0430\u0441     *\/     private static clearGrid(ctx: CanvasRenderingContext2D) {         ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true, \u0435\u0441\u043b\u0438 \u043b\u043e\u043a\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u0438\u0441\u0442\u043e\u0440\u0438 \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u043e\u0432 (shotHistory), \u0438\u043d\u0430\u0447\u0435 false      *\/     public static existsInShotHistory(location: Location): boolean {         return this.containsLocation(location, Game.shotHistory);     }     \/**      * \u0420\u0430\u0441\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u043d\u0430 \u0441\u0435\u0442\u043a\u0435 (\u0412 \u0420\u0410\u0417\u0420\u0410\u0411\u041e\u0422\u041a\u0415)      *\/     private static createInitialRandomShips() {      }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true, \u0435\u0441\u043b\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e (\u043d\u0438 \u043e\u0434\u0438\u043d \u0438\u0437 \u043d\u0438\u0445 \u043d\u0435 \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0435\u0442\u0441\u044f \u0441\u043e \u0432\u0441\u0435\u043c\u0438 \u0434\u0440\u0443\u0433\u0438\u043c\u0438),      * \u0438\u043d\u0430\u0447\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 false \u0438 \u043c\u0430\u0441\u0441\u0438\u0432 \u0441 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u0439      *\/     public static isArrangementCorrect(): [boolean, Location[] | undefined] {          let intersections: Location[] = [];          for (let i = 0; i &lt; Game.ships.length - 1; i++) {             const outerShip = Game.ships[i];              for (let j = i + 1; j &lt; Game.ships.length; j++) {                 const innerShip = Game.ships[j];                 const res = outerShip.isIntersect(innerShip);                 if (res[0])                     Array.prototype.push.apply(intersections, [res[1], res[2]]);             };         };          if (intersections.length &gt; 0)             return [false, intersections];          return [true, undefined];     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u043e \u0435\u0433\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c      *\/     public static getShipByHeadLocation(location: Location): Ship | undefined {         return Game.ships.find(s =&gt; (s.location.x === location.x &amp;&amp; s.location.y === location.y));     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u043e \u0434\u0430\u043d\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u0446\u0438\u0438      *\/     public static getShipByLocation(location: Location): Ship | undefined {          let notDestroyedShips: Ship[] = Game.ships.filter(s =&gt; s.hitsNumber &lt; s.length);          for (const ship of notDestroyedShips)             if (Game.containsLocation(location, ship.getLocations()))                 return ship;          return undefined;     }     \/**      * \u0420\u0438\u0441\u0443\u0435\u0442 \u0441\u0435\u0442\u043a\u0443 \u043d\u0430 \u043a\u0430\u043d\u0432\u0435\u0441\u0435      *\/     public static makeGrid(ctx: CanvasRenderingContext2D) {         ctx.save();         ctx.beginPath();          ctx.lineWidth = GameStore.state.gridLineThickness;         ctx.setLineDash([3, 3]);          const st = GameStore.state;          let cw: number = GameStore.getters.getCanvasWidth(st);         let ch: number = GameStore.getters.getCanvasHeight(st);         let gcw: number = GameStore.getters.getGridCellWidth(st);         let gch: number = GameStore.getters.getGridCellHeight(st);          for (let x = 0; x &lt;= cw; x += gcw) {             ctx.moveTo(x, 0);             ctx.lineTo(x, ch);         };          for (let y = 0; y &lt;= ch; y += gch) {             ctx.moveTo(0, y);             ctx.lineTo(cw, y);         };          ctx.stroke();         ctx.restore();     }     \/**      * \u0420\u0438\u0441\u0443\u0435\u0442 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u043d\u0430 \u043a\u0430\u043d\u0432\u0430\u0441\u0435      *\/     public static drawShips(ctx: CanvasRenderingContext2D): void {         ctx.save();          const st = GameStore.state;         let cw: number = GameStore.getters.getCanvasWidth(st);         let ch: number = GameStore.getters.getCanvasHeight(st);          ctx.clearRect(0, 0, cw, ch);         this.makeGrid(ctx);         Game.ships.forEach(ship =&gt; ship.draw(ctx));         ctx.restore();     }     \/**     * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true, \u0435\u0441\u043b\u0438 \u043c\u0430\u0441\u0441\u0438\u0432 locations \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043b\u043e\u043a\u0430\u0446\u0438\u044e loc, \u0438\u043d\u0430\u0447\u0435 false     *\/     public static containsLocation(loc: Location, locations: Location[]): boolean {         for (const l of locations)             if (l.x === loc.x &amp;&amp; l.y === loc.y)                 return true;         return false;     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0435 \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438 \u043f\u043e \u043e\u0442\u043d\u043e\u0448\u0435\u043d\u0438\u044e \u043a \u0434\u0430\u043d\u043d\u043e\u0439      *\/     public static getDiagonalLocations(location: Location): Location[] {          let diagonalLocs: Location[] = [];          for (let i = -1; i &lt;= 1; i++) {             for (let j = -1; j &lt;= 1; j++) {                  let neighborX = location.x + i;                 let neighborY = location.y + j;                  if (i === 0 || j === 0 || neighborX &lt; 0 || neighborY &lt; 0 || neighborX &gt; 9 || neighborY &gt; 9)                     continue;                  diagonalLocs.push(new Location(neighborX, neighborY));             }         }          return diagonalLocs;     }     \/**      * \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 true, \u0435\u0441\u043b\u0438 \u0432\u0441\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u044b, \u0438\u043d\u0430\u0447\u0435 false      *\/     public static allShipsAreSunk(): boolean {         return Game.ships.every(ship =&gt; ship.length === ship.hitsNumber);     }     \/**      * \u041f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438 \u0438, \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e, \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u0445 \u0432 \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u043e\u0432      *\/     public static async highlightDiagonals(         ctx: CanvasRenderingContext2D,         diagonals: Location[]     ) {         for (const loc of diagonals)             await loc.highlight(ctx);     }     \/**      * \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043b\u043e\u043a\u0430\u0446\u0438\u044e \u0432 \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u043e\u0432      *\/     public static addToShotHistory(shot: Location) {         if (!Game.containsLocation(shot, Game.shotHistory))             Game.shotHistory.push(shot);     }     \/**      * \u041e\u0447\u0438\u0449\u0430\u0435\u0442 \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u043e\u0432      *\/     public static async clearShotHistory() {         Game.shotHistory = [];     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 WS \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0444\u0440\u043e\u043d\u0442\u0430 \u0442\u0430\u043a\u0436\u0435 \u0431\u044b\u043b \u0441\u0434\u0435\u043b\u0430\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043a\u043b\u0430\u0441\u0441:<\/p>\n<details class=\"spoiler\">\n<summary>WebSocketManager.ts<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">import { GameCreationBodyType } from \"@\/model\/WSDataTransferRoot\"; import GameProcessManager from \".\/GameProcessManager\";  export default class WebSocketManager {      private static ws: WebSocket;     private static readonly WS_SERVER_HOST: string = process.env.VUE_APP_WS_SERVER_HOST;     private static readonly WS_SERVER_PORT: number = 5000;      public static createWebSocket(clientUUID: string): WebSocket {          let host = WebSocketManager.WS_SERVER_HOST;         let port = WebSocketManager.WS_SERVER_PORT;          let socket: WebSocket = new WebSocket(`ws:\/\/${host}:${port}\/client\/${clientUUID}\/ws`);          return socket;     }     public static setupWSAndCreateGameOnOpen(ws: WebSocket, gameCreationBody: GameCreationBodyType) {          ws.onopen = function (event) {             console.log(\"Opening a connection...\");             WebSocketManager.ws = ws;             ws.send(JSON.stringify(gameCreationBody));             console.log(\"Successfully connected to the websocket server\");         };          ws.onmessage = async function (event: MessageEvent&lt;string&gt;) {             await GameProcessManager.processData(event.data);         };          ws.onerror = function (event: Event) {             console.log(\"Connection error\");         };                  ws.onclose = function (event: CloseEvent) {             if (event.wasClean) {                 console.log(\"Connection closed correctly\");             } else {                 console.error(\"The connection was broken\");             }         };     }     public static getWebSocket(): WebSocket {         return WebSocketManager.ws;     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u043b\u043e\u0433\u0438\u043a\u0430 \u043f\u043e \u043e\u0431\u0449\u0435\u043d\u0438\u044e \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043a\u043b\u0430\u0441\u0441\u0435:<\/p>\n<details class=\"spoiler\">\n<summary>GameProcessManager.ts<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"typescript\">import GameState from \"@\/model\/enums\/GameState\"; import GameType from \"@\/model\/enums\/GameType\"; import MessageType from \"@\/model\/enums\/MessageType\"; import Game from \"@\/model\/Game\"; import Location from \"@\/model\/Location\"; import { FireResponseType, GameCreationBodyType, ShipType, TransferLevel2Type, UnSunkShipsType, WSDataTransferRootType } from \"@\/model\/WSDataTransferRoot\"; import GameStore from \"@\/store\/index\"; import WebSocketManager from \".\/WebSocketManager\"; import HighlightType from \"@\/model\/enums\/HighlightType\"; import ShotResult from \"@\/model\/enums\/ShotResult\"; import Ship from \"@\/model\/Ship\"; import UIHandler from \"@\/helpers\/UIHandler\";  export default class GameProcessManager {      private static enemyClientUuid: string;     private static gameId: string;      public static getEnemyUUID() {         return GameProcessManager.enemyClientUuid;     }     public static getGameId() {         return GameProcessManager.gameId;     }     public static async processData(dataFromServer: string) {         \/\/ console.log('Data from server was received: ', dataFromServer);          let parsedData: WSDataTransferRootType = JSON.parse(dataFromServer);          switch (parsedData.msg_type) {             case MessageType.GAME_CREATION:                 console.log('MessageType.GAME_CREATION data:', parsedData);                 if (parsedData.is_status_ok)                     GameProcessManager.processGameCreation(parsedData.data);                 break;              case MessageType.SHIPS_ARE_ARRANGED:                 console.log('MessageType.SHIPS_ARE_ARRANGED data:', parsedData);                 if (parsedData.is_status_ok)                     GameProcessManager.processShipArrangement(parsedData.data);                 break;              case MessageType.PLAY:                 console.log('MessageType.PLAY data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processStartingToPlay(parsedData.data);                 break;              case MessageType.FIRE_REQUEST:                 console.log('MessageType.FIRE_REQUEST data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processFireRequest(parsedData.data);                 break;              case MessageType.FIRE_RESPONSE:                 console.log('MessageType.FIRE_RESPONSE data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processFireResponse(parsedData.data);                 break;              case MessageType.GAME_OVER:                 console.log('MessageType.GAME_OVER data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processGameOver(parsedData.data);                 break;              case MessageType.UNSUNK_SHIPS:                 console.log('MessageType.UNSUNK_SHIPS data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processUnsunkShips(parsedData.data);                 break;              case MessageType.DISCONNECTION:                 console.log('MessageType.DISCONNECTION data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processDisconnection(parsedData.data);                 break;              case MessageType.PLAY_AGAIN:                 console.log('MessageType.PLAY_AGAIN data:', parsedData);                 if (parsedData.is_status_ok)                     await GameProcessManager.processPlayAgain(parsedData.data);                 break;              default:                 break;         }     }      public static getGameCreationBody(gameType: GameType, nickName: string, enemy_client_id: string = \"\"): GameCreationBodyType {          const gameCreationBody: GameCreationBodyType = {             msg_type: MessageType.GAME_CREATION,             game_type: gameType,             nickName: nickName,         };          if (gameType === GameType.FRIEND &amp;&amp; enemy_client_id)             gameCreationBody.enemy_client_id = enemy_client_id;          return gameCreationBody;     }     public static processGameCreation(data: TransferLevel2Type) {         GameProcessManager.gameId = data.gameId;         GameStore.commit(\"setMyState\", GameState.SHIPS_POSITIONING);         GameStore.commit(\"setEnemyState\", GameState.SHIPS_POSITIONING);         GameStore.commit(\"setEnemyNickname\", data.enemy_nickname);     }     private static processShipArrangement(data: TransferLevel2Type) {         GameStore.commit(\"setEnemyState\", GameState.SHIPS_ARE_ARRANGED);         GameProcessManager.enemyClientUuid = data.enemy_client_id;     }     private static async processStartingToPlay(data: TransferLevel2Type) {         GameStore.commit(\"setEnemyState\", GameState.PLAYING);         GameStore.commit(\"setMyState\", GameState.PLAYING);         GameStore.commit(\"setMyTurnToShoot\", data.turn_to_shoot);          if (data.turn_to_shoot) await GameStore.dispatch(\"enableShooting\");     }     private static async processFireRequest(data: TransferLevel2Type) {          let shot: Location = new Location(             data.shot_location._x,             data.shot_location._y         );          let ht: HighlightType = HighlightType.CIRCLE;          let ctx = GameStore.getters.getContext2D;          let fireResponse: FireResponseType = {             msg_type: MessageType.FIRE_RESPONSE,             shot_result: ShotResult.MISS,             enemy_client_id: GameProcessManager.enemyClientUuid,             shot_location: data.shot_location,         };          let ship: Ship | undefined = Game.getShipByLocation(shot);          \/\/ \u0435\u0441\u043b\u0438 \u043d\u0430\u0448 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u0440\u0430\u043d\u0438\u043b\u0438         if (ship) {             ht = HighlightType.CROSS; \/\/ \u043c\u0435\u043d\u044f\u0435\u043c \u0442\u0438\u043f \u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043d\u0430 \"\u043a\u0440\u0435\u0441\u0442\"             ship.hitsNumber++; \/\/ \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u043c \u0441\u0447\u0435\u0442\u0447\u0438\u043a \u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443 \u043f\u043e\u0434\u0431\u0438\u0442\u043e\u0433\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u044f              \/\/ \u043d\u0430\u0445\u043e\u0434\u0438\u043c \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438             let diags: Location[] = Game.getDiagonalLocations(shot);             \/\/ \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u043c \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438             await Game.highlightDiagonals(ctx, diags);              \/\/ \u0435\u0441\u043b\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u043d\u0435\u043d             if (ship.hitsNumber &lt; ship.length) {                 fireResponse.shot_result = ShotResult.HIT;             } else {                 \/\/ \u0435\u0441\u043b\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d                 fireResponse.shot_result = ShotResult.SUNK;                  \/\/ \u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u043c \u043f\u043e\u0434\u0431\u0438\u0442\u044b\u0439 \u043a\u043e\u0440\u0430\u0431\u043b\u044c                 let ss: ShipType = {                     length: ship.length,                     loc: { _x: ship.location.x, _y: ship.location.y },                     type: ship.type,                 };                  fireResponse.sunkShip = ss;                 \/\/ \u043d\u0430\u0445\u043e\u0434\u0438\u043c \u0431\u043e\u043a\u043e\u0432\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438                 let edgeLocs = await Ship.getFrontAndBackLocations(                     ss.length,                     ss.loc._x,                     ss.loc._y,                     ss.type                 );                  \/\/ \u0432\u044b\u0434\u0435\u043b\u044f\u0435\u043c \u0438\u0445 \u043d\u0430 \u0441\u0432\u043e\u0435\u043c \u0433\u0440\u0438\u0434\u0435                 for (const loc of edgeLocs) await loc.highlight(ctx);                  \/\/ \u0415\u0441\u043b\u0438 \u0432\u0441\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u044b, \u0434\u0430\u0435\u043c \u0437\u043d\u0430\u0442\u044c \u043e\u0431 \u044d\u0442\u043e\u043c \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u0438\u043a\u0443. \u0418\u0433\u0440\u0430 \u043e\u043a\u043e\u043d\u0447\u0435\u043d\u0430!                 if (Game.allShipsAreSunk()) {                     GameStore.commit(\"setMyState\", GameState.GAME_IS_OVER);                     GameStore.commit(\"setEnemyState\", GameState.GAME_IS_OVER);                     \/\/ \u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0443 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u0438\u0433\u0440\u044b \u0441 \u0442\u0438\u043f\u043e\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f GAME_OVER                     const ws: WebSocket = WebSocketManager.getWebSocket();                     ws.send(                         JSON.stringify({                             msg_type: MessageType.GAME_OVER,                             enemy_client_id: GameProcessManager.enemyClientUuid,                         })                     );                 }             }             await GameStore.dispatch(\"disableShooting\");         } else await GameStore.dispatch(\"enableShooting\");          await shot.highlight(ctx, ht);          \/\/ \u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0443 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u043f\u0430\u0434\u0430\u043d\u0438\u0438 (\u043c\u0438\u043c\u043e\/\u0440\u0430\u043d\u0438\u043b\/\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d)         \/\/ \u0441 \u0442\u0438\u043f\u043e\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f FIRE_RESPONSE         const ws: WebSocket = WebSocketManager.getWebSocket();         ws.send(JSON.stringify(fireResponse));          GameStore.commit(\"setEnemyShotHint\", shot.toString());     }     private static async processFireResponse(data: TransferLevel2Type) {         let hostileCtx = GameStore.getters.getHostileContext2D;          let shot: Location = new Location(data.shot_location._x, data.shot_location._y);         Game.addToShotHistory(shot);          if (data.shot_result === ShotResult.MISS) {             \/\/ \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u043c\u0430\u0445\u043d\u0443\u043b\u0438\u0441\u044c             await shot.highlight(hostileCtx);             GameStore.commit(\"setEnemyShotHint\", \"\");             await GameStore.dispatch(\"disableShooting\");         } else {             \/\/ \u0435\u0441\u043b\u0438 \u043f\u043e\u043f\u0430\u043b\u0438 (\u0440\u0430\u043d\u0438\u043b\u0438), \u0442\u043e             \/\/ \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u043c \u043b\u043e\u043a\u0430\u0446\u0438\u044e \u043d\u0430 \u0432\u0440\u0430\u0436\u0435\u0441\u043a\u043e\u043c \u0433\u0440\u0438\u0434\u0435             await shot.highlight(hostileCtx, HighlightType.CROSS);             \/\/ \u043d\u0430\u0445\u043e\u0434\u0438\u043c \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438             let diags: Location[] = Game.getDiagonalLocations(shot);             \/\/ \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u043c \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438             await Game.highlightDiagonals(hostileCtx, diags);             \/\/ \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0434\u0438\u0430\u0433\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438 \u0432 \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u043e\u0432             for (const loc of diags) Game.addToShotHistory(loc);              \/\/ \u0435\u0441\u043b\u0438 \u043e\u0442 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043f\u0440\u0438\u0448\u043b\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u0447\u0442\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d             if (data.shot_result === ShotResult.SUNK) {                 let sunkShip = data.sunkShip;                 if (sunkShip) {                     \/\/ \u043d\u0430\u0445\u043e\u0434\u0438\u043c \u0431\u043e\u043a\u043e\u0432\u044b\u0435 \u043b\u043e\u043a\u0430\u0446\u0438\u0438                     let edgeLocs = await Ship.getFrontAndBackLocations(                         sunkShip.length,                         sunkShip.loc._x,                         sunkShip.loc._y,                         sunkShip.type                     );                      \/\/ \u0432\u044b\u0434\u0435\u043b\u044f\u0435\u043c \u0438\u0445 \u043d\u0430 \u0433\u0440\u0438\u0434\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430                     for (const loc of edgeLocs) await loc.highlight(hostileCtx);                     \/\/ \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0432 \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u043e\u0432                     for (const loc of edgeLocs) Game.addToShotHistory(loc);                 }             }         }     }     private static async processGameOver(data: TransferLevel2Type) {         GameStore.commit(\"setMyState\", GameState.GAME_IS_OVER);         GameStore.commit(\"setEnemyState\", GameState.GAME_IS_OVER);         GameStore.commit(\"setIsWinner\", true);          await GameStore.dispatch(\"disableShooting\");         \/\/ \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0443 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u0440\u0430\u0431\u043b\u044f\u0445         await this.sendUnsunkShipsToEnemy();     }     private static async processUnsunkShips(data: TransferLevel2Type) {         let unsunkShips: Ship[] = data.unSunkShips.map(             (s) =&gt;                 new Ship(s.length, s.type, new Location(s.loc._x, s.loc._y))         );          \/\/ \u041f\u043e\u043a\u0430\u0436\u0435\u043c \u0433\u0434\u0435 \u0443 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043e\u0441\u0442\u0430\u043b\u0438\u0441\u044c \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0438         let hostileCtx = GameStore.getters.getHostileContext2D;         unsunkShips.forEach((ship) =&gt; ship.draw(hostileCtx, \"red\"));     }     private static async processDisconnection(data: TransferLevel2Type) {         UIHandler.showAlert(             \"\u041a \u0441\u043e\u0436\u0430\u043b\u0435\u043d\u0438\u044e, \u0432\u0430\u0448 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u0440\u0430\u0437\u043e\u0440\u0432\u0430\u043b \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0438 \u0432\u044b\u0448\u0435\u043b \u0438\u0437 \u0438\u0433\u0440\u044b. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0434\u043b\u044f \u043d\u043e\u0432\u043e\u0439 \u0438\u0433\u0440\u044b\",             \"danger\",             10000         );         GameStore.commit(\"setMyState\", GameState.NOT_CREATED);         GameStore.commit(\"setEnemyState\", GameState.NOT_CREATED);     }     private static async processPlayAgain(data: TransferLevel2Type) {         UIHandler.showAlert(             \"\u0421\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u0445\u043e\u0447\u0435\u0442 \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0441 \u0412\u0430\u043c\u0438 \u0441\u043d\u043e\u0432\u0430\",             \"success\",             15000         );         GameStore.commit(\"setEnemyState\", GameState.SHIPS_POSITIONING);     }      public static handleHostileGridClick(event: MouseEvent) {         let shotLocation: Location = Location.getLocationByOffsetXY(             event.offsetX,             event.offsetY         );          if (!shotLocation.isValid()) return;          if (Game.existsInShotHistory(shotLocation)) {             UIHandler.showAlert(\"\u0412\u044b \u0443\u0436\u0435 \u0441\u0442\u0440\u0435\u043b\u044f\u043b\u0438 \u0441\u044e\u0434\u0430\", \"warning\");             return;         }          const ws: WebSocket = WebSocketManager.getWebSocket();          ws.send(             JSON.stringify({                 msg_type: MessageType.FIRE_REQUEST,                 shot_location: shotLocation,                 enemy_client_id: GameProcessManager.enemyClientUuid,             })         );     }     private static async sendUnsunkShipsToEnemy() {         const ws: WebSocket = WebSocketManager.getWebSocket();          let unsunkShipsResp: UnSunkShipsType = {             msg_type: MessageType.UNSUNK_SHIPS,             enemy_client_id: GameProcessManager.enemyClientUuid,             unSunkShips: [],         };          let unSunkShips: Ship[] = Game.getShips().filter(             (s) =&gt; s.hitsNumber &lt; s.length         );          for (const ship of unSunkShips) {             unsunkShipsResp.unSunkShips.push({                 loc: { _x: ship.location.x, _y: ship.location.y },                 length: ship.length,                 type: ship.type,             });         }          ws.send(JSON.stringify(unsunkShipsResp));     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u0438\u0433\u0440\u044b \u0444\u0440\u043e\u043d\u0442 \u043f\u043e\u0441\u044b\u043b\u0430\u0435\u0442 \u043d\u0430 \u0431\u044d\u043a \u0442\u0435\u043b\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0441 \u0442\u0438\u043f\u043e\u043c FIRE_REQUEST, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0442\u0435\u043c \u0441\u0430\u043c\u044b\u043c \u0432\u044b\u0441\u0442\u0440\u0435\u043b \u043f\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c, \u0431\u044d\u043a \u043f\u0435\u0440\u0435\u0441\u044b\u043b\u0430\u0435\u0442 \u0435\u0433\u043e \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0443. \u0421\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u043e\u0442\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0435\u0433\u043e \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u043f\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u043e\u0442\u0432\u0435\u0442 \u0441 \u0442\u0438\u043f\u043e\u043c FIRE_RESPONSE, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u0430 (\u043c\u0438\u043c\u043e\/\u0440\u0430\u043d\u0438\u043b\/\u0443\u0431\u0438\u043b).<\/p>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<p>\u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430 \u043f\u0440\u043e\u0447\u0442\u0435\u043d\u0438\u0435!<\/p>\n<p>\u0413\u043e\u0442\u043e\u0432 \u043e\u0442\u0432\u0435\u0442\u0438\u0442\u044c \u043d\u0430 \u0412\u0430\u0448\u0438 \u0432\u043e\u043f\u0440\u043e\u0441\u044b. \u0412\u0441\u0435\u043c \u0434\u043e\u0431\u0440\u0430!<\/p>\n<p>\u0421\u0441\u044b\u043b\u043a\u0438 \u043d\u0430 \u0440\u0435\u043f\u044b \u0432 \u0433\u0438\u0442:<\/p>\n<p><a href=\"https:\/\/github.com\/greenDev7\/battleship\" rel=\"noopener noreferrer nofollow\">frontend<\/a><\/p>\n<p><a href=\"https:\/\/github.com\/greenDev7\/battleship-back\" rel=\"noopener noreferrer nofollow\">backend<\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/874188\/\"> https:\/\/habr.com\/ru\/articles\/874188\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440!<\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043f\u043e\u043f\u0443\u043b\u044f\u0440\u043d\u043e\u0439 \u0438\u0433\u0440\u044b. \u041c\u043e\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0438\u0433\u0440\u0430\u0442\u044c \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0438 \u0441\u0438\u043b\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c.<\/p>\n<p><a href=\"http:\/\/91.245.227.98\/\" rel=\"noopener noreferrer nofollow\">\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u0438\u0433\u0440\u0443<\/a><\/p>\n<p>\u0418\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0438 \u0432 \u043a\u043e\u043d\u0446\u0435 \u0441\u0442\u0430\u0442\u044c\u0438.<\/p>\n<p>\u0422\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0441\u0442\u044d\u043a:<\/p>\n<ul>\n<li>\n<p>Frontend: VueJS [Options API] (TypeScript, Vuex, Canvas, Bootstrap, Nginx)<\/p>\n<\/li>\n<li>\n<p>Backend: Python (FastAPI, Postgres, SQLAlchemy, Websockets)<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u0432\u0435\u0440\u043d\u0443\u0442\u043e \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043c\u0438 docker-compose.<\/p>\n<p>\u0420\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e \u0441\u0430\u043c\u0443 \u0438\u0433\u0440\u0443 \u0432 \u043c\u043e\u0440\u0441\u043a\u043e\u0439 \u0431\u043e\u0439 \u044f \u043d\u0435 \u0431\u0443\u0434\u0443. \u041a\u0430\u0436\u0434\u044b\u0439 \u0438\u0437 \u043d\u0430\u0441 \u043d\u0430\u0432\u0435\u0440\u043d\u044f\u043a\u0430 \u0438\u0433\u0440\u0430\u043b \u0432 \u043d\u0435\u0435 \u0432 \u0442\u043e\u043c \u0438\u043b\u0438 \u0438\u043d\u043e\u043c \u0432\u043e\u0437\u0440\u0430\u0441\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u0436\u0435 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e \u043f\u0440\u043e \u043c\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043e \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0434\u0435\u0442\u0430\u043b\u044f\u0445 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0442.\u0434.<\/p>\n<p>\u041e\u0442 \u0432\u0430\u0441 \u0436\u0434\u0443 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043a\u0440\u0438\u0442\u0438\u043a\u0438 \u0438 \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0445 \u0441\u043e\u0432\u0435\u0442\u043e\u0432.<\/p>\n<h2>\u041e\u0431 \u0438\u0433\u0440\u0435<\/h2>\n<p>\u0418\u0433\u0440\u0430 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 \u043e\u0434\u043d\u043e\u0441\u0442\u0440\u0430\u043d\u0438\u0447\u043d\u043e\u0435 SPA-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u043e\u0435 \u043d\u0430 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0435 VueJS \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432 \u044f\u0437\u044b\u043a\u0430 TypeScript (\u0434\u0430\u043b\u0435\u0435 &#8212; TS). \u0414\u0430, \u0432 \u0438\u0433\u0440\u0435 \u0435\u0441\u0442\u044c \u043a\u0435\u0439\u0441\u044b, \u043a\u043e\u0433\u0434\u0430 \u043e\u043d\u0430 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0434\u043b\u044f &#171;\u043f\u043e\u043b\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438&#187; \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0442 \u0438\u0433\u0440\u044b \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0438\u0433\u0440\u043e\u043a\u043e\u0432), \u043d\u043e \u0432 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u0433\u0440\u0443 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u043c SPA-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u043c.<\/p>\n<p>\u0427\u0442\u043e\u0431\u044b \u0438\u0433\u0440\u0430\u0442\u044c \u0431\u044b\u043b\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430, \u043d\u043e \u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u2014 \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 Responsive Design, \u0432 \u0447\u0435\u043c \u043c\u043d\u0435 \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u043c\u043e\u0433\u043b\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 <a href=\"https:\/\/getbootstrap.com\/\" rel=\"noopener noreferrer nofollow\">Bootstrap [v5.3.3]<\/a>. \u0412\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u044f \u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u043d\u0430 \u043a\u0430\u043d\u0432\u0430\u0441\u0435 \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u043d\u044b \u043a \u0442\u0430\u0447\u0430\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u043e\u0436\u043d\u043e \u043b\u0435\u0433\u043a\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0438\u043c\u0438 \u0438 \u0441 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.<\/p>\n<h2>\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0433\u0440\u044b<\/h2>\n<p>\u041d\u0430 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0434\u0432\u0430 \u0440\u0435\u0436\u0438\u043c\u0430 \u0438\u0433\u0440\u044b\u00a0\u2014 \u00ab\u0418\u0433\u0440\u0430 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c\u00bb \u0438 \u00ab\u0418\u0433\u0440\u0430 \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u043c\u00bb. \u0412 \u00ab\u0431\u044d\u043a\u043b\u043e\u0433\u0435\u00bb \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u00ab\u0418\u0433\u0440\u044b \u0441\u00a0\u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u043e\u043c\u00bb.<\/p>\n<p>\u041f\u0440\u0438\u00a0\u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 UI \u0438\u0433\u0440\u044b \u0438 \u0435\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u0430 \u2014 \u044f \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0435\u0435 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e user-\u0444\u0440\u0435\u043d\u0434\u043b\u0438 \u0438 \u0438\u043d\u0442\u0443\u0438\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043d\u044f\u0442\u043d\u043e\u0439, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0440\u0430\u0441\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u0430. \u0410 \u0442\u0430\u043a\u0436\u0435 \u0434\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0438\u0433\u0440\u043e\u043a \u043d\u0435 \u043a\u043b\u0438\u043a\u043d\u0443\u043b \u0442\u0443\u0434\u0430, \u043a\u0443\u0434\u0430 \u043a\u043b\u0438\u043a\u0430\u0442\u044c \u043d\u0435 \u043d\u0430\u0434\u043e, \u0430 \u0435\u0441\u043b\u0438 \u0438 \u043a\u043b\u0438\u043a\u043d\u0443\u043b, \u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u043b \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0432\u043d\u044b\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f, \u0442\u0438\u043f\u0430:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 1. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0438\u0433\u0440\u044b.<\/figcaption><\/div>\n<\/figure>\n<p>\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432 \u043b\u0435\u0432\u043e\u043c \u043d\u0438\u0436\u043d\u0435\u043c \u0443\u0433\u043b\u0443 \u043e\u043a\u043d\u0430.<\/p>\n<p>\u041d\u043e \u043a\u0440\u0430\u0442\u043a\u043e \u0432\u0441\u0435-\u0442\u0430\u043a\u0438 \u0440\u0430\u0441\u043f\u0438\u0448\u0443 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0433\u0440\u044b.<\/p>\n<p>\u0414\u043b\u044f \u0438\u0433\u0440\u044b \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0438\u043a \u0438 \u043f\u0440\u043e\u0439\u0442\u0438 \u043a\u0430\u043f\u0447\u0443 (\u043e\u043d\u0430 \u043d\u0435 \u0441\u043b\u043e\u0436\u043d\u0430\u044f, \u0432\u0441\u0435\u0433\u043e 4 \u0445\u043e\u0440\u043e\u0448\u043e \u0447\u0438\u0442\u0430\u0435\u043c\u044b\u0445 \u0441\u0438\u043c\u0432\u043e\u043b\u0430).<\/p>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0440\u0435\u0436\u0438\u043c \u0438\u0433\u0440\u044b: \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u043c \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u043a\u043d\u043e\u043f\u043a\u0443.<\/p>\n<h3>\u0418\u0433\u0440\u0430 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c<\/h3>\n<p>\u041f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0438\u0433\u0440\u044b \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u2014 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043c\u0438\u0433\u0430\u044e\u0449\u0430\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c \u00ab\u041f\u043e\u0438\u0441\u043a \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430\u00bb \u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0431\u0443\u0434\u0435\u0442 \u0436\u0434\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0433\u0440\u043e\u043a\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0437\u0430\u0439\u0434\u0435\u0442 \u0432 \u0438\u0433\u0440\u0443 \u0438 \u0442\u043e\u0436\u0435 \u0432\u044b\u0431\u0435\u0440\u0435\u0442 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u044b\u0439 \u0442\u0438\u043f \u0438\u0433\u0440\u044b. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0438\u0433\u0440\u0430 \u00ab\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u00bb \u0438\u0445 \u0432\u043c\u0435\u0441\u0442\u0435 \u0438 \u0432\u044b\u0432\u0435\u0434\u0435\u0442 \u0443 \u043e\u0431\u043e\u0438\u0445 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d \u043d\u0430\u0434\u043f\u0438\u0441\u044c:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 2. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0435 \u0434\u043b\u044f \u0438\u0433\u0440\u044b \u0438 \u0435\u0433\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u0435.<\/figcaption><\/div>\n<\/figure>\n<p>\u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u0440\u0438\u0441\u0442\u0443\u043f\u0430\u0435\u043c \u043a \u0440\u0430\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.<\/p>\n<p>\u041d\u0430 \u043b\u0435\u0432\u043e\u043c \u0433\u0440\u0438\u0434\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0412\u0430\u0448\u0430 \u0444\u043b\u043e\u0442\u0438\u043b\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u043f\u043e\u043b\u0435 \u0431\u043e\u044f. \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u2014 10 \u0448\u0442\u0443\u043a. \u041e\u0434\u0438\u043d 4-\u0445 \u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0439, \u0434\u0432\u0430 3-\u0445 \u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0445, \u0442\u0440\u0438 2-\u0445 \u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0445 \u0438 \u0447\u0435\u0442\u044b\u0440\u0435 \u043e\u0434\u043d\u043e\u043f\u0430\u043b\u0443\u0431\u043d\u044b\u0445 . \u041f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0442\u044c \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u043c\u043e\u0436\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043f\u0435\u0440\u0432\u0443\u044e \u043a\u043b\u0435\u0442\u043a\u0443. \u0415\u0441\u043b\u0438 \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c (\u0442\u0430\u043f\u043d\u0443\u0442\u044c) \u043f\u043e \u043d\u0435\u0439 \u0434\u0432\u043e\u0439\u043d\u044b\u043c \u043a\u043b\u0438\u043a\u043e\u043c \u2014 \u043a\u043e\u0440\u0430\u0431\u043b\u044c \u043f\u043e\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 (\u0441 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442). \u041f\u0440\u0438 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u043d\u0430 \u0433\u0440\u0438\u0434\u0435 \u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438<\/p>\n<div class=\"floating-image\">\n<figure class=\"float\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 3. \u041a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u043c\u0435\u0441\u0442\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u044f\/\u043a\u0430\u0441\u0430\u043d\u0438\u044f \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.<\/figcaption><\/div>\n<\/figure>\n<p>\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u2014 \u0438\u0433\u0440\u0430 \u0441\u043b\u0435\u0434\u0438\u0442, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u044b\u043b\u043e \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u0439, \u0430 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0438\u0445 \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u2014 \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u043c\u0435\u0441\u0442\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u0439 (\u043a\u0430\u0441\u0430\u043d\u0438\u0439) \u043a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c. \u041f\u0440\u0438 \u0442\u0430\u043a\u043e\u0439 \u0440\u0430\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0438\u0433\u0440\u043e\u043a\u0443 \u043d\u0430\u0447\u0430\u0442\u044c \u0438\u0433\u0440\u0443 \u0438 \u0432\u044b\u0432\u0435\u0434\u0435\u0442 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435. \u041f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u2014 \u0435\u0441\u043b\u0438 \u043e\u043d \u0441\u0442\u043e\u0438\u0442 \u0440\u044f\u0434\u043e\u043c \u0441 \u0433\u0440\u0430\u043d\u0438\u0446\u0435\u0439 \u0438 \u043f\u0440\u0438 \u043d\u043e\u0432\u043e\u043c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u0435\u0435 \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0442\u044c \u2014 \u0438\u0433\u0440\u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0434\u0438\u0442 \u043e\u0431 \u044d\u0442\u043e\u043c. \u0422.\u0435. \u0435\u0441\u043b\u0438, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u043e\u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0445\u043f\u0430\u043b\u0443\u0431\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u043d\u0430 \u0440\u0438\u0441\u0443\u043d\u043a\u0435 3 \u0441 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 4. \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u043e \u0432\u044b\u0445\u043e\u0434\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u044b \u0441\u0435\u0442\u043a\u0438.<\/figcaption><\/div>\n<\/figure>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0442\u043e\u0433\u043e \u043a\u0430\u043a \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u2014 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u043c \u043a\u043d\u043e\u043f\u043a\u0443 &#171;\u0418\u0433\u0440\u0430\u0442\u044c&#187;, \u044d\u0442\u043e \u043f\u0440\u043e\u0438\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u0412\u044b \u0433\u043e\u0442\u043e\u0432\u044b \u043a \u0438\u0433\u0440\u0435:<\/p>\n<\/div>\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 5. \u0418\u043d\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e \u0433\u043e\u0442\u043e\u0432\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043a \u0438\u0433\u0440\u0435.<\/figcaption><\/div>\n<\/figure>\n<p>\u0415\u0441\u043b\u0438 \u0412\u044b \u0438 \u0412\u0430\u0448 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u043e\u0431\u0430 \u043d\u0430\u0436\u0430\u043b\u0438 \u00ab\u0418\u0433\u0440\u0430\u0442\u044c\u00bb\u00a0\u2014 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c \u00ab\u0418\u0433\u0440\u0430 \u043d\u0430\u0447\u0430\u043b\u0430\u0441\u044c\u00bb \u0438 \u0438\u0433\u0440\u0430 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439 \u0432\u043e\u00a0\u0432\u0440\u0435\u043c\u044f \u0438\u0433\u0440\u044b \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f. \u041f\u0435\u0440\u0432\u0435\u043d\u0441\u0442\u0432\u043e \u0445\u043e\u0434\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u00a0\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c. \u0423\u00a0\u0438\u0433\u0440\u043e\u043a\u0430, \u0447\u044c\u044f \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u0441\u0442\u0440\u0435\u043b\u044f\u0442\u044c\u00a0\u2014 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0438\u0433\u0430\u044e\u0449\u0430\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c \u00ab\u0432\u0430\u0448 \u0445\u043e\u0434\u00bb:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 6. \u041d\u0430\u0447\u0430\u043b\u043e \u0438\u0433\u0440\u044b.<\/figcaption><\/div>\n<\/figure>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0438\u0433\u0440\u043e\u043a\u0438 \u0434\u0435\u043b\u0430\u044e\u0442 \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u044b \u043f\u0443\u0442\u0435\u043c \u043a\u043b\u0438\u043a\u0430 \u043f\u043e \u0442\u043e\u0439 \u0438\u043b\u0438 \u0438\u043d\u043e\u0439 \u043b\u043e\u043a\u0430\u0446\u0438\u0438 (\u043a\u043b\u0435\u0442\u043a\u0435 \u0441 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438) \u043d\u0430 \u0433\u0440\u0438\u0434\u0435 \u0441\u043f\u0440\u0430\u0432\u0430. \u041f\u0440\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u043d\u0438\u0438 \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u2014 \u0437\u0430 \u0438\u0433\u0440\u043e\u043a\u043e\u043c \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0441\u0442\u044c \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u0430; \u0435\u0441\u043b\u0438 \u0436\u0435 \u0438\u0433\u0440\u043e\u043a \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u0438\u043b \u043c\u0438\u043c\u043e, \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442 \u043a \u0435\u0433\u043e \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0443. <\/p>\n<p>\u041f\u0440\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u043d\u0438\u0438\/\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0438 \u043a\u043e\u0440\u0430\u0431\u043b\u044f \u2014 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0433\u0440\u0430\u043d\u0438\u0447\u043d\u044b\u0435 (\u0443\u0433\u043b\u043e\u0432\u044b\u0435) \u043a\u043b\u0435\u0442\u043a\u0438 \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0430 \u043e\u0431\u043e\u0438\u0445 \u0433\u0440\u0438\u0434\u0430\u0445:<\/p>\n<figure class=\"\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 7. \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0430 \u043b\u043e\u043a\u0430\u0446\u0438\u0439.<\/figcaption><\/div>\n<\/figure>\n<p>\u0418\u0433\u0440\u0430 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442\u0441\u044f \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440 \u043f\u043e\u043a\u0430 \u043a\u0442\u043e-\u043d\u0438\u0431\u0443\u0434\u044c \u0438\u0437 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u0432 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043d\u0435 \u043f\u043e\u0442\u043e\u043f\u0438\u0442 \u0432\u0440\u0430\u0436\u0435\u0441\u043a\u0443\u044e \u0444\u043b\u043e\u0442\u0438\u043b\u0438\u044e. \u041f\u043e\u0441\u043b\u0435 \u043f\u043e\u0431\u0435\u0434\u044b\/\u043f\u043e\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0443 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u044b, \u0430 \u0443 \u043f\u0440\u043e\u0438\u0433\u0440\u0430\u0432\u0448\u0435\u0433\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0442\u0441\u044f \u043a\u0440\u0430\u0441\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0435 \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430:<\/p>\n<figure class=\"\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 8. \u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u0442\u043e\u043f\u043b\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u0440\u0430\u0431\u043b\u0435\u0439.<\/figcaption><\/div>\n<\/figure>\n<p>\u041f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u2014 \u043c\u043e\u0436\u043d\u043e \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0435\u0449\u0435 \u0440\u0430\u0437, \u043d\u0435 \u043f\u0440\u043e\u0445\u043e\u0434\u044f \u0437\u0430\u043d\u043e\u0432\u043e \u0432\u044b\u0448\u0435\u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u0432\u0432\u043e\u0434\u0443 \u043d\u0438\u043a\u0430, \u043a\u0430\u043f\u0447\u0438, \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u0438\u0433\u0440\u044b, \u043f\u043e\u0438\u0441\u043a\u0443 \u0438 \u0442.\u043f. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0441\u043b\u0435 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f \u0438\u0433\u0440\u044b \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043a\u043d\u043e\u043f\u043a\u0430:<\/p>\n<figure class=\"\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 9. \u041a\u043d\u043e\u043f\u043a\u0430 \u0434\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0438\u0433\u0440\u044b \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c.<\/figcaption><\/div>\n<\/figure>\n<p>\u041d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043d\u0435\u0435 \u2014 \u0443 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u0430 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435:<\/p>\n<figure class=\"\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 10. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u0438\u043a\u0430 \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0435\u0449\u0435 \u043e\u0434\u043d\u0443 \u0438\u0433\u0440\u0443.<\/figcaption><\/div>\n<\/figure>\n<p>\u0415\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a \u0442\u043e\u0436\u0435 \u043d\u0430\u0436\u043c\u0435\u0442 \u044d\u0442\u0443 \u043a\u043d\u043e\u043f\u043a\u0443\u00a0\u2014 \u0443\u00a0\u043e\u0431\u043e\u0438\u0445 \u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0437\u0430\u043d\u043e\u0432\u043e \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0438 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0438\u0433\u0440\u0443. \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439 \u043d\u0430\u00a0\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0433\u0440 \u043d\u0435\u0442. \u0414\u043b\u044f\u00a0\u0432\u044b\u0445\u043e\u0434\u0430 \u0438\u0437\u00a0\u0438\u0433\u0440\u044b \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u043e\u0434\u0438\u043d \u0438\u0437\u00a0\u0438\u0433\u0440\u043e\u043a\u043e\u0432 \u0432\u044b\u0445\u043e\u0434\u0438\u0442 \u0438\u0437\u00a0\u0438\u0433\u0440\u044b (\u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443,\u00a0\u043b\u0438\u0431\u043e \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0443) \u0432\u00a0\u0435\u0435 \u00ab\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u0444\u0430\u0437\u0435\u00bb\u00a0\u2014 \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e\u00a0\u0440\u0430\u0437\u0440\u044b\u0432\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 11. \u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e \u0440\u0430\u0437\u0440\u044b\u0432\u0435 \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0438 \u0432\u044b\u0445\u043e\u0434\u0435 \u0438\u0437 \u0438\u0433\u0440\u044b.<\/figcaption><\/div>\n<\/figure>\n<h3>\u0418\u0433\u0440\u0430 \u0441 \u0434\u0440\u0443\u0433\u043e\u043c<\/h3>\n<p>\u041f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c \u2014 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u0444\u043e\u0440\u043c\u0430 \u0441 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u043c \u0412\u0430\u0448\u0435\u0433\u043e id \u0438 \u043f\u043e\u043b\u0435 \u0432\u0432\u043e\u0434\u0430 \u0434\u043b\u044f id \u0434\u0440\u0443\u0433\u0430 \u0441 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438:<\/p>\n<figure class=\"\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 12. \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438\u0433\u0440\u044b \u0441 \u0434\u0440\u0443\u0433\u043e\u043c.<\/figcaption><\/div>\n<\/figure>\n<p>\u0421\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u2014 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043c\u0435\u043d\u044f\u0442\u044c\u0441\u044f \u0441 \u0434\u0440\u0443\u0433\u043e\u043c id-\u0448\u043d\u0438\u043a\u0430\u043c\u0438, \u0432\u0432\u0435\u0441\u0442\u0438 id \u0434\u0440\u0443\u0433\u0430 \u0432 \u043d\u0443\u0436\u043d\u043e\u0435 \u043f\u043e\u043b\u0435 \u0438 \u043d\u0430\u0436\u0430\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0433\u0440\u0443\u00bb. \u0414\u0430\u043b\u0435\u0435\u00a0\u2014 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u0438\u0433\u0440\u0435 \u0441\u043e \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u0441\u043e\u043f\u0435\u0440\u043d\u0438\u043a\u043e\u043c\u00a0\u2014 \u043d\u0443\u0436\u043d\u043e \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0440\u0430\u0431\u043b\u0438 \u0438 \u043d\u0430\u0436\u0430\u0442\u044c \u00ab\u0418\u0433\u0440\u0430\u0442\u044c\u00bb. \u041f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0438\u0433\u0440\u044b \u0441\u00a0\u0434\u0440\u0443\u0433\u043e\u043c\u00a0\u2014 \u0443\u00a0\u0432\u0430\u0441 \u0442\u0430\u043a\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u044b\u0433\u0440\u0430\u0442\u044c \u0441 \u043d\u0438\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0431\u0435\u0437 \u043d\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438\u0433\u0440\u044b.<\/p>\n<h2>Backend<\/h2>\n<p>\u0411\u044d\u043a \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u043d\u0430 \u044f\u0437\u044b\u043a\u0435 Python, \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a FastAPI. \u0414\u043b\u044f \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0434\u043e\u043a\u0435\u0440\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d \u043e\u0434\u0438\u043d \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 uvicorn-\u0432\u043e\u0440\u043a\u0435\u0440. \u0424\u0440\u043e\u043d\u0442 \u043e\u0431\u0449\u0430\u0435\u0442\u0441\u044f \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c WebSocket-\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u0411\u0414 \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437 \u0442\u0440\u0435\u0445 \u0442\u0430\u0431\u043b\u0438\u0447\u0435\u043a:<\/p>\n<details class=\"spoiler\">\n<summary>\u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\">\n<div><figcaption>\u0420\u0438\u0441\u0443\u043d\u043e\u043a 13. \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414.<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<p>\u0414\u0432\u0435 \u0438\u0437 \u043d\u0438\u0445 \u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u044b\u0435 \u2014 \u0441 \u0442\u0438\u043f\u043e\u043c \u0438\u0433\u0440\u044b \u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u0438\u0433\u0440\u043e\u043a\u0430, \u0430 \u0442\u0440\u0435\u0442\u044c\u044f (TRivalCouple &#8212; <em>\u0441\u043e\u043f\u0435\u0440\u043d\u0438\u0447\u0430\u044e\u0449\u0430\u044f \u043f\u0430\u0440\u0430<\/em>) \u2014 \u0431\u0443\u0444\u0435\u0440\u043d\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0438\u0433\u0440\u043e\u043a\u0438 &#171;\u0441\u043e\u0435\u0434\u0438\u043d\u044f\u044e\u0442\u0441\u044f&#187; \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0431\u043e\u0439 \u0434\u043b\u044f \u0438\u0433\u0440\u044b, \u0430 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0438\u0433\u0440\u044b \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0434\u0430\u043b\u044f\u0435\u0442\u0441\u044f.<\/p>\n<p>\u0414\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0441\u043e\u043a\u0435\u0442\u043e\u0432 \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043c\u0438 \u0431\u044b\u043b \u0441\u043e\u0437\u0434\u0430\u043d \u043a\u043b\u0430\u0441\u0441 ConnectionManager, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0445\u0440\u0430\u043d\u0438\u0442 uuid \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u0435\u0433\u043e WS-\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435:<\/p>\n<details class=\"spoiler\">\n<summary>class ConnectionManager<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">class ConnectionManager:     def __init__(self):         self.active_connections: dict[uuid.UUID, WebSocket] = {}      async def connect(self, client_uuid: uuid.UUID, websocket: WebSocket):         await websocket.accept()         self.active_connections[client_uuid] = websocket      async def disconnect(self, client_uuid: uuid.UUID):         self.active_connections.pop(client_uuid)      async def send_personal_message(self, client_uuid: uuid.UUID, message: dict):         if client_uuid in self.active_connections:             await self.active_connections[client_uuid].send_json(message)      async def send_structured_data(self, client_uuid: uuid.UUID, msg_type: str, data: dict, is_status_ok: bool = True):         message_to_send = {'msg_type': msg_type, 'data': data, 'is_status_ok': is_status_ok}         await self.send_personal_message(client_uuid, message_to_send)      async def broadcast(self, message: str):  # \u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0443 \u043f\u043e\u043a\u0430 \u043d\u0435 \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043b         for connection in self.active_connections.values():             await connection.send_text(message)      async def print_number_of_clients(self):         print('Number of clients: ', len(self.active_connections)) <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0412\u0445\u043e\u0434\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u043e\u0439 \u0441\u043b\u0443\u0436\u0438\u0442 ws-\u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442, \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0449\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441\u044b:<\/p>\n<pre><code class=\"python\">manager = ConnectionManager()  @router.websocket(\"\/client\/{client_uuid}\/ws\") async def websocket_endpoint(ws: WebSocket, client_uuid: uuid.UUID):     await manager.connect(client_uuid, ws)     try:         while True:             data_from_client = await ws.receive_json()             await process_data(client_uuid, data_from_client, manager)     except WebSocketDisconnect:         await manager.disconnect(client_uuid)         await delete_rival_couple_and_notify(client_uuid, manager)         await manager.print_number_of_clients()<\/code><\/pre>\n<p>\u041c\u0435\u0442\u043e\u0434 <code>process_data()<\/code> \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 \u043e\u0442\u0432\u0435\u0442 (\u0441\u043c. controller.py):<\/p>\n<details class=\"spoiler\">\n<summary>controller.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">import uuid from datetime import datetime from random import randint  from sqlalchemy import select, func, or_, and_, ColumnElement, asc  import db from entities.model import TRivalCouple, TGameType from helper.game_state import GameState from helper.game_type import GameType from helper.message_type import MessageType from websocket.common import notify_enemy_about_game_creation from websocket.connection_manager import ConnectionManager from websocket.friend_game_controller import process_friend_game_creation  player_available_condition: ColumnElement[bool] = or_(     and_(TRivalCouple.dfplayer1.__ne__(None), TRivalCouple.dfplayer2.__eq__(None)),     and_(TRivalCouple.dfplayer1.__eq__(None), TRivalCouple.dfplayer2.__ne__(None)), )  random_game_clause: ColumnElement[bool] = TGameType.id.__eq__(GameType.RANDOM.value)   def find_by_client_id_clause(client_uuid: uuid.UUID) -&gt; ColumnElement[bool]:     clause: ColumnElement[bool] = or_(         TRivalCouple.dfplayer1.__eq__(client_uuid),         TRivalCouple.dfplayer2.__eq__(client_uuid)     )     return clause   def available_random_couple_exists() -&gt; bool:     with db.session_scope() as s_:         stmt =<\/code><\/pre>\n<\/div>\n<\/details>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-445565","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/445565","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=445565"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/445565\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=445565"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=445565"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=445565"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}