1
0

Compare commits

...

118 Commits

Author SHA256 Message Date
05e219ad18 Merge pull request 'bug solved in event picker' (#6) from green/battle_royale_sim:master into master
Reviewed-on: Cryz/battle_royale_sim#6
2025-08-11 22:14:05 +02:00
0d36f1dd52 bug solved in event picker 2025-08-10 15:32:47 +02:00
13bb11eb8c event picket to be tested but im lazy 2025-08-05 21:03:20 +02:00
af7f0019ec initial event picker 2025-08-04 21:10:12 +02:00
804a3961c9 Update README.md 2025-08-02 11:18:38 +02:00
andrea
cdb69699ab replace print with log_debug 2025-08-02 10:04:30 +02:00
andrea
6c3fe6326f restore map size 2025-08-02 09:55:16 +02:00
andrea
2550f0b262 fix movements 2025-08-02 09:52:20 +02:00
andrea
3cc6966d86 fix merge regression and exlude death player from attackable players 2025-08-02 09:33:06 +02:00
62c7c7f2c2 Update README.md 2025-08-02 09:06:07 +02:00
andrea
aa245700c6 fix and improve debug game simulation 2025-08-02 08:58:36 +02:00
andrea
7f36158c40 fix double kill on an already death player 2025-08-02 00:07:22 +02:00
andrea
171b9fe787 fix 2025-08-02 00:00:00 +02:00
andrea
eb3b7da07a start implement random action 2025-08-01 23:49:39 +02:00
andrea
8ed1bd3c4f bugfix and more complete resource handling on map 2025-08-01 23:00:58 +02:00
andrea
0379a3f935 texts 2025-08-01 22:30:41 +02:00
andrea
18bccfded8 fix conflicts 2025-08-01 22:27:10 +02:00
andrea
c6c53caeee define a resource class for every entity that needs to be controlled on the game map 2025-08-01 22:24:38 +02:00
andrea
a5831085ba define a resource class for every entity that needs to be controlled on the game map 2025-08-01 22:05:03 +02:00
andrea
9ad177b459 debug.py now has a list of 2 actions, attack and move, and every player get a random action from this list 2025-08-01 21:42:48 +02:00
andrea
f2c8acb80a player movements 2025-08-01 19:25:36 +02:00
andrea
47729ceeb9 bot log more user info 2025-08-01 19:20:23 +02:00
c30c46e20f test push 2025-07-30 22:51:10 +02:00
frostbite
51e3367a1c fixed SyntaxWarning backslash for python 3.13.0 2025-07-29 14:25:28 +02:00
andrea
e0dc558c27 try using a smaller map and no mountain to get more available space 2025-07-28 23:06:19 +02:00
andrea
29157e22d4 fix kills counter 2025-07-28 22:56:46 +02:00
andrea
53189e3fd8 outline cell on the image map to make is more clear 2025-07-28 22:39:17 +02:00
andrea
87c0cefb56 requirements 2025-07-28 22:30:31 +02:00
andrea
52cc55e611 added image map generator, map cell's color and emoji legend, death players emoji and color 2025-07-28 22:29:27 +02:00
andrea
56da6031f2 show map with players on telegram bot 2025-07-28 20:45:42 +02:00
andrea
5002890540 debug random map generator 2025-07-28 20:08:15 +02:00
andrea
9653965d2b show players and items on the map 2025-07-28 20:03:08 +02:00
andrea
37578437f6 clear error message for missing local_settings.py 2025-07-28 18:55:33 +02:00
frostbite
9b503872e8 fixed code 2025-07-28 14:16:03 +02:00
frostbite
e604743796 added end of the map 2025-07-28 12:50:52 +02:00
frostbite
4dcb87723d added mountain icon 2025-07-28 12:20:21 +02:00
frostbite
5c3e349cb6 added icon dead player 2025-07-28 12:12:43 +02:00
andrea
5e6816269b define players and items coordinates to be used on the map 2025-07-27 23:34:35 +02:00
andrea
a7fd66e9e4 define Map class with basics rendering and parsing methods 2025-07-27 23:23:21 +02:00
andrea
172b387dcc fix duplicate logs 2025-07-27 09:10:09 +02:00
andrea
4639d85f00 better loggings but i need to fix a duplicate log line error 2025-07-27 08:56:40 +02:00
andrea
24f0f3e3e1 daily logs with a new file hanlder 2025-07-27 08:40:01 +02:00
andrea
0bfa74c0e2 cleanings to make code more compact and readable 2025-07-27 08:17:12 +02:00
andrea
c82f31a101 less logs 2025-07-27 00:07:20 +02:00
andrea
b9161a052f fix logs command 2025-07-27 00:06:59 +02:00
andrea
bd0601e115 better log name 2025-07-26 23:59:35 +02:00
andrea
bee2e94f82 item 2025-07-26 23:10:51 +02:00
andrea
da0368d6c5 fix loop requesting seconds 2025-07-26 22:41:24 +02:00
andrea
f8a3e12909 less logs lines 2025-07-26 22:33:56 +02:00
111e5e04df Update README.md 2025-07-26 22:13:36 +02:00
1a0422414c Update README.md 2025-07-26 22:11:59 +02:00
af70d30279 Update README.md 2025-07-26 22:10:40 +02:00
308107fdc7 Update README.md 2025-07-26 22:09:47 +02:00
7c6e7fb7b4 Update README.md 2025-07-26 22:09:29 +02:00
876c55522d Update README.md 2025-07-26 22:08:58 +02:00
76a14d69ce Update README.md 2025-07-26 22:08:02 +02:00
aa46acc812 Update README.md 2025-07-26 22:06:36 +02:00
c1c8301e11 Update README.md 2025-07-26 22:06:04 +02:00
andrea
269724345d show logs inside the telegram bot 2025-07-26 21:58:38 +02:00
andrea
a9cb58100f README 2025-07-26 21:32:52 +02:00
andrea
8f3d6dfaff explicit logs 2025-07-26 21:25:38 +02:00
andrea
460a6ccdec clean bot and fix autostop with scheduled game 2025-07-26 20:58:35 +02:00
andrea
2bf65583b7 cleanings 2025-07-26 20:34:15 +02:00
andrea
5e530e1af1 move upstart code 2025-07-26 19:44:45 +02:00
andrea
ffbaa1c3b5 more logs 2025-07-26 19:30:13 +02:00
andrea
72ec279036 better logs format 2025-07-26 19:24:45 +02:00
andrea
f55afe5959 logging info 2025-07-26 19:13:32 +02:00
andrea
669701dbb7 typo 2025-07-26 19:12:00 +02:00
andrea
9b538e7d2f fix 2025-07-26 19:11:26 +02:00
andrea
4d3dd5c781 fix binary path 2025-07-26 19:09:34 +02:00
andrea
a948db5af3 add logging lib 2025-07-26 19:04:17 +02:00
andrea
11e4f9b2dd upstart bot 2025-07-26 18:38:40 +02:00
andrea
56e5e35b53 upstart bot 2025-07-26 18:28:46 +02:00
f55a6a2cdb Merge pull request 'master' (#5) from green/battle_royale_sim:master into master
Reviewed-on: Cryz/battle_royale_sim#5
2025-07-26 18:07:53 +02:00
andrea
4729a45c30 player stats review and minor bot improvements 2025-07-26 18:07:14 +02:00
5a6810e948 merge upstream 2025-07-26 17:57:18 +02:00
31637dc845 ranking as commands or at the end of each day 2025-07-26 17:56:54 +02:00
andrea
079dcd6639 README 2025-07-26 17:17:21 +02:00
andrea
65688b9100 add random health, random damage and show full player stats with emoji 2025-07-26 15:53:26 +02:00
andrea
308c9e3602 use internal method to get the player's gender 2025-07-26 15:36:22 +02:00
andrea
871b3d2ab1 fix player random extraction number 2025-07-26 15:35:45 +02:00
andrea
5f0d6ad3f5 fix bug where a death player is killed multiple time during the same day 2025-07-26 15:31:28 +02:00
andrea
551c2ef8ef cleans 2025-07-26 15:25:27 +02:00
andrea
4ce19d1a50 add kills counter random agility for each player and random init players 2025-07-26 15:20:03 +02:00
andrea
67804a394d split main bot lib into multiple smaller libs and improve end game message 2025-07-26 14:44:16 +02:00
andrea
415cdfc123 Merge branch 'green-master' 2025-07-26 14:08:24 +02:00
9f745bd451 player reputation for sponsor and alliance 2025-07-26 13:52:08 +02:00
andrea
fe356864c8 debug daily events, now all players plays during a day 2025-07-26 12:52:55 +02:00
andrea
c5a6b886d6 allow multiple players insertion 2025-07-26 12:38:06 +02:00
andrea
a7c6dd25df show winner player and break loop game if you executed the periodically game 2025-07-26 11:52:01 +02:00
andrea
888f22ac1b show better names on telegram message 2025-07-26 11:22:55 +02:00
andrea
0a6deafd32 improve message and add periodically run 2025-07-26 11:15:44 +02:00
andrea
100bb4fe53 implement bot keyboard with basics commands 2025-07-26 11:00:20 +02:00
andrea
f360fe4200 move syms location 2025-07-26 10:13:20 +02:00
andrea
0df960ea22 split player commands from main bot 2025-07-26 10:04:19 +02:00
andrea
634c651cb5 typo fix and better players output 2025-07-26 09:46:04 +02:00
Crystal
07dd5611e5 fix missing message return, and immediate first message of the day 2025-07-26 00:06:29 +02:00
Crystal
09c892a786 unused imports 2025-07-25 23:57:25 +02:00
Crystal
3d2a0bcc70 telegram bot more commands, and use the debug function to start a first day iteration 2025-07-25 23:41:44 +02:00
andrea
c10872a47e test 2025-07-25 21:57:53 +02:00
andrea
e8f2dbbc29 Merge branch 'master' of https://gitea.rpicloud.ovh/Cryz/battle_royale_sim 2025-07-25 21:56:28 +02:00
andrea
62ac0e83a5 test 2025-07-25 21:52:47 +02:00
frostbite
82be14e603 minor 2025-07-25 09:24:31 +02:00
Crystal
b12db42c0b more players 2025-07-24 23:07:33 +02:00
5986e0f880 initial event picker idea 2025-07-24 23:00:34 +02:00
Crystal
0bb2c840f9 lgbtq+ texts 2025-07-24 22:58:46 +02:00
526668ed5d initial event picker idea 2025-07-24 22:57:28 +02:00
Crystal
1c284bad97 basic player entity and debug module to quickly test player attack 2025-07-24 22:36:36 +02:00
Crystal
761cf794b4 fix requirements for job-queue 2025-07-24 21:36:34 +02:00
andrea
f193464006 bot reframe, based on local_settings TOKEN, and periodic run 2025-07-24 21:28:42 +02:00
andrea
c5a6f4ea36 syntax bugfix 2025-07-24 21:27:36 +02:00
andrea
5a672b336f requirements 2025-07-24 19:18:43 +02:00
andrea
05d1718775 is_alive() is now based on player's health instead of a dedicated state 2025-07-24 19:16:30 +02:00
e66a6fbd9c Merge pull request 'master' (#3) from frostbite/battle_royale_sim:master into master
Reviewed-on: Cryz/battle_royale_sim#3
2025-07-24 19:11:17 +02:00
frostbite
e038f711d5 Merge remote-tracking branch 'refs/remotes/origin/master' 2025-07-24 17:41:14 +02:00
frostbite
82141d89d6 escape of the combat 2025-07-24 17:39:49 +02:00
0098861e54 Merge pull request 'arena as class' (#2) from green/battle_royale_sim:master into master
Reviewed-on: Cryz/battle_royale_sim#2
2025-07-24 16:38:56 +02:00
db86f3dd06 arena as class 2025-07-24 16:08:06 +02:00
23 changed files with 1410 additions and 192 deletions

View File

@@ -1,3 +1,62 @@
# battle_royale_sim # Battle Royale Simulator (Hunger Games)
Hunger Games Simulator Hunger Games Simulator for telegram.
You can find the Official Bot here:
@Brsimgen_Bot - https://t.me/Brsimgen_Bot
Feel free to fork the project and make your own Bot
This game is inspired to this Hunger Games Simulator: https://brantsteele.net/hungergames/reaping.php
# Bot Instructions (Telegram)
1. start a chat with the bot: https://t.me/Brsimgen_Bot
2. open the bot keyboard
3. Press button "Init/Restart" to initialize the world
4. now is time to add players (max players is 70):
- you can manually add new players with button "Add Player" then you'll need to insert the player name (or multiple names comma separated)
- test
5. Now you can start with the game simulation
- press "Simulate Day" to make time elapse till the end of the day, and watch what's happend during the day
- press "Run Periodically" to make the bot do everything, you only need to insert the periodicity of the scheduler: for example 30
now every day will automatically end in 30 seconds
6. you can always check the game status with the buttons:
- Get Players: (see all the players)
- Get Alive Players: (list of alive players)
- Get Death Players: (list of death players)
- Get Ranking Players: (this is the leaderboard, based on number of kills)
- Show Map UTF8 or Show Map Image (show Players/Items position on the map)
# Bot Instructions (CLI)
1. open a python shell
2. `import debug`
3. `debug.init_debug_simulation()`
# Fork Instructions
if you want to fork this project
remember to create a file
`local_settings.py`
on the same folder of
`bot.py`
file
containing
```
TOKEN = 'your-bot-token'
BOT_PATH= '/the/path/of/the/project'
LOG_PATH= '/the/path/where/you/want/put/game/daily/logs'
BOT_EXEC_CMD= 'python3 bot.py' # or any other way you start the bot
SUPER_USERS= [ your_chat_id ]
```

27
assets/events.py Normal file
View File

@@ -0,0 +1,27 @@
EVENTS = [
{
'id' : 'ATTACK',
'text' : '{Player1} ha attacato {player2}',
'resolve_text' : '{Player1} ha causato {effetto_collaterale}',
'fail_text' : '{Player1} ha fallito, {player2} è indenne',
'success_percentage' : 80,
'fail_percentage' : 19,
'extreme_fail_percentage' : 1,
'requirements' : {},
'weight' : 1,
'affected_players' : 1,
},
{
'id' : 'BOMB_EXPLOSION',
'text' : '{Player1} ha attacato {player2}',
'resolve_text' : '{Player1} ha causato {effetto_collaterale}',
'fail_text' : '{Player1} ha fallito, {player2} è indenne',
'success_percentage' : 80,
'fail_percentage' : 19,
'extreme_fail_percentage' : 1,
'requirements' : {
'weapons' : ['BOMB']
},
'affected_players' : [2,3,4],
},
]

130
bot.py
View File

@@ -1,64 +1,104 @@
import asyncio from telegram.ext import Application
import datetime from telegram.ext import CommandHandler
from telegram import Update from telegram.ext import MessageHandler
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from telegram.ext import filters
import bot_syms as _botsyms from telegram import ReplyKeyboardMarkup
import main as _brsim from telegram import ReplyKeyboardRemove
async def loop_game(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: from utils import logs as _log
chat_id = update.effective_chat.id
if 'arena' in context.application.bot_data:
pass
else:
print('Arena non trovata')
await update.message.reply_text('Che e\' successo? un Guarino ha rubato l\'arena, avvia una nuova partita con /start')
async def bot_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: from entities import arena as _arena
from bot_libs import special_commands as _scmd
from bot_libs import syms as _botsyms
from bot_libs import commands_handling as _cmd
async def bot_start(update, context):
await update.message.reply_text(_botsyms.START_MSG) await update.message.reply_text(_botsyms.START_MSG)
if 'ask_name' in context.application.bot_data:
del(context.application.bot_data['ask_name'])
if 'ask_seconds' in context.application.bot_data:
del(context.application.bot_data['ask_seconds'])
keyboard = [
['Init/Restart'],
['Add Player', 'Add random Players', 'Add random color Players'],
['Get Players', 'Get Alive Players', 'Get Death Players', 'Get Ranking Players',],
['Simulate Day', 'Run Periodically', 'Show Map UTF8', 'Show Map Image']
]
reply_markup= ReplyKeyboardMarkup(keyboard, one_time_keyboard=False, resize_keyboard=True)
chat_id = update.effective_chat.id chat_id = update.effective_chat.id
print(f'{chat_id}: Sto costruendo il mondo di gioco...') _log.log_debug(f'bot_start: {chat_id} - I\'m building the world\'s game...')
Arena= _brsim.init_arena() Arena= _arena.BrSimArena()
players= Arena.get_players()
weapons= Arena.get_weapons()
print(f'Ecco il mondo di gioco, questi sono i giocatori: {players}')
print(f'Ecco le armi disponibili nel mondo: {weapons}')
await update.message.reply_text('Ho creato il mondo di gioco') await update.message.reply_text('Ho creato il mondo di gioco', reply_markup=reply_markup)
await update.message.reply_text(f'Ecco la lista degli sfortunati avventurieri:\n{players}')
await update.message.reply_text(f'Queste le armi che avranno a disposizione nell\'arena:\n{weapons}')
context.application.bot_data['arena'] = Arena context.application.bot_data['arena'] = Arena
context.job_queue.run_daily( async def bot_commands(update, context):
loop_game, text= update.message.text
time=datetime.time(hour=0, minute=0, second=5, tzinfo=datetime.timezone.utc), chat_id = update.effective_chat.id
chat_id=chat_id, username= update.effective_chat.username
name=str(chat_id) first_name= update.effective_chat.first_name
) last_name= update.effective_chat.last_name
print(f'Job giornaliero creato per la chat {chat_id}') chat_type= update.effective_chat.type.name # PRIVAT|BOT|ETC..
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
testo_ricevuto = update.message.text _log.log_info(f'bot_command: \
user id="{chat_id}" \
username="{username}" \
first_name="{first_name}" \
last_name="{last_name}" \
chat_type="{chat_type}"')
# init or restart the game
if text == 'Init/Restart':
return await bot_start(update, context)
# player game commands
if text == 'Add Player':
return await _cmd.cmd_add_player(context, update, chat_id)
if text == 'Get Players':
return await _cmd.cmd_get_players(context, update, chat_id)
if text == 'Get Alive Players':
return await _cmd.cmd_get_alive_players(context, update, chat_id)
if text == 'Get Death Players':
return await _cmd.cmd_get_death_players(context, update, chat_id)
if text == 'Get Ranking Players':
return await _cmd.cmd_get_ranking_players(context, update, chat_id)
if text == 'Simulate Day':
return await _cmd.cmd_simulate_day(context, update, chat_id)
if text == 'Run Periodically':
return await _cmd.cmd_simulate_day_cron(context, update, chat_id)
if text == 'Add random Players':
return await _cmd.cmd_add_random_players(context, update, chat_id)
if text == 'Add random color Players':
return await _cmd.cmd_add_random_color_players(context, update, chat_id)
if text == 'Show Map UTF8':
return await _cmd.cmd_show_game_map_unicode(context, update, chat_id)
if text == 'Show Map Image':
return await _cmd.cmd_show_game_map_image(context, update, chat_id)
# special commands
if text == 'upstart': return await _scmd.update_bot(update, context)
if text == 'logs': return await _scmd.show_logs(update, context)
# get user input
if context.application.bot_data.get('ask_name'):
return await _cmd.cmd_get_player_name(context, update, chat_id, text)
if context.application.bot_data.get('ask_seconds'):
return await _cmd.cmd_get_cron_time(context, update, chat_id, text)
_log.log_debug(f'bot_command: {chat_id} - sent this text: {text}')
await update.message.reply_text(_botsyms.WIP_MSG) await update.message.reply_text(_botsyms.WIP_MSG)
async def add_player(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
name= " ".join(context.args)
print(f'sto aggiungendo il giocatore {name} all\'arena')
_brsim.BrSimArena
Arena= context.application.bot_data['arena']
Arena.add_player(name)
print(f'Giocatori: {Arena.get_players()}')
await update.message.reply_text(f'Ecco i giocatori presenti nel mondo do gioco: \n{Arena.get_players()}')
def main(): def main():
application = Application.builder().token(_botsyms.TOKEN).build() application = Application.builder().token(_botsyms.TOKEN).build()
application.add_handler(CommandHandler('start', bot_start)) application.add_handler(CommandHandler('start', bot_start))
application.add_handler(CommandHandler('addplayer', add_player)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, bot_commands))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
print('Bot in esecuzione...') _log.log_info('main: Bot is running...')
application.run_polling() application.run_polling()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -0,0 +1,84 @@
from utils import logs as _log
from bot_libs import syms as _bot_syms
from bot_libs import player_handling as _bot_player
from bot_libs import simulation as _bot_simulation
from bot_libs import repeating as _bot_repeat
### parse user input
async def _cmd_list_of_players(context, update, chat_id):
Arena= context.application.bot_data['arena']
players= [p.get_name_and_stats() for p in Arena.get_players()]
players_str= '\n'.join(players)
return await update.message.reply_text(f'Ecco i {len(players)} giocatori presenti nel mondo do gioco: \n{players_str}')
async def cmd_get_player_name(context, update, chat_id, text):
_log.log_info(f'cmd_get_player_name: {chat_id} - Collected Player Name {text}')
if not 'ask_name' in context.application.bot_data: return
del(context.application.bot_data['ask_name'])
players= text.split(',')
for player in players:
await _bot_player.add_player(update, context, chat_id, player.strip())
return await _cmd_list_of_players(context, update, chat_id)
async def cmd_get_cron_time(context, update, chat_id, text):
_log.log_info(f'cmd_get_cron_time: {chat_id} - User Wants to auto-run the game every {text} seconds')
try: text= int(text)
except: return
seconds= max(1, text)
if 'ask_seconds' in context.application.bot_data:
del(context.application.bot_data['ask_seconds'])
return await _bot_repeat.start_loop_game(update, context, seconds)
### basic commands handling
async def cmd_add_random_players(context, update, chat_id):
await _bot_player.add_random_players(update, context, chat_id, colors_names= False)
return await _cmd_list_of_players(context, update, chat_id)
async def cmd_add_random_color_players(context, update, chat_id):
await _bot_player.add_random_players(update, context, chat_id, colors_names= True)
return await _cmd_list_of_players(context, update, chat_id)
async def cmd_add_player(context, update, chat_id):
context.application.bot_data['ask_name'] = 1
if 'ask_seconds' in context.application.bot_data:
del(context.application.bot_data['ask_seconds'])
return await update.message.reply_text('Inserisci il Nome del giocatore (o piu\' nomi separati da virgola)')
async def cmd_get_players(context, update, chat_id):
return await _bot_player.get_players(update, context, chat_id)
async def cmd_get_alive_players(context, update, chat_id):
return await _bot_player.get_alive_players(update, context, chat_id)
async def cmd_get_death_players(context, update, chat_id):
return await _bot_player.get_death_players(update, context, chat_id)
async def cmd_get_ranking_players(context, update, chat_id):
return await _bot_player.get_ranking_players(update, context, chat_id)
async def cmd_simulate_day(context, update, chat_id):
return await _bot_simulation.simulate_day(context, chat_id)
async def cmd_show_game_map_unicode(context, update, chat_id):
Arena= context.application.bot_data['arena']
Gamemap= Arena.get_map()
rendered_map= Gamemap.get_renderized_map()
await update.message.reply_text(rendered_map)
return await update.message.reply_text(_bot_syms.MAP_UTF8_LEGEND, parse_mode='MarkdownV2')
async def cmd_show_game_map_image(context, update, chat_id):
Arena= context.application.bot_data['arena']
Gamemap= Arena.get_map()
image_map= Gamemap.get_image_map()
bot= update.get_bot()
await bot.send_photo(chat_id= chat_id, photo= image_map)
return await update.message.reply_text(_bot_syms.MAP_IMAGE_LEGEND, parse_mode='MarkdownV2')
async def cmd_simulate_day_cron(context, update, chat_id):
context.application.bot_data['ask_seconds'] = 1
if 'ask_name' in context.application.bot_data:
del(context.application.bot_data['ask_name'])
return await update.message.reply_text('Inserisci il numero di secondi, ad esempio \n(60 = 1 minuto)(600 = 10 minuti)\n(3600 = 1 ora)\n(86400 = 1 giorno)')

View File

@@ -0,0 +1,53 @@
import random as _rand
from utils import logs as _log
from bot_libs import syms as _bot_syms
async def add_player(update, context, chat_id, name):
_log.log_info(f'add_player: {chat_id} - {name}')
Arena= context.application.bot_data['arena']
#if len(Arena.get_players()) >= 70: return # prevent message too long error
if len(Arena.get_players()) >= 20: return # maybe this should depend on the map dimension
Arena.add_player(name)
async def add_random_players(update, context, chat_id, colors_names= False):
if colors_names: names= _bot_syms.COLORS_NAMES
else: names= _bot_syms.RANDOM_NAMES
max_players= len(names)
min_players= min(7, max_players)
players_num= _rand.randint(min_players, max_players)
_rand.shuffle(names)
lucky_players= _rand.sample(names, players_num)
_log.log_info(f'add_random_players: {chat_id} - extracting {players_num} random players for the game')
for name in lucky_players: await add_player(update, context, chat_id, name)
async def get_players(update, context, chat_id):
Arena= context.application.bot_data['arena']
players= [p.get_name_and_stats() for p in Arena.get_players()]
players_str= '\n'.join(players)
_log.log_info(f'get_players: {chat_id} - {players_str}')
await update.message.reply_text(f'Ecco i giocatori presenti nel mondo do gioco: \n{players_str}')
async def get_alive_players(update, context, chat_id):
Arena= context.application.bot_data['arena']
alive= [a.get_name_and_stats() for a in Arena.get_alive_players()]
alive_str= '\n'.join(alive)
_log.log_info(f'get_alive_players: {chat_id} - {alive_str}')
await update.message.reply_text(f'Ecco i giocatori ancora vivi: \n{alive_str}')
async def get_death_players(update, context, chat_id):
Arena= context.application.bot_data['arena']
death= [d.get_name_and_stats() for d in Arena.get_death_players()]
death_str= '\n'.join(death)
_log.log_info(f'get_death_players: {chat_id} - {death_str}')
await update.message.reply_text(f'Ecco i giocatori morti x.x: \n{death_str}')
async def get_ranking_players(update, context, chat_id):
Arena = context.application.bot_data['arena']
leaderboard_text = Arena.get_ranking()
_log.log_info(f'get_ranking_players: {chat_id} - {leaderboard_text}')
await update.message.reply_text(
f'{leaderboard_text}',
parse_mode='Markdown'
)

24
bot_libs/repeating.py Normal file
View File

@@ -0,0 +1,24 @@
from utils import logs as _log
from bot_libs import simulation as _bot_sim
async def _loop_game(context):
chat_id = context.job.chat_id
_log.log_info(f'_loop_game: {chat_id} - run game simulation day')
return await _bot_sim.simulate_day(context, chat_id)
async def start_loop_game(update, context, seconds):
await update.message.reply_text(f'Ok capo!! giochero\' per te ogni {seconds}s')
chat_id = update.effective_chat.id
if 'arena' not in context.application.bot_data:
_log.log_info(f'start_loop_game: {chat_id} - Arena not found')
await update.message.reply_text(f'Arena non trovata, avviare con /start')
return
Arena= context.application.bot_data['arena']
if len(Arena.get_players()) < 2:
_log.log_info(f'start_loop_game: {chat_id} - Not enough player to start the match')
await update.message.reply_text(f'Servono almeno 2 giocatori. Ecco i giocatori presenti nel mondo do gioco: \n{Arena.get_players()}')
return
context.job_queue.run_repeating(_loop_game, interval= seconds, first=1, chat_id= chat_id)
_log.log_info(f'start_loop_game: {chat_id} - Cron job started')

41
bot_libs/simulation.py Normal file
View File

@@ -0,0 +1,41 @@
import debug as _dbg
from utils import logs as _log
def get_winner(context, Arena, chat_id):
winner= Arena.get_alive_players()[0]
try:
context.job.schedule_removal()
_log.log_info(f'simulate_day: {chat_id} Loop removed')
except: pass
msg= f'{winner.get_name_and_stats()} Vince la cruenta battaglia '
msg+= f'uccidendo {winner.get_kills()} giocatori '
msg+= f'e schivando {winner.get_dodges()} colpi nemici, e vive felice e '
if winner.player_gender_is_male():
msg+= 'contento con Guarino'
elif winner.player_gender_is_female():
msg+= 'contenta con Guarino'
else:
msg+= 'content# con Guarino'
return msg
async def simulate_day(context, chat_id):
if 'arena' not in context.application.bot_data:
_log.log_info('simulate_day: {chat_id} Arena not Found')
await context.bot.send_message(chat_id, 'Che e\' successo? un Guarino ha rubato l\'arena, avvia una nuova partita con /start')
return
Arena= context.application.bot_data['arena']
if len(Arena.get_alive_players()) <= 1: return await context.bot.send_message(chat_id, 'Il gioco e\' finito, Grazie per aver giocato!')
await context.bot.send_message(chat_id, f'Giorno #{Arena.day}')
msg= _dbg.play_one_day_debug(Arena)
await context.bot.send_message(chat_id, msg)
#Print the ranking each day
msg= Arena.get_ranking()
await context.bot.send_message(chat_id, msg)
if len(Arena.get_alive_players()) == 1:
msg= get_winner(context, Arena, chat_id)
return await context.bot.send_message(chat_id, msg)

View File

@@ -0,0 +1,50 @@
import os as _os
from utils import logs as _log
from bot_libs import syms as _botsyms
async def _pull_repo(update):
err= _os.system(f'cd {_botsyms.BOT_PATH}; /usr/bin/git pull')
if err:
_log.log_error(f'update_bot: {chat_id} error {err} while trying to update the app')
await update.message.reply_text('Errore durante l\'aggiornamento del Bot')
return err
async def _upstart_service(update):
err= _os.system(_botsyms.BOT_EXEC_CMD)
# this error is fake, probably due to systemd restart that make the bot istance broke
#if err:
#_log.log_error(f'update_bot: {chat_id} error {err} while trying to upstart the app')
#return await update.message.reply_text('Errore durante il riavvio del Bot')
async def update_bot(update, context):
chat_id = update.effective_chat.id
if update.message.chat.id not in _botsyms.SUPER_USERS:
return _log.log_warning(f'update_bot: user {chat_id} not allowed')
await update.message.reply_text('Sto aggiornando il Bot...')
_log.log_info(f'update_bot: {chat_id} bot is updating...')
err= await _pull_repo(update)
if err: return
err= await _upstart_service(update)
if err: return
_log.log_info(f'update_bot: {chat_id} bot successfully updated')
await update.message.reply_text('Bot aggiornato e riavviato!')
async def show_logs(update, context):
chat_id = update.effective_chat.id
if update.message.chat.id not in _botsyms.SUPER_USERS:
return _log.log_warning(f'open_logs: user {chat_id} not allowed')
await update.message.reply_text('Sto provando ad aprire i log...')
_log.log_debug(f'open_logs: {chat_id} trying opening logs...')
try:
game_log= _os.path.expanduser(f'{_log.get_log_name()}')
with open(game_log, 'r') as _log_file:
lines= _log_file.readlines()[-30:]
log_content= '\n'.join(lines)
await update.message.reply_text(f"Contenuto del log:\n{log_content}")
except Exception as e:
await update.message.reply_text(f"Errore nella lettura del file di log: {str(e)}")

115
bot_libs/syms.py Normal file
View File

@@ -0,0 +1,115 @@
import os as _os
from utils import logs as _logs
_MISSING_LOCAL_SETTINGS= """
=============== ERROR ===============
Missing local configurations:
create: local_settings.py
with these settings:
TOKEN= '<this_is_your_token_id>' # created using https://t.me/BotFather
BOT_PATH= '/path/of/your/repo/directory' # used for upstart command
LOG_PATH= '/path/where/you/place/logs' # used to save daily logs
BOT_EXEC_CMD= 'python /path/of/your/repo/directory/bot.py' # the command you use to launch the bot instance
SUPER_USERS= [<chat_id>] # this is the superuser, allowed to use special commands (upstart and logs)
=====================================
"""
try:
from local_settings import TOKEN as _token
TOKEN= _token
except: _logs.log_debug(_MISSING_LOCAL_SETTINGS)
try:
from local_settings import BOT_PATH as _bot_path
BOT_PATH= _os.path.expanduser(_bot_path)
except: BOT_PATH= ''
try:
from local_settings import LOG_PATH as _log_path
LOG_PATH= _os.path.expanduser(_log_path)
except: LOG_PATH= '/tmp'
try:
from local_settings import BOT_EXEC_CMD as _bot_exec_cmd
BOT_EXEC_CMD= _bot_exec_cmd
except: BOT_EXEC_CMD= None
try:
from local_settings import SUPER_USERS as _superusers
SUPER_USERS= _superusers
except: SUPER_USERS= []
START_MSG= """Benvenuto nel crudele mondo di Battle Royal Simulator,
La tua avventura e quella dei tuoi compagni inizia qui.
Questo Bot simula Hunger Games,
Tu e i tuoi compagni, sarete catapultati in questo mondo,
ma solo 1 di voi riuscira' a salvarsi.
Uccidi o sarai tu ad essere ucciso
"""
WIP_MSG= "Ehi, mio padre mi sta ancora finendo di creare, abbi pazienza, che fretta hai di entrare in questo mondo per morire?"
RANDOM_NAMES = [
"Aeliana", "Thorne", "Kael", "Seraphine", "Jaxon", "Lyra", "Darius", "Elowen",
"Zander", "Nyssa", "Orion", "Vesper", "Kieran", "Isolde", "Riven", "Calista",
"Draven", "Mira", "Zephyr", "Selene", "Ashen", "Talia", "Finnian", "Aria",
"Kaelan", "Liora", "Soren", "Elara", "Thalia", "Jett", "Cressida", "Lucian",
"Freya", "Ronan", "Niamh", "Kellan", "Zara", "Dorian", "Amara", "Jace",
"Elysia", "Caius", "Sable", "Alaric", "Veda", "Quinn", "Thorne", "Lirael",
"Rhea", "Kade", "Isadora", "Ash", "Nyx", "Cassian", "Elowen", "Tamsin",
"Rylan", "Faye", "Jorah", "Sienna", "Kieran", "Astra", "Zane", "Lyric",
"Dax", "Ember", "Orion", "Selah", "Juno", "Kaia", "Thorne", "Vespera",
"Riven", "Caden", "Liora", "Soren", "Elara", "Talia", "Jett", "Freya",
"Ronan", "Niamh", "Kellan", "Zara", "Dorian", "Amara", "Jace", "Elysia",
"Caius", "Sable", "Alaric", "Veda", "Quinn", "Thorne", "Lirael", "Rhea",
"Kade", "Isadora", "Ash", "Nyx",
]
COLORS_NAMES= [
"Yellow", "Grey", "Violet", "Black", "Lime", "Ruby", "Avocado", "Crystal",
"Pink", "Maize", "Coral", "Jade", "Platinum", "Emerald", "Carmine", "Nickel",
"Chocolate", "Slate", "Turquoise", "Silver", "Teal", "Jet", "Ivory", "Cobalt",
"Vermillion", "Aero", "Orange", "Rhythm", "Amber", "Olive", "Sepia", "Cyan",
"Green", "Ochre", "Denim", "Erin", "Fuchsia", "Aqua", "Iceberg", "Blue",
"Canary", "Red", "Mint", "Scarlet", "Coffee", "Indigo", "Mystic", "Rose",
"Pearl", "Pumpkin", "Navy", "Ultramarine", "Sapphire", "Desert", "Cherry",
"Tulip",
]
MAP_UTF8_FIELD= '🟩'
MAP_UTF8_MOUNTAIN= '⛰️'
MAP_UTF8_PLAYER_MALE= '👨'
MAP_UTF8_PLAYER_FEMALE= '👧🏻'
MAP_UTF8_PLAYER_NONBINARY= '⚧️'
MAP_UTF8_DEATH_PLAYER= '💀'
MAP_UTF8_ITEM= '📦'
MAP_UTF8_LEGEND= f"""*Legenda*:
- *{MAP_UTF8_FIELD}*: Cella *libera* per muoversi
- *{MAP_UTF8_MOUNTAIN}*: Bordo della mappa, *non raggiungibile*
- *{MAP_UTF8_PLAYER_MALE}*: Posizione di un *giocatore Maschio*
- *{MAP_UTF8_PLAYER_FEMALE}*: Posizione di una *giocatorice Femmina*
- *{MAP_UTF8_PLAYER_NONBINARY}*: Posizione di un *giocatore non binario*
- *{MAP_UTF8_DEATH_PLAYER}*: Posizione di un *giocatore morto*
- *{MAP_UTF8_ITEM}*: Posizione di un *oggetto* (non ancora implementato)
"""
MAP_IMAGE_FIELD= (0, 255, 0) # green
MAP_IMAGE_MOUNTAIN= (0, 0, 0) # black
MAP_IMAGE_PLAYER_MALE= (0, 0, 255) # blue
MAP_IMAGE_PLAYER_FEMALE= (255, 0, 0) # red
MAP_IMAGE_PLAYER_NONBINARY= (255, 255, 0) # yellow
MAP_IMAGE_DEATH_PLAYER= (160, 160, 160) # grey
MAP_IMAGE_ITEM= (255, 255, 255) # white
MAP_IMAGE_LEGEND= r"""*Legenda*:
- *Verde*: Cella *libera* per muoversi
- *Nero*: Bordo della mappa, *non raggiungibile*
- *Blue*: Posizione di un *giocatore Maschio*
- *Rosso*: Posizione di una *giocatorice Femmina*
- *Giallo*: Posizione di un *giocatore non binario*
- *Grigio*: Posizione di un *giocatore morto*
- *Bianco*: Posizione di un *oggetto* (non ancora implementato)
"""

View File

@@ -1,11 +0,0 @@
TOKEN = "7670066927:AAG8jI5n9NcyxPksYky7LPYqA08BThs07c4"
START_MSG= """Benvenuto nel crudele mondo di Battle Royal Simulator,
La tua avventura e quella dei tuoi compagni inizia qui.
Questo Bot simula Hunger Games,
Tu e i tuoi compagni, sarete catapultati in questo mondo,
ma solo 1 di voi riuscira' a salvarsi.
Uccidi o sarai tu ad essere ucciso
"""
WIP_MSG= "Ehi, mio padre mi sta ancora finendo di creare, abbi pazienza, che fretta hai di entrare in questo mondo per morire?"

119
debug.py Normal file
View File

@@ -0,0 +1,119 @@
import time as _time
import random as _rand
import main as _main
from utils import logs as _logs
from bot_libs import syms as _syms
def _debug_data():
players= _syms.COLORS_NAMES
weapons= []
Arena= _main.init_arena()
for player in players:
Arena.add_player(player)
return Arena
def _end_game_debug(alive_players, day):
last_player= alive_players[0]
msg= f'{last_player.get_name()} sopravvive e vince dopo {day} lunghi Giorni, conquistando l\'amore eterno di Guarino'
_logs.log_debug(msg)
return msg
def _random_action(Arena, Player_one):
_RANDOM_ACTIONS= {
1: 'attack',
2: 'move',
}
Map= Arena.get_map()
avail_actions= Map.get_player_available_actions(Player_one)
_logs.log_debug(f'{Player_one.get_name()}:{Player_one.get_coordinates()}, avail_actions: {avail_actions}')
msg= ''
if 1 in avail_actions:
# XXX maybe in future this action is available only if you are near to another player
# so Player_two is no more random, but will be a random near player
preys= avail_actions[1]
Player_two= _rand.sample(preys, 1)[0]
while Player_one.get_id() == Player_two.get_id() and not Player_two.is_alive():
Player_two= _rand.sample(preys, 1)[0]
_dmg, msg= Player_one.attack(Player_two)
elif 2 in avail_actions:
Map= Arena.get_map()
available_movements= Map.get_player_available_directions(Player_one)
_logs.log_debug(f'{Player_one.get_name()}:{Player_one.get_coordinates()}, avail_movements: {available_movements}')
if not available_movements:
# XXX probably should skip this action and look for another action
return f'{Player_one.get_name()} Pensa a Guarino tutto il giorno'
_rand.shuffle(available_movements)
x, y, direction= available_movements[0]
Player_one.move(x, y)
Map.init_map_matrix()
_logs.log_debug(Map.get_renderized_map())
msg= f'{Player_one.get_name()} Si muove verso »»» {direction}'
return msg
def play_one_day_debug(Arena):
if not Arena.get_players(): return
_logs.log_debug(f'Giorno #{Arena.day}')
alive_players= Arena.get_alive_players()
if len(alive_players) == 1:
day= Arena.day
return _end_game_debug(alive_players, day)
daily_events= []
_rand.shuffle(alive_players)
for Player_one in alive_players:
if not Player_one.is_alive(): continue # he could be dead during this day cycle
msg= _random_action(Arena, Player_one)
#p_two= _rand.sample(Arena.get_alive_players(), 1)[0]
#while Player_one.get_id() == p_two.get_id():
#p_two= _rand.sample(Arena.get_alive_players(), 1)[0]
#_dmg, msg= Player_one.attack(p_two)
daily_events.append(msg)
Arena.next_day()
res= '\n'.join(daily_events)
return res
#p_one, p_two= _rand.sample(alive_players, 2)
#_dmg, msg= p_one.attack(p_two)
#return msg
def init_debug_simulation():
Arena= _debug_data()
while (len(Arena.get_alive_players()) > 1):
events= play_one_day_debug(Arena)
_logs.log_debug('#################')
_logs.log_debug('#################')
_logs.log_debug('#################')
_logs.log_debug(events)
_logs.log_debug('#################')
_logs.log_debug('#################')
_logs.log_debug('#################')
_time.sleep(0.3)
def init_debug_loop():
Arena= _debug_data()
while (len(Arena.get_alive_players()) > 1):
alive_players= Arena.get_alive_players()
p_one, p_two= _rand.sample(alive_players, 2)
p_one.attack(p_two)
#Start a day
#At 23:59:
Arena.next_day()
_time.sleep(0.3)
#End of day
last_player= Arena.get_alive_players()[0]
_logs.log_debug(f'{last_player.get_name()} sopravvive e vince dopo {Arena.day} lunghi Giorni, conquistando l\'amore eterno di Guarino')
def debug_random_map():
from entities import map as _map;
from entities import player;
M= _map.BrSimMap(players= [player.BrSimPlayer(i) for i in range(20)]);
res= M.get_renderized_map()
_logs.log_debug(res)
if __name__ == '__main__':
init_debug()

134
entities/arena.py Normal file
View File

@@ -0,0 +1,134 @@
from entities import player as _player
from entities import event_picker as _events
from entities import gamemap as _map
from entities.items import weapons as _weapons
from utils import logs as _logs
class BrSimArena():
# players = [{'name': name, 'inventory': default_inventory, other_stats}]
# weapons = [{WEAPON.KNIFE: quantity}, etc...] # this is the whole quantity of the items available on the world
def __init__(self, players= None, weapons= None):
self.day= 1
self.players= []
self.weapons= []
self.eventClass = _events.ArenaEventPicker(self.players)
self.init_players(players)
self.init_weapons(weapons)
self.Map= _map.BrSimMap(self.players, self.weapons)
def init_players(self, players):
if not players: return
for player in players:
self.add_player(player['name'], player.get('inventory'))
def init_weapons(self, weapons):
if not weapons: return
for weapon in weapons:
for wtype, quantity in weapon.items():
for i in range(quantity): self.weapons.append(_weapons.BrSimWeapon(wtype))
def next_day(self):
self.day+= 1
_logs.log_debug(f'Giorno: {self.day}')
alive_players_str= ', '.join([p.get_name() for p in self.get_alive_players()])
_logs.log_debug(f'Giocatori vivi: {alive_players_str}')
death_players= self.get_death_players()
if (death_players):
death_players_str= ', '.join([p.get_name() for p in death_players])
_logs.log_debug(f'Giocatori morti: {death_players_str}')
def get_alive_players(self):
res= []
for p in self.players:
if not p.is_alive(): continue
res.append(p)
return res
def get_death_players(self):
res= []
for p in self.players:
if p.is_alive(): continue
res.append(p)
return res
def sort_players_by_kills_and_health(self, players):
def player_sort_key(player):
kills= player.get_kills()
health= player.get_health()
#Negative values to sort in decr mode
return (-kills, -health)
return sorted(players, key=player_sort_key)
def get_ranking(self):
medals = ['🥇', '🥈', '🥉']
leaderboard = []
leaderboard.append('🏆 *Classifica attuale:* \n')
alive_players= self.get_alive_players();
alive_sorted= self.sort_players_by_kills_and_health(alive_players)
death_players= self.get_death_players()
death_sorted= self.sort_players_by_kills_and_health(death_players)
# Alive Players
alive_players= self.get_alive_players();
alive_sorted= self.sort_players_by_kills_and_health(alive_players)
for index, player in enumerate(alive_sorted):
name = player.get_name()
kills = player.get_kills()
health = '♥️' * player.get_health()
if index < 3:
position = medals[index]
else:
position = f"{index + 1}."
line = f"{position} {name} - {kills} uccision{'i' if kills != 1 else 'e'}, {health}"
leaderboard.append(line)
# Death players:
death_players= self.get_death_players()
death_sorted= self.sort_players_by_kills_and_health(death_players)
if death_sorted:
leaderboard.append("\n-- GIOCATORI ELIMINATI --")
for player in death_sorted:
name = player.get_name()
kills = player.get_kills()
health = player.get_health()
line = f"💀 {name} - {kills} uccision{'i' if kills != 1 else 'e'}"
leaderboard.append(line)
return '\n'.join(leaderboard)
def do_random_event(self):
#XXX random player does random action according to his inventory health, wounds, available weapons on the world, etc...
pass
def supporter_donation(self):
#XXX supporter donate a random item or weapon to a random player
#TODO maybe in future a player can have charism stats that can influence the chance to get a donation
pass
def add_player(self, name, inventory= None):
player= _player.BrSimPlayer(name, inventory)
self.players.append(player)
self.Map.add_player_to_map(player)
def add_weapon(self, weapon_type):
weapon= _weapons.BrSimWeapon(weapon_type)
self.weapons.append(weapon)
self.Map.add_item_to_map(item)
def get_players(self):
return self.players
def get_weapons(self):
res= []
for w in self.weapons:
#XXX implement me
res.append(w)
return res
def get_map(self):
return self.Map

5
entities/event.py Normal file
View File

@@ -0,0 +1,5 @@
class Event():
def __init__(self, event_settings, the_player, affected_players = []):
# this will be the class that manage the event, so the result and what ahppens to the players
# will do it later
pass

81
entities/event_picker.py Normal file
View File

@@ -0,0 +1,81 @@
from assets.events import EVENTS
import random as _random
class ArenaEventPicker():
def __init__(self, players):
self.players = players
self.event_list = EVENTS
self.already_picked_players = []
def pick_a_player():
the_player = _random.choice(self.players)
the_player_id = the_player.get('id')
self.players = list(filter(lambda x : x.get('id') != the_player_id))
self.already_picked_players.append(the_player)
return the_player
def pick_event(the_player):
player_inventory = the_player.get('inventory') or []
status = the_player['health']
reputation = the_player['reputation']
elegible_events = []
for event in EVENTS:
requirements = event['requirements']
keys_to_check = ['item', 'weapon', 'status', 'reputation', 'affected_players']
for check in keys_to_check:
if requirements.get(check) and check == 'item':
needed_items = requirements.get('check')
if needed_items in player_inventory:
elegible_events.append(event)
if requirements.get(check) and check == 'weapon':
needed_weapons = requirements.get('check')
if needed_items in player_inventory:
elegible_events.append(event)
if requirements.get(check) and check == 'status':
needed_health = requirements.get('check')
if '>' in requirements.get(check) and requirements.get(check) > needed_health:
elegible_events.append(event)
if '<' in requirements.get(check) and requirements.get(check) < needed_health:
elegible_events.append(event)
if requirements.get(check) and check == 'reputation':
needed_reputation = requirements.get('reputation')
if '>' in requirements.get(check) and requirements.get(check) > needed_reputation:
elegible_events.append(event)
if '<' in requirements.get(check) and requirements.get(check) < needed_reputation:
elegible_events.append(event)
if check == 'affected_players':
needed_players = event.get('affected_players')
if needed_players < len(self.players):
elegible_events.append(event)
the_event = _random.choice(elegible_events)
return the_event
def pick_targets(number_of_targets):
random.shuffle(self.players)
return list(self.players[0:number_of_targets])
def resolve_event():
playing_player = self.pick_a_player()
assigned_event = self.pick_event(playing_player)
affected_players = assigned_event['affected_players']
if isinstance(affected_players,list):
affected_players = _random.choice(affected_players)
targeted_players = self.pick_targets(affected_players)
event_instance = _event.Event(assigned_event, playing_player)

235
entities/gamemap.py Normal file
View File

@@ -0,0 +1,235 @@
import random as _random
import copy as _copy
import io as _io
from PIL import Image as _Image
from PIL import ImageDraw as _ImageDraw
from utils import logs as _logs
from bot_libs import syms as _bot_syms
from entities import resource as _resource
from utils import logs as _logs
class BrSimMap():
def __init__(self, players= None, items= None):
self.players= players or []
self.items= items or []
self.world_width= 10 #seems a reasonable width for smartphones larger maps would go on a new line
self.world_height= 10
self.game_map= []
self.field_sym= _bot_syms.MAP_UTF8_FIELD
self.player_male_sym= _bot_syms.MAP_UTF8_PLAYER_MALE
self.player_female_sym= _bot_syms.MAP_UTF8_PLAYER_FEMALE
self.player_nonbinary_sym= _bot_syms.MAP_UTF8_PLAYER_NONBINARY
self.dead_player_sym= _bot_syms.MAP_UTF8_DEATH_PLAYER
self.item_sym= _bot_syms.MAP_UTF8_ITEM
self.mountain_sym = _bot_syms.MAP_UTF8_MOUNTAIN
self.init_map_matrix()
self.init_players_coordinates()
self.init_items_coordinates()
self.populate_map()
def init_map_matrix(self):
# show a matrix representing the game's map
# 🟩 is and empty cell
# (tomorrow we can choose different colors for different locations
# 🟠 this is a player (we could use different colors for different genders)
# 📦 this is an item (weapon or another item)
# 💀 this is icon when the player is dead
# ⛰️ this is icon for the mountain (We can prevent players from passing through the mountains and thus use them for map boundaries.)
self.game_map= []
width= []
#mon = []
#for i in range(self.world_width):
#mon.append(self.mountain_sym)
for i in range(self.world_width):
#if i == 0 or i == self.world_width - 1: width.append(self.mountain_sym)
#else: width.append(self.field_sym)
width.append(None)
#width.append(self.field_sym)
for i in range(self.world_height):
#if i == 0 or i == self.world_height - 1: self.game_map.append(mon)
#else: self.game_map.append(_copy.deepcopy(width))
self.game_map.append(_copy.deepcopy(width))
_logs.log_debug(f'init_map_matrix: {self.game_map}')
def populate_map(self):
for player in self.players:
p_coord_x, p_coord_y= player.get_coordinates()
self.game_map[p_coord_y][p_coord_x]= player
#if not player.is_alive(): self.game_map[p_coord_y][p_coord_x]= self.dead_player_sym
#elif player.player_gender_is_male(): self.game_map[p_coord_y][p_coord_x]= self.player_male_sym
#elif player.player_gender_is_female(): self.game_map[p_coord_y][p_coord_x]= self.player_female_sym
#else: self.game_map[p_coord_y][p_coord_x]= self.player_nonbinary_sym
for item in self.items:
i_coord_x, i_coord_y= item.get_coordinates()
self.game_map[p_coord_y][p_coord_x]= item
#self.game_map[i_coord_y][i_coord_x]= self.item_sym
def _put_resource_on_map(self, target):
#x= _random.randint(1, self.world_width -2) # from 1 to width-2 because 1 cell is occupied by the mountain
#y= _random.randint(1, self.world_height -2)
x= _random.randint(0, self.world_width -1)
y= _random.randint(0, self.world_height -1)
resource= self.get_map_matrix()[y][x]
while resource:
#while self.get_map_matrix()[y][x] != self.field_sym:
_logs.log_debug('_put_resource_on_map: collision, regenerate coordinates')
x= _random.randint(0, self.world_width -1)
y= _random.randint(0, self.world_height -1)
resource= self.get_map_matrix()[y][x]
_logs.log_debug(f'{target.get_name()} >>> ({x},{y})')
target.set_coordinates(x, y)
self.get_map_matrix()[y][x]= target
def init_players_coordinates(self):
for player in self.players:
self._put_resource_on_map(player)
def init_items_coordinates(self):
for item in self.items:
self._put_resource_on_map(item)
def add_player_to_map(self, player):
self.players.append(player)
self._put_resource_on_map(player)
def add_item_to_map(self, item):
self.items.append(item)
self._put_resource_on_map(item)
def get_map_matrix(self):
return self.game_map
def get_player_available_directions(self, Player):
coord_x, coord_y= Player.get_coordinates()
avail_directions= []
#XXX for now move only on available cells, no over other players/items
for shift in [-1, 1]:
x= coord_x + shift
if x < 0 or x > self.world_width -1: continue
resource= self.get_map_matrix()[coord_y][x]
direction= shift == -1 and 'sinistra' or 'destra'
if not resource: avail_directions.append((shift, 0, direction))
for shift in [-1, 1]:
y= coord_y + shift
if y < 0 or y > self.world_height -1: continue
resource= self.get_map_matrix()[y][coord_x]
direction= shift == -1 and 'su' or 'giu\''
if not resource: avail_directions.append((0, shift, direction))
return avail_directions
def check_near_players(self, Player):
# TODO Implement me
# 1. range weapons like arch can attack from distance
# 2. knife, sword and punch can attack only on immediate near cell
coord_x, coord_y= Player.get_coordinates()
attackable_players= []
for shift in [-1, 1]:
x= coord_x + shift
if x < 0 or x >= self.world_width -1: continue
resource= self.get_map_matrix()[coord_y][x]
if resource and resource.is_player() and resource.is_alive(): attackable_players.append(resource)
for shift in [-1, 1]:
y= coord_y + shift
if y < 0 or y >= self.world_height -1: continue
resource= self.get_map_matrix()[y][coord_x]
if resource and resource.is_player() and resource.is_alive(): attackable_players.append(resource)
return attackable_players
def check_near_items(self, Player):
# TODO Implement me
return []
def get_player_available_actions(self, Player):
# TODO: define actions list
coord_x, coord_y= Player.get_coordinates()
avail_actions= {}
attack= self.check_near_players(Player)
if attack:
_logs.log_debug(f'{Player.get_name()} can attack {[a.get_name() for a in attack]}')
#avail_actions.append(1) #XXX replace with attack action (or maybe other actions on players)
avail_actions[1]= attack #XXX replace with attack action (or maybe other actions on players)
if self.get_player_available_directions(Player):
avail_actions[2]= True #XXX replace with action move
items= self.check_near_items(Player)
if items:
avail_actions[3]= items #XXX replace with get item action
return avail_actions
def get_renderized_map(self):
res= ''
self.populate_map()
game_map= self.get_map_matrix()
for y in game_map:
for x in y:
if not x: el= self.field_sym
#XXX how to manage mountains?
elif x.is_player():
if not x.is_alive(): el= self.dead_player_sym
elif x.player_gender_is_male(): el= self.player_male_sym
elif x.player_gender_is_female(): el= self.player_female_sym
else: el= self.player_nonbinary_sym
elif x.is_item():
el= self.item_sym
res+= el
res+= '\n'
return res
def get_image_map(self):
self.populate_map()
scale_x= 20
scale_y= 20
final_x= self.world_width * scale_x
final_y= self.world_height * scale_y
image = _Image.new('RGB', (final_x, final_y))
draw = _ImageDraw.Draw(image)
outline= '#000000'
for y in range(self.world_height):
for x in range(self.world_width):
resource= self.game_map[y][x]
if not resource:
pixel_color= _bot_syms.MAP_IMAGE_FIELD
# XXX how to manage mountains? maybe another class?
#elif resource == self.mountain_sym:
#pixel_color= _bot_syms.MAP_IMAGE_MOUNTAIN
elif resource.is_item():
pixel_color= _bot_syms.MAP_IMAGE_ITEM
elif resource.is_player():
if not resource.is_alive():
pixel_color= _bot_syms.MAP_IMAGE_DEATH_PLAYER
elif resource.player_gender_is_male():
pixel_color= _bot_syms.MAP_IMAGE_PLAYER_MALE
elif resource.player_gender_is_female():
pixel_color= _bot_syms.MAP_IMAGE_PLAYER_FEMALE
elif resource.player_gender_is_not_binary():
pixel_color= _bot_syms.MAP_IMAGE_PLAYER_NONBINARY
scaled_x_coord= x * scale_x
scaled_y_coord= y * scale_y
# if x == 1 distance from top-right is 20 (because everything is 20x bigger)
# then we want to draw a rectanghe 20x20 (instead of 1x1)
# this mean that if x == 1 (20px from top-right), x+1 == 2 (40px from top-right)
# the same for y
# this means that i keep the same factor proportions but 20x bigger
scaled_x_width= (x + 1) * scale_x
scaled_y_height= (y + 1) * scale_y
draw.rectangle([scaled_x_coord, scaled_y_coord, scaled_x_width, scaled_y_height],
fill= pixel_color, outline= outline
)
# debug
#image.save('/tmp/battle_royale_map.png')
#image.show()
bio = _io.BytesIO()
image.save(bio, 'PNG')
bio.seek(0)
return bio

27
entities/items/item.py Normal file
View File

@@ -0,0 +1,27 @@
import random as _random
from entities import resource as _resource
class BrSimItem(_resource.BrSimResource):
# test
def __init__(self):
self.coord_x= 0
self.coord_y= 0
def is_item(self):
return True
def get_name(self):
return self.name
def get_item_type(self):
return self.item_type
def get_weight(self):
return self.weight
def is_weapon(self):
return False
def is_cure(self):
return False

View File

@@ -1,6 +1,5 @@
KNIFE= 1 KNIFE= 1
GUN= 2 ARCH= 2
BOMB= 3
SHORT_RANGE= 1 SHORT_RANGE= 1
FAR_RANGE= 2 FAR_RANGE= 2
@@ -14,7 +13,7 @@ WEAPONS= {
'ammons': -1, # -1, no limit 'ammons': -1, # -1, no limit
'range': SHORT_RANGE, 'range': SHORT_RANGE,
}, },
GUN: { ARCH: {
'weight': 2, 'weight': 2,
'name': 'gun', 'name': 'gun',
'damage': 3, 'damage': 3,
@@ -22,12 +21,4 @@ WEAPONS= {
'ammons': 10, # -1, no limit 'ammons': 10, # -1, no limit
'range': FAR_RANGE, 'range': FAR_RANGE,
}, },
BOMB: {
'weight': 2,
'name': 'bomb',
'damage': 10,
'miss_chance': 5, # from 0 to 100, this is the probably to miss the hit
'ammons': 1,
'range': FAR_RANGE,
},
} }

View File

@@ -1,10 +1,11 @@
import random as _random import random as _random
from entities import weapon_syms as _syms from entities.items import item as _item
from entities.items import weapon_syms as _syms
class BrSimWeapon(): class BrSimWeapon(_item.BrSimItem):
def __init__(self, wtype): def __init__(self, wtype= None):
self.weapon= _syms.WEAPONS[wtype] self.weapon= _syms.WEAPONS[wtype or _random.randint(1,2)]
self.name= self.weapon['name'] self.name= self.weapon['name']
self.damage= self.weapon['damage'] self.damage= self.weapon['damage']
self.weight= self.weapon['weight'] self.weight= self.weapon['weight']

View File

@@ -1,71 +1,208 @@
import random as _random import random as _random
import uuid as _uuid
from entities import resource as _resource
from utils import logs as _logs
class BrSimPlayer(): class BrSimPlayer(_resource.BrSimResource):
def __init__(self, name, inventory= None): def __init__(self, name, inventory= None):
super()
self.id= str(_uuid.uuid4())
self.name= name self.name= name
self.health= 1 self.stats= ''
self.health= _random.randint(1,1)
self.inventory= inventory or [] self.inventory= inventory or []
self.damage= 1 # this is the punch damage amount self.damage= _random.randint(1,2) # this is the punch damage amount
self.max_weight= 5 # this is the max inventory weight self.max_weight= 5 # this is the max inventory weight
self.is_alive= True self.agility= _random.randint(1,3) # chance to avoid an hit
self.agility= 10 # chance to avoid an hit self.kills= 0 # track the number of kills
self.accused_damage= 0
self.survived_days= 0 # track the number of the survived days
self.dodges= 0
self.equipped_weapon= None
self.gender= _random.sample(['m', 'f', '-'], 1)[0] # for now get a random gender
self.reputation= 50 #Like RDR2 the player can be evil(0) or good(100). This should influence the sponsors and internal alliance
def is_alive(self): def is_player(self):
return self.is_alive return True
def attack(self, target): ### control methods
if not self.is_alive(): return
if not target.is_alive(): return
if target.try_to_avoid_hit(): return # print something like 'enemy doges the attacl'
target.accuses_damage(self.damage)
def accuses_damage(self, damage): def get_name_and_stats(self):
self.health -= damage health= '♥️' * self.health or '☠️'
if self.health <= 0: strength= '⚔️' * self.damage
self.health = 0 agility= '🚀' * self.agility
self.is_alive = False if self.player_gender_is_male(): gender= ''
# show something like 'player is dead' elif self.player_gender_is_female(): gender= ''
else: else: gender= ''
# show something like 'get hit' name= f'{self.name} {gender} {health} {strength} {agility}'
pass return name
def try_to_avoid_hit(self): def get_id(self):
# maybe depend on the attack, if it is a gun shot it's quite impossible to dodge return self.id
rnd= _random.randint(0, 100)
# if rnd > self.agility: return True ## XXX this is strange, if the agility is high the chances to dodge are lower
if rnd < self.agility: return True
return False
def steal(self):
#XXX can steal from death players or from sleeping players
pass
def heal(self):
#XXX if you have a wound and you have a medikit item, you can heal your wound or sickness
pass
def get_inventory(self):
return self.inventory
def get_name(self): def get_name(self):
return self.name return self.name
def get_gender(self):
return self.gender
def player_gender_is_male(self):
return self.gender == 'm'
def player_gender_is_female(self):
return self.gender == 'f'
def player_gender_is_not_binary(self):
return self.gender == '-'
def get_inventory(self):
return self.inventory
def get_inventory_weight(self):
weight= 0
for item in self.get_inventory():
weight+= item.get_weight()
return weight
def get_dodges(self):
return self.dodges
def get_max_weight(self):
return self.max_weight
def get_health(self): def get_health(self):
return self.health return self.health
def get_equipped_weapon(self):
return self.equipped_weapon
def get_damage(self): def get_damage(self):
return self.damage weapon= self.get_equipped_weapon()
if not weapon: return self.damage
return weapon.get_damage()
def get_agility(self): def get_agility(self):
return self.agility return self.agility
def get_reputation(self):
return self.reputation
def get_data(self): def get_data(self):
return { return {
'id': self.get_id(),
'name': self.get_name(), 'name': self.get_name(),
'name_stats': self.get_name_and_stats(),
'gender': self.get_gender(),
'inventory': self.get_inventory(), 'inventory': self.get_inventory(),
'inventory_weight': self.get_inventory_weight(),
'health': self.get_health(), 'health': self.get_health(),
'damage': self.get_damage(), 'damage': self.get_damage(),
'agility': self.get_agility(), 'agility': self.get_agility(),
'reputation': self.get_reputation(),
} }
def is_alive(self):
return self.health > 0
def get_kills(self):
return self.kills
### player actions
def _equip_weapon(self):
if not self.inventory: return
available_weapons= []
for item in self.get_inventory():
# XXX
# i don't know yet if this is ok,
# we'll see it when weapon and items are defined
# maybe we need item.is_weapon() method
if not item.damage: continue
available_weapons.append(item)
self.equipped_weapon= random.sample(available_weapons, 1)[0]
def dodge(self):
# maybe depend on the attack, if it is a gun shot it's quite impossible to dodge
rnd= _random.randint(0, 10)
if rnd < self.agility:
self.dodges+= 1
return True
return False
def accuses_damage(self, damage):
self.health-= damage
self.accused_damage+= damage
if self.health > 0: return self.get_health()
self.health= 0
if self.player_gender_is_male():
_logs.log_debug(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono morto x.x')
elif self.player_gender_is_female():
_logs.log_debug(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono morta x.x')
else:
_logs.log_debug(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono mort* x.x')
return damage
def attack(self, target):
self._equip_weapon()
if target.dodge():
if target.player_gender_is_male():
msg= f'Ehhhh voleviiii!!! sei lentoo! {target.get_name_and_stats()} schiva il colpo di {self.get_name_and_stats()}'
elif target.player_gender_is_female():
msg= f'Ehhhh voleviiii!!! sei lentaa! {target.get_name_and_stats()} schiva il colpo di {self.get_name_and_stats()}'
else:
msg= f'Ehhhh voleviiii!!! sei lent##! {target.get_name_and_stats()} schiva il colpo di {self.get_name_and_stats()}'
return 0, msg
target.accuses_damage(self.damage)
msg= f'{self.get_name_and_stats()} Colpisce {target.get_name_and_stats()}'
weapon= self.get_equipped_weapon()
if weapon: msg+= f' con un {weapon.get_name}'
else: msg+= f' con un pugno'
if not target.is_alive(): self.kills+= 1
return self.damage, msg
def get_item(self, item):
if self.get_inventory_weight() + item.get_weight() >= self.get_max_weight():
if self.player_gender_is_male():
_logs.log_debug(f'Sono sovraccarico, {self.get_name_and_stats} non puo\' prendere questo oggetto')
elif self.player_gender_is_female():
_logs.log_debug(f'Sono sovraccarica, {self.get_name_and_stats} non puo\' prendere questo oggetto')
else:
_logs.log_debug(f'Sono sovraccaric#, {self.get_name_and_stats} non puo\' prendere questo oggetto')
return False
self.inventory.append(item)
def move(self, delta_x, delta_y):
# XXX maps limits:
# probably this isn't player's business
# game orchestror should manage it
# to avoid that the player can go out from the map
# or can reach unaccessible points
# also because the player doens't know the Map (entities/gamemap.py)
self.coord_x += delta_x
self.coord_y += delta_y
def move_right(self):
self._move(1, 0)
def move_left(self):
self._move(-1, 0)
def move_top(self):
self._move(0, -1)
def move_bottom(self):
self._move(0, 1)
def escape(self):
# TODO It can run away from the fighting
return
def heal(self):
# TODO heal system
# if you have a wound and you have a medikit item,
# you can heal your wound or sickness
return

18
entities/resource.py Normal file
View File

@@ -0,0 +1,18 @@
class BrSimResource():
def __init__(self):
self.coord_x= 0
self.coord_y= 0
def is_player(self):
return False
def is_item(self):
return False
def get_coordinates(self):
return self.coord_x, self.coord_y
def set_coordinates(self, x, y):
self.coord_x= x
self.coord_y= y

84
main.py
View File

@@ -1,82 +1,8 @@
from entities import player as _player from entities import arena as _arena
from entities import weapons as _weapons
from entities import weapon_syms as _wsyms
class BrSimArena(): def init_arena(players= None, weapons= None):
return _arena.BrSimArena(players, weapons)
# players = [{'name': name, 'inventory': default_inventory, other_stats}] def run_events(Arena):
# weapons = [{WEAPON.KNIFE: quantity}, etc...] # this is the whole quantity of the items available on the world #A event for each player:
def __init__(self, players, weapons):
self.day= 1
self.players= [_player.BrSimPlayer(p['name'], p.get('inventory')) for p in players]
self.weapons= []
for weapon in weapons:
for wtype, quantity in weapon.items():
for i in range(quantity): self.weapons.append(_weapons.BrSimWeapon(wtype))
def next_day(self):
self.day+= 1
print(f'Giorno: {self.day}')
print(f'Giocatori vivi: {self.get_alive_players()}')
death_players= self.get_death_players()
if (death_players):
print(f'Giocatori morti: {death_players}')
def get_alive_players(self):
res= []
for p in self.players:
if not p.is_alive(): continue
res.append(p)
return res
def get_death_players(self):
res= []
for p in self.players:
if p.is_alive(): continue
res.append(p)
return res
def do_random_event(self):
#XXX random player does random action according to his inventory health, wounds, available weapons on the world, etc...
pass
def supporter_donation(self):
#XXX supporter donate a random item or weapon to a random player
#TODO maybe in future a player can have charism stats that can influence the chance to get a donation
pass
def add_player(self, name, inventory= None):
player= _player.BrSimPlayer(name, inventory)
self.players.append(player)
def get_players(self):
res= []
for p in self.players:
res.append(p.get_data())
return res
def get_weapons(self):
res= []
for w in self.weapons:
#XXX implement me
res.append(w)
return res
def init_arena():
players= [{'name': 'Crystal'}, {'name': 'Andrea'}]
w= _wsyms.KNIFE
#weapons= [{_wsyms.WEAPONS[w]['name' ]: 1}]
weapons= [{w: 1}]
return BrSimArena(players, weapons)
def run_event(Arena):
pass pass
def local_debug():
Arena= init_arena()
print(f'Players: {Arena.get_players()}')
print(f'Weapons: {Arena.get_weapons()}')
run_event(Arena)

View File

@@ -0,0 +1,2 @@
python-telegram-bot[job-queue]==22.3
pillow==11.3.0

60
utils/logs.py Normal file
View File

@@ -0,0 +1,60 @@
import os as _os
import logging as _logging
from datetime import datetime as _dt
from bot_libs import syms as _bot_syms
logger= _logging.getLogger(__name__)
file_handler= None
def get_log_name():
now= _dt.now()
year= now.year
month= now.month
if month < 10: month= f'0{month}'
day= now.day
if day < 10: day= f'0{day}'
fname= f'battle_royale-{now.year}{month}{day}.log'
return _os.path.join(_bot_syms.LOG_PATH, fname)
def _create_file_handler():
global file_handler
current_log_file = get_log_name()
file_handler = _logging.FileHandler(current_log_file, encoding='utf-8')
formatter = _logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
#logger.setLevel(_logging.INFO)
logger.setLevel(_logging.DEBUG)
return file_handler
def _setup_logging_file():
current_log_file = get_log_name()
if not file_handler: return _create_file_handler()
if file_handler.baseFilename == _os.path.abspath(current_log_file): return
logger.removeHandler(file_handler)
file_handler.close()
_create_file_handler()
def log_debug(txt):
print(txt)
_setup_logging_file()
logger.debug(txt)
def log_info(txt):
_setup_logging_file()
logger.info(txt)
def log_warning(txt):
_setup_logging_file()
logger.warning(txt)
def log_error(txt):
_setup_logging_file()
logger.error(txt)
def log_critical(txt):
_setup_logging_file()
logger.critical(txt)