Compare commits

..

45 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: #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
17 changed files with 680 additions and 112 deletions

View File

@@ -7,7 +7,9 @@ You can find the Official Bot here:
Feel free to fork the project and make your own Bot
# Bot Instrictions
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
@@ -24,6 +26,13 @@ Feel free to fork the project and make your own Bot
- 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
@@ -45,6 +54,8 @@ 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 ]

View File

@@ -7,8 +7,21 @@ EVENTS = [
'success_percentage' : 80,
'fail_percentage' : 19,
'extreme_fail_percentage' : 1,
'required_items' : [],
'requirements' : {},
'weight' : 1,
'number_of_players' : 2,
'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],
},
]

59
bot.py
View File

@@ -8,9 +8,9 @@ from telegram import ReplyKeyboardRemove
from utils import logs as _log
from entities import arena as _arena
from bot_libs import special_commands as _bot_special_cmd
from bot_libs import special_commands as _scmd
from bot_libs import syms as _botsyms
from bot_libs import commands_handling as _bot_commands
from bot_libs import commands_handling as _cmd
async def bot_start(update, context):
@@ -24,7 +24,7 @@ async def bot_start(update, context):
['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']
['Simulate Day', 'Run Periodically', 'Show Map UTF8', 'Show Map Image']
]
reply_markup= ReplyKeyboardMarkup(keyboard, one_time_keyboard=False, resize_keyboard=True)
@@ -38,37 +38,56 @@ async def bot_start(update, context):
async def bot_commands(update, context):
text= update.message.text
chat_id = update.effective_chat.id
username= update.effective_chat.username
first_name= update.effective_chat.first_name
last_name= update.effective_chat.last_name
chat_type= update.effective_chat.type.name # PRIVAT|BOT|ETC..
_log.log_info(f'bot_command: {chat_id} - text received: {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 _bot_commands.bot_command_add_player(context, update, chat_id)
return await _cmd.cmd_add_player(context, update, chat_id)
if text == 'Get Players':
return await _bot_commands.bot_command_get_players(context, update, chat_id)
return await _cmd.cmd_get_players(context, update, chat_id)
if text == 'Get Alive Players':
return await _bot_commands.bot_command_get_alive_players(context, update, chat_id)
return await _cmd.cmd_get_alive_players(context, update, chat_id)
if text == 'Get Death Players':
return await _bot_commands.bot_command_get_death_players(context, update, chat_id)
return await _cmd.cmd_get_death_players(context, update, chat_id)
if text == 'Get Ranking Players':
return await _bot_commands.bot_command_get_ranking_players(context, update, chat_id)
return await _cmd.cmd_get_ranking_players(context, update, chat_id)
if text == 'Simulate Day':
return await _bot_commands.bot_command_simulate_day(context, update, chat_id)
return await _cmd.cmd_simulate_day(context, update, chat_id)
if text == 'Run Periodically':
return await _bot_commands.bot_command_simulate_day_cron(context, update, chat_id)
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)
waiting_for_name= context.application.bot_data.get('ask_name')
if waiting_for_name or text in ['Add random Players', 'Add random color Players']:
return await _bot_commands.get_name_from_input(context, update, chat_id, text)
# special commands
if text == 'upstart': return await _scmd.update_bot(update, context)
if text == 'logs': return await _scmd.show_logs(update, context)
waiting_for_seconds= context.application.bot_data.get('ask_seconds')
if waiting_for_seconds:
return await _bot_commands.get_seconds_from_input(context, update, chat_id, text)
if text == 'upstart': return await _bot_special_cmd.update_bot(update, context)
if text == 'logs': return await _bot_special_cmd.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)

View File

@@ -1,31 +1,29 @@
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 get_name_from_input(context, update, chat_id, text):
_log.log_info(f'bot_command: {chat_id} - Collected Player Name {text}')
if 'ask_name' in context.application.bot_data:
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())
elif text == 'Add random Players':
await _bot_player.add_random_players(update, context, chat_id, colors_names= False)
elif text == 'Add random color Players':
await _bot_player.add_random_players(update, context, chat_id, colors_names= True)
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 get_seconds_from_input(context, update, chat_id, text):
_log.log_info(f'bot_command: {chat_id} - User Wants to auto-run the game every {text} seconds')
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)
@@ -35,28 +33,51 @@ async def get_seconds_from_input(context, update, chat_id, text):
### basic commands handling
async def bot_command_add_player(context, update, chat_id):
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 bot_command_get_players(context, update, chat_id):
async def cmd_get_players(context, update, chat_id):
return await _bot_player.get_players(update, context, chat_id)
async def bot_command_get_alive_players(context, update, 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 bot_command_get_death_players(context, update, 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 bot_command_get_ranking_players(context, update, 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 bot_command_simulate_day(context, update, chat_id):
async def cmd_simulate_day(context, update, chat_id):
return await _bot_simulation.simulate_day(context, chat_id)
async def bot_command_simulate_day_cron(context, update, 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'])

View File

@@ -5,7 +5,8 @@ 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()) >= 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):

View File

@@ -39,9 +39,9 @@ async def show_logs(update, context):
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_info(f'open_logs: {chat_id} trying opening logs...')
_log.log_debug(f'open_logs: {chat_id} trying opening logs...')
try:
game_log= _os.path.expanduser(f'{_botsyms.BOT_PATH}/battle_royale.log')
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)

View File

@@ -1,12 +1,44 @@
from local_settings import TOKEN as _token
from local_settings import BOT_PATH as _bot_path
from local_settings import BOT_EXEC_CMD as _bot_exec_cmd
from local_settings import SUPER_USERS as _superusers
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
BOT_PATH= _bot_path
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.
@@ -44,3 +76,40 @@ COLORS_NAMES= [
"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)
"""

101
debug.py
View File

@@ -1,44 +1,60 @@
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= [
{
'name': 'Elara',
},
{
'name': 'Kaelen',
},
{
'name': 'Zephyr',
},
{
'name': 'Lyra',
},
{
'name': 'Orion',
},
{
'name': 'Seraphina',
},
]
players= _syms.COLORS_NAMES
weapons= []
Arena= _main.init_arena(players, weapons)
print(f'Players: {Arena.get_players()}')
print(f'Weapons: {Arena.get_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'
print(msg)
_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
print(f'Giorno #{Arena.day}')
_logs.log_debug(f'Giorno #{Arena.day}')
alive_players= Arena.get_alive_players()
if len(alive_players) == 1:
day= Arena.day
@@ -46,12 +62,13 @@ def play_one_day_debug(Arena):
daily_events= []
_rand.shuffle(alive_players)
for p_one in alive_players:
if not p_one.is_alive(): continue # he could be dead during this day cycle
p_two= _rand.sample(Arena.get_alive_players(), 1)[0]
while p_one.get_id() == p_two.get_id():
p_two= _rand.sample(Arena.get_alive_players(), 1)[0]
_dmg, msg= p_one.attack(p_two)
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()
@@ -61,6 +78,19 @@ def play_one_day_debug(Arena):
#_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):
@@ -76,7 +106,14 @@ def init_debug_loop():
#End of day
last_player= Arena.get_alive_players()[0]
print(f'{last_player.get_name()} sopravvive e vince dopo {Arena.day} lunghi Giorni, conquistando l\'amore eterno di Guarino')
_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()

View File

@@ -1,6 +1,8 @@
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():
@@ -14,6 +16,7 @@ class BrSimArena():
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
@@ -28,13 +31,13 @@ class BrSimArena():
def next_day(self):
self.day+= 1
print(f'Giorno: {self.day}')
_logs.log_debug(f'Giorno: {self.day}')
alive_players_str= ', '.join([p.get_name() for p in self.get_alive_players()])
print(f'Giocatori vivi: {alive_players_str}')
_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])
print(f'Giocatori morti: {death_players_str}')
_logs.log_debug(f'Giocatori morti: {death_players_str}')
def get_alive_players(self):
res= []
@@ -110,10 +113,12 @@ class BrSimArena():
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
@@ -124,3 +129,6 @@ class BrSimArena():
#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

View File

@@ -7,7 +7,75 @@ class ArenaEventPicker():
self.event_list = EVENTS
self.already_picked_players = []
def pick_event():
pass
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

View File

@@ -1,9 +1,15 @@
import random as _random
from entities import resource as _resource
class BrSimItem():
class BrSimItem(_resource.BrSimResource):
# test
def __init__(self):
pass
self.coord_x= 0
self.coord_y= 0
def is_item(self):
return True
def get_name(self):
return self.name

View File

@@ -1,13 +1,16 @@
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):
super()
self.id= str(_uuid.uuid4())
self.name= name
self.stats= ''
self.health= _random.randint(1,3)
self.health= _random.randint(1,1)
self.inventory= inventory or []
self.damage= _random.randint(1,2) # this is the punch damage amount
self.max_weight= 5 # this is the max inventory weight
@@ -20,6 +23,9 @@ class BrSimPlayer():
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_player(self):
return True
### control methods
def get_name_and_stats(self):
@@ -132,11 +138,11 @@ class BrSimPlayer():
self.health= 0
if self.player_gender_is_male():
print(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono morto x.x')
_logs.log_debug(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono morto x.x')
elif self.player_gender_is_female():
print(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono morta x.x')
_logs.log_debug(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono morta x.x')
else:
print(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono mort* x.x')
_logs.log_debug(f'[{self.get_name_and_stats()}]: Guarino, perdonami se sono mort* x.x')
return damage
def attack(self, target):
@@ -155,20 +161,42 @@ class BrSimPlayer():
weapon= self.get_equipped_weapon()
if weapon: msg+= f' con un {weapon.get_name}'
else: msg+= f' con un pugno'
self.kills+= 1
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():
print(f'Sono sovraccarico, {self.get_name_and_stats} non puo\' prendere questo oggetto')
_logs.log_debug(f'Sono sovraccarico, {self.get_name_and_stats} non puo\' prendere questo oggetto')
elif self.player_gender_is_female():
print(f'Sono sovraccarica, {self.get_name_and_stats} non puo\' prendere questo oggetto')
_logs.log_debug(f'Sono sovraccarica, {self.get_name_and_stats} non puo\' prendere questo oggetto')
else:
print(f'Sono sovraccaric#, {self.get_name_and_stats} non puo\' prendere questo oggetto')
_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

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

View File

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

View File

@@ -1,7 +1,10 @@
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()
@@ -10,23 +13,48 @@ def get_log_name():
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}-{now.time()}.log'
fname= f'battle_royale-{now.year}{month}{day}.log'
return fname
_logging.basicConfig(filename= get_log_name(), encoding= 'utf-8', format='%(asctime)s - %(levelname)s - %(message)s', level= _logging.INFO)
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)