initial commit
This commit is contained in:
commit
7f01b847d8
10
api/Dockerfile
Normal file
10
api/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
FROM python:3
|
||||||
|
|
||||||
|
COPY requirements.txt /tmp/
|
||||||
|
|
||||||
|
RUN pip install -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
COPY . /code
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
CMD python -m workers
|
||||||
74
docker-compose.yml
Normal file
74
docker-compose.yml
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external:
|
||||||
|
name: proxy
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
||||||
|
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: postgres
|
||||||
|
env_file:
|
||||||
|
- postgresql
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3.11.5
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy
|
||||||
|
env_file:
|
||||||
|
- rabbitmq
|
||||||
|
|
||||||
|
message_receiver:
|
||||||
|
image: twitchbot/message_receveiver
|
||||||
|
build:
|
||||||
|
context: message_receiver
|
||||||
|
environment:
|
||||||
|
RABBITMQ_HOST: rabbitmq
|
||||||
|
RABBITMQ_QUEUE: raw_messages
|
||||||
|
env_file:
|
||||||
|
- rabbitmq
|
||||||
|
- twitch
|
||||||
|
depends_on:
|
||||||
|
- rabbitmq
|
||||||
|
|
||||||
|
tetris: &tetris
|
||||||
|
image: twichbot/tetris
|
||||||
|
build:
|
||||||
|
context: tetris
|
||||||
|
volumes:
|
||||||
|
- ./tetris:/app
|
||||||
|
environment:
|
||||||
|
RABBITMQ_HOST: rabbitmq
|
||||||
|
RABBITMQ_QUEUE_MESSAGES: raw_messages
|
||||||
|
RABBITMQ_EXCHANGE_TETRIS: tetris
|
||||||
|
env_file:
|
||||||
|
- rabbitmq
|
||||||
|
depends_on:
|
||||||
|
- rabbitmq
|
||||||
|
- message_receiver
|
||||||
|
|
||||||
|
tetris-api:
|
||||||
|
<<: *tetris
|
||||||
|
command: uvicorn tetris.web:app --reload --host=0.0.0.0
|
||||||
|
environment:
|
||||||
|
VIRTUAL_HOST: tetris.de-codeur.com
|
||||||
|
LETSENCRYPT_HOST: tetris.de-codeur.com
|
||||||
|
VIRTUAL_PORT: 8000
|
||||||
|
RABBITMQ_HOST: rabbitmq
|
||||||
|
RABBITMQ_QUEUE_MESSAGES: raw_messages
|
||||||
|
RABBITMQ_EXCHANGE_TETRIS: tetris
|
||||||
|
RABBITMQ_QUEUE_TETRIS_UI: tetris-ui
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy
|
||||||
11
message_receiver/Dockerfile
Normal file
11
message_receiver/Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM node
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json /app
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
CMD npm run bot
|
||||||
62
message_receiver/bot.js
Normal file
62
message_receiver/bot.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
const tmi = require('tmi.js');
|
||||||
|
var amqp = require('amqplib/callback_api');
|
||||||
|
|
||||||
|
twitch_token = process.env.TWITCH_ACCESSTOKEN
|
||||||
|
twitch_username = process.env.TWITCH_USERNAME
|
||||||
|
twitch_channel = process.env.TWITCH_CHANNEL
|
||||||
|
|
||||||
|
rabbitmq_user = process.env.RABBITMQ_DEFAULT_USER
|
||||||
|
rabbitmq_pass = process.env.RABBITMQ_DEFAULT_PASS
|
||||||
|
rabbitmq_host = process.env.RABBITMQ_HOST
|
||||||
|
rabbitmq_queue = process.env.RABBITMQ_QUEUE
|
||||||
|
|
||||||
|
// Define twitch configuration options
|
||||||
|
const opts = {
|
||||||
|
identity: {
|
||||||
|
username: twitch_username,
|
||||||
|
password: twitch_token,
|
||||||
|
},
|
||||||
|
channels: [twitch_channel,]
|
||||||
|
};
|
||||||
|
console.log(opts);
|
||||||
|
|
||||||
|
|
||||||
|
amqp_url = `amqp://${rabbitmq_user}:${rabbitmq_pass}@${rabbitmq_host}`;
|
||||||
|
console.log(amqp_url);
|
||||||
|
amqp.connect(amqp_url, function(error0, connection) {
|
||||||
|
if (error0) {
|
||||||
|
throw error0;
|
||||||
|
}
|
||||||
|
connection.createChannel(function(error1, channel) {
|
||||||
|
if (error1) {
|
||||||
|
throw error1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var queue = rabbitmq_queue;
|
||||||
|
|
||||||
|
channel.assertQueue(queue, {
|
||||||
|
durable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a clieng with our options
|
||||||
|
const client = new tmi.client(opts);
|
||||||
|
// Connect to Twitch:
|
||||||
|
client.connect();
|
||||||
|
client.on('message', onMessageHandler);
|
||||||
|
client.on('connected', onConnectedHandler);
|
||||||
|
|
||||||
|
function onMessageHandler (target, context, msg, self) {
|
||||||
|
console.log('msg:', msg)
|
||||||
|
channel.sendToQueue(queue, Buffer.from(JSON.stringify({
|
||||||
|
"msg": msg,
|
||||||
|
"context": context,
|
||||||
|
"target": target,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Called every time the bot connects to Twitch chat
|
||||||
|
function onConnectedHandler (addr, port) {
|
||||||
|
console.log(`* Connected to ${addr}:${port}`);
|
||||||
|
}
|
||||||
195
message_receiver/package-lock.json
generated
Normal file
195
message_receiver/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
{
|
||||||
|
"name": "dice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "dice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"amqplib": "^0.10.3",
|
||||||
|
"tmi.js": "^1.8.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@acuminous/bitsyntax": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-more-ints": "~1.0.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"safe-buffer": "~5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/amqplib": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz",
|
||||||
|
"integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@acuminous/bitsyntax": "^0.1.2",
|
||||||
|
"buffer-more-ints": "~1.0.0",
|
||||||
|
"readable-stream": "1.x >=1.1.9",
|
||||||
|
"url-parse": "~1.5.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-more-ints": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg=="
|
||||||
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
|
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/querystringify": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.1",
|
||||||
|
"isarray": "0.0.1",
|
||||||
|
"string_decoder": "~0.10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "0.10.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||||
|
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
|
||||||
|
},
|
||||||
|
"node_modules/tmi.js": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmi.js/-/tmi.js-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew==",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"ws": "^8.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
|
"node_modules/url-parse": {
|
||||||
|
"version": "1.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"querystringify": "^2.1.1",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
|
||||||
|
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
message_receiver/package.json
Normal file
16
message_receiver/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "twitch_chat_injector",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "bot.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"bot": "node bot.js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"amqplib": "^0.10.3",
|
||||||
|
"tmi.js": "^1.8.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
postgresql
Normal file
3
postgresql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
POSTGRES_DB=bot
|
||||||
|
POSTGRES_USER=user
|
||||||
|
POSTGRES_PASSWORD=password
|
||||||
2
rabbitmq
Normal file
2
rabbitmq
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
RABBITMQ_DEFAULT_USER=user
|
||||||
|
RABBITMQ_DEFAULT_PASS=pass
|
||||||
11
tetris/Dockerfile
Normal file
11
tetris/Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM python:3
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
COPY requirements.txt /app
|
||||||
|
RUN pip install -U -r requirements.txt
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
CMD python -m tetris
|
||||||
4
tetris/requirements.txt
Normal file
4
tetris/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pika
|
||||||
|
fastapi[all]
|
||||||
|
uvicorn
|
||||||
|
sse_starlette
|
||||||
11
tetris/static/<
Normal file
11
tetris/static/<
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
const eventSource = new EventSource('/stream');
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
console.log(`Message reçu: ${event.data}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('Erreur SSE:', error);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
51
tetris/static/game.css
Normal file
51
tetris/static/game.css
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
table {
|
||||||
|
border-collpase: collpase;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: black;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
td :not(.empty_cell) {
|
||||||
|
box-shadow: 3px 3px 0px #2c2e38, 5px 5px 0px #5c5f72;
|
||||||
|
margin: 20px
|
||||||
|
padding: 20px
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
border-color: black
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty_cell {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
border: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.I_tetrimino {
|
||||||
|
background-color: FireBrick;
|
||||||
|
}
|
||||||
|
.O_tetrimino {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
.T_tetrimino {
|
||||||
|
background-color: aquamarine;
|
||||||
|
}
|
||||||
|
.L_tetrimino {
|
||||||
|
background-color: cadetBlue;
|
||||||
|
}
|
||||||
|
.J_tetrimino {
|
||||||
|
background-color: BlueViolet;
|
||||||
|
}
|
||||||
|
.Z_tetrimino {
|
||||||
|
background-color: darkkhaki
|
||||||
|
}
|
||||||
|
.S_tetrimino {
|
||||||
|
background-color: Coral;
|
||||||
|
}
|
||||||
|
|
||||||
58
tetris/static/game.js
Normal file
58
tetris/static/game.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
const eventSource = new EventSource("/stream");
|
||||||
|
|
||||||
|
|
||||||
|
color_mapping = [
|
||||||
|
"empty_cell",
|
||||||
|
"I_tetrimino",
|
||||||
|
"O_tetrimino",
|
||||||
|
"T_tetrimino",
|
||||||
|
"L_tetrimino",
|
||||||
|
"J_tetrimino",
|
||||||
|
"Z_tetrimino",
|
||||||
|
"S_tetrimino",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
function draw_grid(data) {
|
||||||
|
const table = document.getElementById("grid");
|
||||||
|
table.innerHTML = "";
|
||||||
|
for (row in data.grid) {
|
||||||
|
let tr = document.createElement("tr");
|
||||||
|
for (cell in data.grid[row]) {
|
||||||
|
let td = document.createElement("td");
|
||||||
|
td.id = `${cell}-${row}`;
|
||||||
|
td.classList.add("cell");
|
||||||
|
td.classList.add(color_mapping[data.grid[row][cell]]);
|
||||||
|
tr.appendChild(td);
|
||||||
|
}
|
||||||
|
table.appendChild(tr);
|
||||||
|
}
|
||||||
|
let x = parseInt(data.current_tetrimino.position[0]);
|
||||||
|
let y = parseInt(data.current_tetrimino.position[1]);
|
||||||
|
console.log(data.current_tetrimino.shape);
|
||||||
|
for (i in data.current_tetrimino.shape) {
|
||||||
|
console.log("x", x,i);
|
||||||
|
for (j in data.current_tetrimino.shape[i]) {
|
||||||
|
console.log("y", y, j);
|
||||||
|
if (data.current_tetrimino.shape[i][j] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let current_x = x + parseInt(j);
|
||||||
|
let current_y = data.size.height - y + parseInt(i);
|
||||||
|
console.log(`${current_x}-${current_y}`);
|
||||||
|
let cell = document.getElementById(`${current_x}-${current_y}`)
|
||||||
|
cell.classList.remove("empty_cell");
|
||||||
|
cell.classList.add(color_mapping[data.current_tetrimino.value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
draw_grid(JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('Erreur SSE:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
31
tetris/static/index.html
Normal file
31
tetris/static/index.html
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>My Website</title>
|
||||||
|
<link rel="stylesheet" href="./game.css">
|
||||||
|
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<table id="grid">
|
||||||
|
</table>
|
||||||
|
<script src=/static/game.js></script>
|
||||||
|
<iframe name="dummyframe" id="dummyframe" style="display: none;"></iframe>
|
||||||
|
<form action="/play" method="post" target="dummyframe">
|
||||||
|
<input type="submit" name="action" value="r left">
|
||||||
|
<input type="submit" name="action" value="left">
|
||||||
|
<input type="submit" name="action" value="down">
|
||||||
|
<input type="submit" name="action" value="right">
|
||||||
|
<input type="submit" name="action" value="r right">
|
||||||
|
</br>
|
||||||
|
<input type="submit" name="action" value="restart">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
65
tetris/tetris/__main__.py
Normal file
65
tetris/tetris/__main__.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pika
|
||||||
|
|
||||||
|
from tetris.game import Game
|
||||||
|
|
||||||
|
|
||||||
|
rabbitmq_queue_message = os.environ['RABBITMQ_QUEUE_MESSAGES']
|
||||||
|
rabbitmq_exchange_tetris = os.environ['RABBITMQ_EXCHANGE_TETRIS']
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel():
|
||||||
|
connection = pika.BlockingConnection(
|
||||||
|
pika.ConnectionParameters(
|
||||||
|
host=os.environ['RABBITMQ_HOST'],
|
||||||
|
credentials=pika.PlainCredentials(
|
||||||
|
os.environ['RABBITMQ_DEFAULT_USER'],
|
||||||
|
os.environ['RABBITMQ_DEFAULT_PASS'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
channel = connection.channel()
|
||||||
|
channel.exchange_declare(rabbitmq_exchange_tetris, exchange_type="direct")
|
||||||
|
channel.queue_declare(queue=rabbitmq_queue_message)
|
||||||
|
#channel.queue_declare(queue=rabbitmq_queue_tetris)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
GAME = Game()
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_message(channel, method, properties, body):
|
||||||
|
body = json.loads(body)
|
||||||
|
message = body["msg"]
|
||||||
|
if message == '!start':
|
||||||
|
GAME.restart()
|
||||||
|
elif message == "!mleft":
|
||||||
|
GAME.move_left()
|
||||||
|
elif message == "!mdown":
|
||||||
|
GAME.move_down()
|
||||||
|
elif message == "!mright":
|
||||||
|
GAME.move_right()
|
||||||
|
elif message == "!rleft":
|
||||||
|
GAME.rotate_left()
|
||||||
|
elif message == "!rright":
|
||||||
|
GAME.rotate_right()
|
||||||
|
else:
|
||||||
|
print("not a command")
|
||||||
|
GAME.display()
|
||||||
|
GAME.move()
|
||||||
|
publish(channel, "donno", GAME.serialize())
|
||||||
|
|
||||||
|
|
||||||
|
def publish(channel, routing, msg):
|
||||||
|
channel.basic_publish(rabbitmq_exchange_tetris, routing, json.dumps(msg))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
channel = get_channel()
|
||||||
|
channel.basic_consume(on_message_callback=dispatch_message, queue=rabbitmq_queue_message, auto_ack=True)
|
||||||
|
channel.start_consuming()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
224
tetris/tetris/game.py
Normal file
224
tetris/tetris/game.py
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
import random
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
class Tetrimino:
|
||||||
|
value = 0
|
||||||
|
rotation = 0
|
||||||
|
shapes = []
|
||||||
|
|
||||||
|
def rotate_left(self):
|
||||||
|
self.rotate(-1)
|
||||||
|
|
||||||
|
def rotate_right(self):
|
||||||
|
self.rotate(1)
|
||||||
|
|
||||||
|
def rotate(self, sense):
|
||||||
|
old_width = self.size[0]
|
||||||
|
new_index = (self.rotation + sense) % len(self.shapes)
|
||||||
|
self.rotation = new_index
|
||||||
|
return self.size[0] > old_width
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_random_tetrimino(cls):
|
||||||
|
return random.choice(cls.__subclasses__())()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.shape)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
return (len(self.shape[0]), len(self.shape))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self):
|
||||||
|
return self.shapes[self.rotation]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class I_Tetrimino(Tetrimino):
|
||||||
|
value = 1
|
||||||
|
shapes = [
|
||||||
|
[[1, 1, 1, 1]],
|
||||||
|
[[1],
|
||||||
|
[1],
|
||||||
|
[1],
|
||||||
|
[1],
|
||||||
|
[1]]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class O_Tetrimino(Tetrimino):
|
||||||
|
value = 2
|
||||||
|
shapes = [[[2, 2],
|
||||||
|
[2, 2]]]
|
||||||
|
|
||||||
|
class T_Tetrimino(Tetrimino):
|
||||||
|
value = 3
|
||||||
|
shapes = [
|
||||||
|
[[3, 3, 3],
|
||||||
|
[0, 3, 0]],
|
||||||
|
[[0, 3],
|
||||||
|
[3, 3],
|
||||||
|
[0, 3]],
|
||||||
|
[[0, 3, 0],
|
||||||
|
[3, 3, 3]],
|
||||||
|
[[3, 0],
|
||||||
|
[3, 3],
|
||||||
|
[3, 0]]
|
||||||
|
]
|
||||||
|
|
||||||
|
class L_Tetriminos(Tetrimino):
|
||||||
|
value = 4
|
||||||
|
shapes = [
|
||||||
|
[[4, 4, 4],
|
||||||
|
[4, 0, 0]],
|
||||||
|
[[4, 4],
|
||||||
|
[0, 4],
|
||||||
|
[0, 4]],
|
||||||
|
[[0, 0, 4],
|
||||||
|
[4, 4, 4]],
|
||||||
|
[[4, 0],
|
||||||
|
[4, 0],
|
||||||
|
[4, 4]]
|
||||||
|
]
|
||||||
|
|
||||||
|
class J_Tetriminos(Tetrimino):
|
||||||
|
value = 5
|
||||||
|
shapes = [
|
||||||
|
[[5, 5, 5],
|
||||||
|
[0, 0, 5]],
|
||||||
|
[[0, 5],
|
||||||
|
[0, 5],
|
||||||
|
[5, 5]],
|
||||||
|
[[5, 0, 0],
|
||||||
|
[5, 5, 5]],
|
||||||
|
[[5, 5],
|
||||||
|
[5, 0],
|
||||||
|
[5, 0]]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Z_Tetriminos(Tetrimino):
|
||||||
|
value = 6
|
||||||
|
shapes = [
|
||||||
|
[[6, 6, 0],
|
||||||
|
[0, 6, 6]],
|
||||||
|
[[0, 6],
|
||||||
|
[6, 6],
|
||||||
|
[6, 0]],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class S_Tetriminos(Tetrimino):
|
||||||
|
value = 7
|
||||||
|
shapes = [
|
||||||
|
[[0, 7, 7],
|
||||||
|
[7, 7, 0]],
|
||||||
|
[[7, 0],
|
||||||
|
[7, 7],
|
||||||
|
[0, 7]],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
def __init__(self):
|
||||||
|
self._width = 0
|
||||||
|
self._heighth = 0
|
||||||
|
self.grid = self.init_grid()
|
||||||
|
self.current_tetrimino = Tetrimino.get_random_tetrimino()
|
||||||
|
self.current_position = [self.width // 2, self.height - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.grid = self.init_grid()
|
||||||
|
self.current_tetrimino = Tetrimino.get_random_tetrimino()
|
||||||
|
self.current_position = [self.width // 2, self.height - 1]
|
||||||
|
|
||||||
|
def init_grid(self, width=10, height=20):
|
||||||
|
self._width = width
|
||||||
|
self._height = height
|
||||||
|
array = []
|
||||||
|
for _ in range(height):
|
||||||
|
array.append([0] * width)
|
||||||
|
return array
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
print(self.current_tetrimino, self.current_tetrimino.size)
|
||||||
|
print(self.current_position)
|
||||||
|
grid = deepcopy(self.grid)
|
||||||
|
for i, part in enumerate(self.current_tetrimino.shape):
|
||||||
|
for j, value in enumerate(part):
|
||||||
|
grid[self.current_position[1] - i][self.current_position[0] + j] = value
|
||||||
|
for line in reversed(grid):
|
||||||
|
print(line)
|
||||||
|
print(self.is_blocked())
|
||||||
|
|
||||||
|
def rotate_left(self):
|
||||||
|
self.current_tetrimino.rotate_left()
|
||||||
|
self.current_position[0] = min(self.current_position[0], self.width - self.current_tetrimino.size[0])
|
||||||
|
|
||||||
|
def rotate_right(self):
|
||||||
|
self.current_tetrimino.rotate_right()
|
||||||
|
self.current_position[0] = min(self.current_position[0], self.width - self.current_tetrimino.size[0])
|
||||||
|
|
||||||
|
def move_left(self):
|
||||||
|
self.current_position[0] -= 1
|
||||||
|
self.current_position[0] = max(self.current_position[0], 0)
|
||||||
|
|
||||||
|
def move_right(self):
|
||||||
|
self.current_position[0] += 1
|
||||||
|
self.current_position[0] = min(self.current_position[0], self.width - self.current_tetrimino.size[0])
|
||||||
|
|
||||||
|
def move_down(self):
|
||||||
|
while not self.is_blocked():
|
||||||
|
self.current_position[1] -= 1
|
||||||
|
self.add_tetrimino_to_grid()
|
||||||
|
self.new_tetrimino()
|
||||||
|
|
||||||
|
|
||||||
|
def move(self):
|
||||||
|
self.current_position[1] -= 1
|
||||||
|
if self.is_blocked():
|
||||||
|
self.add_tetrimino_to_grid()
|
||||||
|
self.new_tetrimino()
|
||||||
|
|
||||||
|
def add_tetrimino_to_grid(self):
|
||||||
|
for i, part in enumerate(self.current_tetrimino.shape):
|
||||||
|
for j, value in enumerate(part):
|
||||||
|
if value:
|
||||||
|
self.grid[self.current_position[1] - i][self.current_position[0] + j] = value
|
||||||
|
|
||||||
|
def new_tetrimino(self):
|
||||||
|
self.current_tetrimino = Tetrimino.get_random_tetrimino()
|
||||||
|
self.current_position = [self.width // 2, self.height - 1]
|
||||||
|
|
||||||
|
def is_blocked(self):
|
||||||
|
for i, part in enumerate(self.current_tetrimino.shape):
|
||||||
|
for j, value in enumerate(part):
|
||||||
|
if value != 0 and self.grid[self.current_position[1] - i - 1][self.current_position[0] + j] != 0:
|
||||||
|
return True
|
||||||
|
return self.current_position[1] - self.current_tetrimino.size[1] < 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
return self._width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
return self._height
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {
|
||||||
|
"grid": self.grid[::-1],
|
||||||
|
"size": {
|
||||||
|
"width": self.width,
|
||||||
|
"height": self.height,
|
||||||
|
},
|
||||||
|
"current_tetrimino": {
|
||||||
|
"position": self.current_position,
|
||||||
|
"shape": self.current_tetrimino.shape,
|
||||||
|
"value": self.current_tetrimino.value
|
||||||
|
}
|
||||||
|
}
|
||||||
80
tetris/tetris/web.py
Normal file
80
tetris/tetris/web.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pika
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
#pika
|
||||||
|
connection = pika.BlockingConnection(
|
||||||
|
pika.ConnectionParameters(
|
||||||
|
host=os.environ['RABBITMQ_HOST'],
|
||||||
|
credentials=pika.PlainCredentials(
|
||||||
|
os.environ['RABBITMQ_DEFAULT_USER'],
|
||||||
|
os.environ['RABBITMQ_DEFAULT_PASS'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post('/play')
|
||||||
|
async def play(request: Request):
|
||||||
|
action = await request.body()
|
||||||
|
action = action.decode('utf-8').split("=")[1]
|
||||||
|
print(action)
|
||||||
|
body = {"msg": ""}
|
||||||
|
if action == "left":
|
||||||
|
body["msg"] = "!mleft"
|
||||||
|
elif action == "down":
|
||||||
|
body["msg"] = "!mdown"
|
||||||
|
elif action == "right":
|
||||||
|
body["msg"] = "!mright"
|
||||||
|
elif action == "r+left":
|
||||||
|
body["msg"] = "!rleft"
|
||||||
|
elif action == "r+right":
|
||||||
|
body["msg"] = "!rright"
|
||||||
|
elif action == "restart":
|
||||||
|
body["msg"] = "!start"
|
||||||
|
channel = connection.channel()
|
||||||
|
channel.basic_publish("", "raw_messages", json.dumps(body))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/stream")
|
||||||
|
async def stream(request: Request):
|
||||||
|
rabbitmq_queue = os.environ['RABBITMQ_QUEUE_TETRIS_UI']
|
||||||
|
channel = connection.channel()
|
||||||
|
result = channel.queue_declare(queue=rabbitmq_queue)
|
||||||
|
channel.queue_bind(exchange=os.environ["RABBITMQ_EXCHANGE_TETRIS"], queue=rabbitmq_queue, routing_key="donno")
|
||||||
|
channel.basic_qos(prefetch_count=1)
|
||||||
|
|
||||||
|
async def event_generator():
|
||||||
|
while True:
|
||||||
|
if await request.is_disconnected():
|
||||||
|
break
|
||||||
|
|
||||||
|
method_frame, header_frame, body = channel.basic_get(rabbitmq_queue)
|
||||||
|
if method_frame:
|
||||||
|
print("message sent")
|
||||||
|
yield {
|
||||||
|
"data": body.decode('utf-8')
|
||||||
|
}
|
||||||
|
channel.basic_ack(method_frame.delivery_tag)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
return EventSourceResponse(event_generator(), media_type='text/event-stream')
|
||||||
3
twitch
Normal file
3
twitch
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
TWITCH_ACCESSTOKEN=<insert>
|
||||||
|
TWITCH_USERNAME=Ekouz
|
||||||
|
TWITCH_CHANNEL=ekouz
|
||||||
10
workers/Dockerfile
Normal file
10
workers/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
FROM python:3
|
||||||
|
|
||||||
|
COPY requirements.txt /tmp/
|
||||||
|
|
||||||
|
RUN pip install -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
COPY . /code
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
CMD python -m workers
|
||||||
1
workers/requirements.txt
Normal file
1
workers/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
twitchio
|
||||||
0
workers/workers/__init__.py
Normal file
0
workers/workers/__init__.py
Normal file
19
workers/workers/__main__.py
Normal file
19
workers/workers/__main__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import twitchio
|
||||||
|
from twitchio.ext import pubsub
|
||||||
|
|
||||||
|
BOT_NICK = 'Ekouz'
|
||||||
|
OAUTH_TOKEN = 'oauth:poc2uc91z5b49kowag0kjbn5zxxto7'
|
||||||
|
CHANNEL = 'ekouz'
|
||||||
|
|
||||||
|
|
||||||
|
client = twitchio.Client(token=OAUTH_TOKEN)
|
||||||
|
client.pubsub = pubsub.PubSubPool(client)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
topics = [
|
||||||
|
pubsub.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
client.loop.run_until_complete(main())
|
||||||
Loading…
Reference in a new issue