Compare commits

..

16 Commits

2 changed files with 373 additions and 69 deletions

133
bot.py
View File

@ -2,6 +2,7 @@
from collections import namedtuple from collections import namedtuple
import copy import copy
from datetime import datetime, timedelta
from functools import partial from functools import partial
import json import json
from pathlib import Path from pathlib import Path
@ -36,7 +37,7 @@ CANTONS = {
"SH": "Schaffhouse", "SH": "Schaffhouse",
"SO": "Soleure", "SO": "Soleure",
"SZ": "Schwytz", "SZ": "Schwytz",
"TH": "Thurgovie", "TG": "Thurgovie",
"TI": "Tessin", "TI": "Tessin",
"UR": "Uri", "UR": "Uri",
"VD": "Vaud", "VD": "Vaud",
@ -48,14 +49,14 @@ CANTONS = {
EQUIPES = ["rouge", "vert"] EQUIPES = ["rouge", "vert"]
CodeCanton = Literal["AG", "AI", "AR", "BE", "BL", "BS", "FR", "GE", "GL", "GR", "JU", "LU", "NE", CodeCanton = Literal["AG", "AI", "AR", "BE", "BL", "BS", "FR", "GE", "GL", "GR", "JU", "LU", "NE",
"NW", "OW", "SG", "SH", "SO", "SZ", "TH", "TI", "UR", "VD", "VS", "ZG", "ZH"] "NW", "OW", "SG", "SH", "SO", "SZ", "TG", "TI", "UR", "VD", "VS", "ZG", "ZH"]
Couleur = Literal["rouge", "vert"] Couleur = Literal["rouge", "vert"]
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
PREFIX = '$' PREFIX = '!'
bot = commands.Bot(command_prefix=PREFIX, intents=intents) bot = commands.Bot(command_prefix=PREFIX, intents=intents)
DATA_FILE = Path(__file__).parent / "data.json" DATA_FILE = Path(__file__).parent / "data.json"
@ -68,8 +69,9 @@ else:
'cantons': {code_canton: {'capture': None, 'verrouille': False} for code_canton in CANTONS.keys()}, 'cantons': {code_canton: {'capture': None, 'verrouille': False} for code_canton in CANTONS.keys()},
'defis': { 'defis': {
'mains': {equipe: [] for equipe in EQUIPES}, 'mains': {equipe: [] for equipe in EQUIPES},
'bonus': {equipe: 0 for equipe in EQUIPES},
'tires_capture': [], 'tires_capture': [],
'tires_competition': [], 'tires_vol': [],
} }
} }
with DATA_FILE.open('w') as data_file: with DATA_FILE.open('w') as data_file:
@ -95,7 +97,7 @@ def generer_carte():
cairosvg.svg2png(url='map.svg', write_to='map.png') cairosvg.svg2png(url='map.svg', write_to='map.png')
@bot.command() @bot.command(brief="Affche la carte des cantons capturés.")
async def carte(ctx: commands.Context): async def carte(ctx: commands.Context):
rouges = list(canton_code for canton_code, canton in data['cantons'].items() rouges = list(canton_code for canton_code, canton in data['cantons'].items()
if canton['capture'] == "rouge") if canton['capture'] == "rouge")
@ -117,7 +119,7 @@ async def carte(ctx: commands.Context):
await ctx.send(message, file=discord.File(f, filename="battle4suisse.png")) await ctx.send(message, file=discord.File(f, filename="battle4suisse.png"))
@bot.command() @bot.command(brief=f"Capture un canton pour son équipe : {PREFIX}capturer CODE_CANTON [EQUIPE]")
async def capturer(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None): async def capturer(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None):
if couleur is None: if couleur is None:
author_id = ctx.author.id author_id = ctx.author.id
@ -141,7 +143,7 @@ async def capture_error(ctx, error):
await ctx.send(str(error)) await ctx.send(str(error))
@bot.command() @bot.command(brief=f"Verrouille un canton sur la carte pour son équipe : {PREFIX}verrouiller CODE_CANTON [EQUIPE]")
async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None): async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None):
if couleur is None: if couleur is None:
author_id = ctx.author.id author_id = ctx.author.id
@ -159,7 +161,7 @@ async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Cou
return await carte(ctx) return await carte(ctx)
@bot.command() @bot.command(brief=f"Réinitialise l'état de capture d'un canton : {PREFIX}reset CODE_CANTON")
async def reset(ctx: commands.Context, canton: CodeCanton): async def reset(ctx: commands.Context, canton: CodeCanton):
data['cantons'][canton]['capture'] = None data['cantons'][canton]['capture'] = None
data['cantons'][canton]['verrouille'] = False data['cantons'][canton]['verrouille'] = False
@ -169,7 +171,7 @@ async def reset(ctx: commands.Context, canton: CodeCanton):
return await carte(ctx) return await carte(ctx)
@bot.command() @bot.command(brief=f"Rejoindre une équipe : {PREFIX}equipe EQUIPE")
async def equipe(ctx: commands.Context, couleur: Couleur): async def equipe(ctx: commands.Context, couleur: Couleur):
author_id = ctx.author.id author_id = ctx.author.id
for membres_equipe in data['equipes'].values(): for membres_equipe in data['equipes'].values():
@ -181,13 +183,13 @@ async def equipe(ctx: commands.Context, couleur: Couleur):
await ctx.send(f"Équipe {couleur} rejointe") await ctx.send(f"Équipe {couleur} rejointe")
@bot.command() @bot.command(brief=f"Affiche la liste des noms des défis : {PREFIX}defis [capture | vol]")
async def defis(ctx: commands.Context, *, type_defi: Literal['capture', 'competition'] = "capture"): async def defis(ctx: commands.Context, *, type_defi: Literal['capture', 'vol'] = "capture"):
await ctx.send(f"Liste des défis de {type_defi} :\n" + "\n".join(f"* {defi['id']} : {defi['nom']}" for defi in DEFIS[type_defi])) await ctx.send(f"Liste des défis de {type_defi} :\n" + "\n".join(f"* {defi['id']} : {defi['nom']}" for defi in DEFIS[type_defi]))
@bot.command() @bot.command(brief=f"Affiche la description des défis")
async def description(ctx: commands.Context, type_defi: Literal['capture', 'competition'] = "capture", id_defi: int | None = None): async def description(ctx: commands.Context, type_defi: Literal['capture', 'vol'] = "capture", id_defi: int | None = None):
defis = DEFIS[type_defi] defis = DEFIS[type_defi]
embeds = [] embeds = []
if id_defi is not None: if id_defi is not None:
@ -196,7 +198,7 @@ async def description(ctx: commands.Context, type_defi: Literal['capture', 'comp
defi = next(defi for defi in defis if defi['id'] == id_defi) defi = next(defi for defi in defis if defi['id'] == id_defi)
except StopIteration: except StopIteration:
raise commands.BadArgument(f"Le défi de {type_defi}{id_defi} n'existe pas.") raise commands.BadArgument(f"Le défi de {type_defi}{id_defi} n'existe pas.")
embed.add_field(name=f"{defi['nom']}", value=defi['description'], inline=False) embed.add_field(name=f"{defi['nom']} {defi['bonus'] * ":star:"}", value=defi['description'], inline=False)
embeds.append(embed) embeds.append(embed)
else: else:
for page in range((len(defis) - 1) // 25 + 1): for page in range((len(defis) - 1) // 25 + 1):
@ -204,7 +206,7 @@ async def description(ctx: commands.Context, type_defi: Literal['capture', 'comp
embed = discord.Embed(title=f"Description des défis", colour=discord.Colour.gold()) embed = discord.Embed(title=f"Description des défis", colour=discord.Colour.gold())
embed.set_footer(text=f"Page {page + 1}/{(len(defis) - 1) // 25 + 1}") embed.set_footer(text=f"Page {page + 1}/{(len(defis) - 1) // 25 + 1}")
for defi in defis_page: for defi in defis_page:
embed.add_field(name=f"{defi['nom']} (n°{defi['id']})", value=defi['description'], inline=False) embed.add_field(name=f"{defi['nom']} {defi['bonus'] * ":star:"} (n°{defi['id']})", value=defi['description'], inline=False)
embeds.append(embed) embeds.append(embed)
await ctx.send(embeds=embeds) await ctx.send(embeds=embeds)
@ -229,6 +231,49 @@ async def tirage(ctx: commands.Context, nb_defis: int = 5):
await ctx.send("Les mains de départ ont bien été tirées ! Le contenu vous a été envoyé en MP.") await ctx.send("Les mains de départ ont bien été tirées ! Le contenu vous a été envoyé en MP.")
@bot.command()
async def vol(ctx: commands.Context):
defi_vol = random.choice([defi for defi in DEFIS['vol'] if defi['id'] not in data['defis']['tires_vol']])
data['defis']['tires_vol'].append(defi_vol['id'])
embed = discord.Embed(title=defi_vol['nom'], description=defi_vol['description'])
embed.set_footer(text=f"Défi de compétition n°{defi_vol['id']}")
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
await ctx.send("@everyone Un canton est attaqué ! L'équipe vainqueure de ce défi conservera son contrôle jusqu'à la fin du jeu :", embed=embed)
@bot.command()
async def remiser(ctx: commands.Context, type_defi: Literal['capture', 'vol'] = "capture", id_defi: int | None = None):
defis = DEFIS[type_defi]
try:
defi = next(defi for defi in defis if defi['id'] == id_defi)
except StopIteration:
raise commands.BadArgument(f"Le défi de {type_defi}{id_defi} n'existe pas.")
if id_defi in data['defis'][f'tires_{type_defi}']:
data['defis'][f'tires_{type_defi}'].remove(id_defi)
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
await ctx.reply(f"Le défi de {type_defi}{id_defi} ({defi['nom']}) a été retiré de la défausse et pourra à nouveau être tiré au sort.")
else:
await ctx.reply(f"Le défi de {type_defi}{id_defi} ({defi['nom']}) n'était déjà pas dans la défausse.")
class MainView(discord.ui.View):
def __init__(self, ctx: commands.Context, user_id: int, defis: list[dict], timeout: float | None = 180.0):
super().__init__(timeout=timeout)
async def terminer_defi(self, id_defi: int, user_id: int, interaction: discord.Interaction):
await interaction.response.defer()
await terminer(ctx, id_defi, user_id, interaction.channel)
for id_defi in defis:
defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
button = discord.ui.Button(style=discord.ButtonStyle.success, label=f"Terminer {defi['nom']}")
button.callback = partial(terminer_defi, self, id_defi, user_id)
self.add_item(button)
@bot.command(name="main") @bot.command(name="main")
async def afficher_main(ctx: commands.Context, mode: Literal['public', 'prive'] = "prive", author_id: int | None = None): async def afficher_main(ctx: commands.Context, mode: Literal['public', 'prive'] = "prive", author_id: int | None = None):
author_id = author_id or ctx.author.id author_id = author_id or ctx.author.id
@ -239,25 +284,27 @@ async def afficher_main(ctx: commands.Context, mode: Literal['public', 'prive']
raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.") raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
main = data['defis']['mains'][couleur] main = data['defis']['mains'][couleur]
nb_bonus = data['defis']['bonus'][couleur]
embeds = [] embeds = []
colour = discord.Color.red() if couleur == "rouge" else discord.Color.green() colour = discord.Color.red() if couleur == "rouge" else discord.Color.green()
for id_defi in main: for id_defi in main:
defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi) defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
embed = discord.Embed(title=defi['nom'], description=defi['description'], colour=colour) embed = discord.Embed(title=f"{defi['nom']} {defi['bonus'] * ":star:"}", description=defi['description'], colour=colour)
embed.set_footer(text=f"Défi n°{defi['id']}") embed.set_footer(text=f"Défi n°{defi['id']}")
embeds.append(embed) embeds.append(embed)
if mode == "public": if mode == "public":
await ctx.send(f"Défis de l'équipe **{couleur}** :", embeds=embeds) await ctx.send(f"Défis de l'équipe **{couleur}** :", embeds=embeds)
else: else:
channel_dm = await bot.create_dm(ctx.author) channel_dm = await bot.create_dm(namedtuple('User', 'id')(author_id))
await channel_dm.send("Vos défis en main :", embeds=embeds) await channel_dm.send(f"Vous disposez de **{nb_bonus} bonus {nb_bonus * ":star:"}**.\nVos défis en main :", embeds=embeds, view=MainView(ctx, author_id, main))
@bot.command() @bot.command()
async def terminer(ctx: commands.Context, id_defi: int): async def terminer(ctx: commands.Context, id_defi: int, author_id: int | None = None, channel: discord.abc.Messageable | None = None):
if all(id_defi != defi['id'] for defi in DEFIS['capture']): if all(id_defi != defi['id'] for defi in DEFIS['capture']):
raise commands.BadArgument(f"Erreur : Le défi {id_defi_1} n'existe pas") raise commands.BadArgument(f"Erreur : Le défi {id_defi_1} n'existe pas")
author_id = ctx.author.id defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
author_id = author_id or ctx.author.id
for equipe, membres_equipe in data['equipes'].items(): for equipe, membres_equipe in data['equipes'].items():
if author_id in membres_equipe: if author_id in membres_equipe:
break break
@ -272,17 +319,45 @@ async def terminer(ctx: commands.Context, id_defi: int):
main.remove(id_defi) main.remove(id_defi)
main.append(nouveau_defi['id']) main.append(nouveau_defi['id'])
data['defis']['tires_capture'].append(nouveau_defi['id']) data['defis']['tires_capture'].append(nouveau_defi['id'])
data['defis']['bonus'][equipe] += defi['bonus']
with DATA_FILE.open('w') as data_file: with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2) json.dump(data, data_file, indent=2)
channel = channel or ctx
await channel.send(f"Défi n°{id_defi} **{defi['nom']}** terminé ! Il est retiré de votre main.")
await channel.send(f"Votre équipe gagne **{defi['bonus']} bonus**. Vous en possédez désormais {data['defis']['bonus'][equipe]}.")
colour = discord.Color.red() if equipe == "rouge" else discord.Color.green() colour = discord.Color.red() if equipe == "rouge" else discord.Color.green()
embed = discord.Embed(title=nouveau_defi['nom'], description=nouveau_defi['description'], colour=colour) embed = discord.Embed(title=f"{nouveau_defi['nom']} {defi['bonus'] * ":star:"}", description=nouveau_defi['description'], colour=colour)
embed.set_footer(text=f"Défi n°{nouveau_defi['id']}") embed.set_footer(text=f"Défi n°{nouveau_defi['id']}")
await ctx.send("**Votre nouveau défi en main :**", embed=embed) await channel.send("**Votre nouveau défi en main :**", embed=embed)
for member_id in data['equipes'][equipe]: for member_id in data['equipes'][equipe]:
await afficher_main(ctx, author_id=member_id) await afficher_main(ctx, author_id=member_id)
@bot.command()
async def bonus(ctx: commands.Context, equipe: Couleur | None = None, nouvelle_valeur: int | None = None):
if equipe is None:
author_id = ctx.author.id
for equipe, membres_equipe in data['equipes'].items():
if author_id in membres_equipe:
break
else:
raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
nb_bonus = data['defis']['bonus'][equipe]
if nouvelle_valeur is None:
if nb_bonus >= 1:
data['defis']['bonus'][equipe] -= 1
await ctx.send(f"L'équipe **{equipe}** vient d'utiliser un bonus !")
else:
await ctx.reply(f"Vous n'avez plus de bonus.", ephemeral=True)
else:
data['defis']['bonus'][equipe] = nouvelle_valeur
await ctx.send(f"L'équipe **{equipe}** a désormais **{nouvelle_valeur} bonus**, contre {nb_bonus} auparavant.")
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
@bot.command() @bot.command()
async def echange(ctx: commands.Context, id_defi_1: int, id_defi_2: int): async def echange(ctx: commands.Context, id_defi_1: int, id_defi_2: int):
if all(id_defi_1 != defi['id'] for defi in DEFIS['capture']): if all(id_defi_1 != defi['id'] for defi in DEFIS['capture']):
@ -355,9 +430,16 @@ async def melanger(ctx: commands.Context, nb_defis: int = 5):
await afficher_main(ctx, author_id=member_id) await afficher_main(ctx, author_id=member_id)
@bot.command()
async def de(ctx: commands.Context, nb_faces: int = 6): async def de(ctx: commands.Context, nb_faces: int = 6):
resultat = random.randint(1, nb_faces + 1) resultat = random.randint(1, nb_faces + 1)
await ctx.reply(f":dice: Résultat du dé à {nb_faces} faces : **{resultat}**") await ctx.reply(f":game_die: Résultat du dé à {nb_faces} faces : **{resultat}**")
@bot.command()
async def chronometre(ctx: commands.Context, minutes: int = 30, secondes: int = 0):
fin = datetime.now() + timedelta(minutes=minutes, seconds=secondes)
await ctx.send(f"Chronomètre lancé pour **{minutes:02d}:{secondes:02d}** (fin à <t:{int(fin.timestamp())}:T>)\nFin <t:{int(fin.timestamp())}:R>")
@bot.command() @bot.command()
@ -404,10 +486,15 @@ async def save(ctx: commands.Context):
@defis.error @defis.error
@description.error @description.error
@tirage.error @tirage.error
@vol.error
@remiser.error
@afficher_main.error @afficher_main.error
@terminer.error @terminer.error
@bonus.error
@echange.error @echange.error
@melanger.error @melanger.error
@de.error
@chronometre.error
@debug.error @debug.error
@reload.error @reload.error
@save.error @save.error

View File

@ -2,100 +2,317 @@
"capture": [ "capture": [
{ {
"id": 1, "id": 1,
"nom": "Défi 1", "nom": "Artisan⋅e chocolatièr⋅e",
"description": "Défi 1" "description": "À partir de chocolat local, construis un pont.\n\nLa structure doit rester en place pendant au moins 15 secondes sans s'effondrer.",
"bonus": 0
}, },
{ {
"id": 2, "id": 2,
"nom": "Défi 2", "nom": "Manger (chez) les riches",
"description": "Défi 2" "description": "Mange une spécialité locale du canton.",
"bonus": 0
}, },
{ {
"id": 3, "id": 3,
"nom": "Défi 3", "nom": "Non-alignement",
"description": "Défi 3" "description": "Devant une institution locale, adresse une liste de 20 revendications géopolitiquement neutres.",
"bonus": 0
}, },
{ {
"id": 4, "id": 4,
"nom": "Défi 4", "nom": "Low five",
"description": "Défi 4" "description": "Fais un low-five au point le plus bas d'altitude du canton.",
"bonus": 0
}, },
{ {
"id": 5, "id": 5,
"nom": "Défi 5", "nom": "Artiste GPS",
"description": "Défi 5" "description": "Trace la croix du drapeau suisse à l'aide de ton tracé GPS.\n\nLa croix doit être ressemblante sans être nécessairement parfaite.",
"bonus": 0
}, },
{ {
"id": 6, "id": 6,
"nom": "Défi 6", "nom": "Prendre de la hauteur",
"description": "Défi 6" "description": "Rends-toi sur le toit d'un bâtiment de manière légale.",
"bonus": 1
}, },
{ {
"id": 7, "id": 7,
"nom": "Défi 7", "nom": "Marcher sur l'eau",
"description": "Défi 7" "description": "Traverse un cours d'eau d'une rive à l'autre.\n\nLa distance traversée doit être d'au moins un mètre.\nIl n'est pas possible d'emprunter une structure existante pour le traverser, en particulier d'utiliser un pont.\n\nSi le cours d'eau fait la frontière entre 2 cantons, les deux cantons sont capturés.",
"bonus": 1
}, },
{ {
"id": 8, "id": 8,
"nom": "Défi 8", "nom": "Randonnée",
"description": "Défi 8" "description": "Construis une tour de cailloux à plus de 800 mètres d'altitude.",
}, "bonus": 1
{
"id": 9,
"nom": "Défi 9",
"description": "Défi 9"
}, },
{ {
"id": 10, "id": 10,
"nom": "Défi 10", "nom": "Panorama",
"description": "Défi 10" "description": "Photographie taon partenaire de l'autre côté d'un lac.\n\nLe lac doit être reconnu comme tel sur les cartes, et l'étendue d'eau doit être pleinement visible sur l'image.",
"bonus": 1
}, },
{ {
"id": 11, "id": 11,
"nom": "Défi 11", "nom": "Rageux⋅se",
"description": "Défi 11" "description": "Auprès de la meilleure attraction touristique (musée, monument, paysage,…) du canton, fais une critique négative en 5 points en vidéo.",
"bonus": 0
}, },
{ {
"id": 12, "id": 12,
"nom": "Défi 12", "nom": "Non-binaire",
"description": "Défi 12" "description": "Dans un lieu que l'on peut qualifier d'entre-deux (milieu d'un pont, mezzanine entre 2 étages,…), chante « The Code » de Nemo.",
"bonus": 0
}, },
{ {
"id": 13, "id": 13,
"nom": "Défi 13", "nom": "Démocratie",
"description": "Défi 13" "description": "Organiser une votation citoyenne sur un sujet au choix. La votation doit récolter au moins 10 votant·es.",
"bonus": 1
}, },
{ {
"id": 14, "id": 14,
"nom": "Défi 14", "nom": "Inaction climatique suisse",
"description": "Défi 14" "description": "Nettoie 10 déchets jetés au sol d'un espace.",
"bonus": 0
}, },
{ {
"id": 15, "id": 15,
"nom": "Défi 15", "nom": "Révolution bovine",
"description": "Défi 15" "description": "Parle à une vache dans un pré et tente de la syndicaliser en lui suggérant de se libérer de sa condition d'élevage.\n\nIl n'est pas nécessaire de se faire comprendre.",
"bonus": 1
}, },
{ {
"id": 16, "id": 16,
"nom": "Défi 16", "nom": "Génie incivil",
"description": "Défi 16" "description": "Passe sous 3 ponts.",
} "bonus": 0
],
"competition": [
{
"id": 1,
"nom": "Compétition 1",
"description": "Compétition 1"
}, },
{
"id": 17,
"nom": "Tourisme minimal",
"description": "Va faire du tourisme dans le village le moins peuplé du canton disposant d'une gare à moins de 1 km.",
"bonus": 1
},
{
"id": 18,
"nom": "Jeu de mots au menu",
"description": "Trouve du riz cantonais.",
"bonus": 0
},
{
"id": 19,
"nom": "Patois",
"description": "Demande un mot en romanche.",
"bonus": 1
},
{
"id": 20,
"nom": "Du Jura aux Alpes",
"description": "Transporte une pierre jurassienne dans les Alpes, ou inversement. En cas de succès, les cantons de départ et d'arrivée sont capturés, à moins que le canton de départ n'ait été capturé par l'équipe adverse entre temps, auquel cas c'est un échec.\n\nIl est possible de faire d'autres défis pendant le transport. Les points de départ et d'arrivée doivent être à au moins 500 mètres d'altitude pour le Jura et 700 mètres d'altitude pour les Alpes.",
"bonus": 2
},
{
"id": 21,
"nom": "Du Rhin au Rhône",
"description": "Touche le Rhône et le Rhin dans la même journée. En cas de succès, les cantons de départ et d'arrivée sont capturés, à moins que le canton d'origine n'ait été capturé avant l'accomplissement du défi, auquel cas c'est un échec.\n\nIl est possible de réaliser d'autres défis pendant l'accomplissement de celui-ci.",
"bonus": 2
},
{
"id": 22,
"nom": "Pas moche, juste différent",
"description": "Auprès du bâtiment le plus moche identifié selon ses propres goûts, rédige un poème qui en fait l'éloge et le traduire en français, allemand et italien.\n\nLe poème doit être rédigé manuscrit sur une feuille.",
"bonus": 0
},
{
"id": 23,
"nom": "Rançon",
"description": "Écris « Bataille4Suisse » avec des papiers découpés et ré-agencés, tel un message de rançon.",
"bonus": 0
},
{
"id": 24,
"nom": "Moyenne",
"description": "Rends-toi le plus proche possible du barycentre (point central) d'une commune qui a entre 4100 et 4300 habitant⋅es. De ce point de vue, choisir jusqu'à 10 sommets visibles et non cachés par des bâtiments, et trouver quel est le sommet d'altitude moyenne.",
"bonus": 1
},
{
"id": 25,
"nom": "Helvetica",
"description": "Trouve un texte écrit en police d'écriture Helvetica.\n\nIl peut être imprimé ou sur écran d'affichage, et doit être trouvé et ne peut pas être créé ou mis en cette police par les joueureuses ni à leur demande.",
"bonus": 1
},
{
"id": 26,
"nom": "Terre en vue",
"description": "Positionne-toi à au moins 10 km de tout lac.",
"bonus": 1
},
{
"id": 27,
"nom": "Vue de haut",
"description": "Rends-toi à la gare ferroviaire accessible la plus haute du canton.",
"bonus": 1
},
{
"id": 28,
"nom": "Pi Day",
"description": "Rends-toi dans une commune dont l'un de ses noms officiels contient la lettre P, et dessiner un cercle le plus ressemblant possible.",
"bonus": 1
},
{
"id": 29,
"nom": "Quelle heure est-il ?",
"description": "Trouve une montre ou une horloge à cadran qui indique une heure entre 10h10 et 10h15.\n\nIl n'est pas autorisé de régler l'heure ni de demander à la régler, et il ne peut pas s'agir d'une montre qu'on possède ou portée par un·e passant·e.",
"bonus": 0
},
{
"id": 30,
"nom": "Vers l'infini et au-delà",
"description": "Photographie un sommet à au moins 2000 mètres d'altitude.\n\nUne photo où le sommet est caché par les nuages ou par la nuit est acceptée.",
"bonus": 1
},
{
"id": 31,
"nom": "Traversée",
"description": "Traverse un canton B depuis un canton A vers un canton C.\n\nLe canton B est capturé.",
"bonus": 1
},
{
"id": 32,
"nom": "Promotion culturelle",
"description": "Prends un flyer dans un lieu culturel, et en fais-en la promotion dans un lieu culturel similaire d'un autre canton.\n\nEn cas de succès du défi, les deux cantons sont capturés.",
"bonus": 1
},
{
"id": 33,
"nom": "Placement de produit",
"description": "Filme une pub de parfum.",
"bonus": 0
},
{
"id": 34,
"nom": "Polyglote",
"description": "Trouve une personne qui parle français, une personne qui parle allemand, une personne qui parle italien.\n\nCes trois personnes ne sont pas nécessairement distinctes.",
"bonus": 1
},
{
"id": 35,
"nom": "Cabotage",
"description": "Dans le même canton, prends un train et descend au prochain arrêt sans changer de canton.",
"bonus": 0
},
{
"id": 36,
"nom": "Cadeau !",
"description": "Offre un cadeau à quelqu'un⋅e, que vous l .",
"bonus": 0
},
{
"id": 37,
"nom": "Culture",
"description": "Trouve « Astérix chez les Helvètes » ou un livre sur le Nicaragua.",
"bonus": 0
},
{
"id": 38,
"nom": "Mairiephilie",
"description": "Dessine l'administration municipale d'une commune de ton canton devant elle.",
"bonus": 0
},
{
"id": 39,
"nom": "Sing'in the rain",
"description": "Chante « Sing'in the rain » sous la pluie ou sous la neige. S'il neige, remplace « rain » par « snow ».",
"bonus": 0
},
{
"id": 40,
"nom": "Touch the ~~grass~~ snow",
"description": "Touche de la neige naturelle.",
"bonus": 1
}
],
"vol": [
{ {
"id": 2, "id": 2,
"nom": "Compétition 2", "nom": "Chocolat :chocolate_bar:",
"description": "Compétition 2" "description": "Photographie le plus de chocolats différents (marque/chocolatièr⋅e et nom) en 15 minutes.",
"bonus": 0
}, },
{ {
"id": 3, "id": 3,
"nom": "Compétition 3", "nom": "Fromage :cheese:",
"description": "Compétition 3" "description": "Photographie le plus de fromages différents en 15 minutes.",
"bonus": 0
},
{
"id": 4,
"nom": "Vélo :bike:",
"description": "Photographie le plus de vélos différents en 15 minutes.",
"bonus": 0
},
{
"id": 5,
"nom": "Mot aléatoire",
"description": "Fais dire à un⋅e passant⋅e le mot aléatoire de ton choix le plus rapidement possible, sans donner le mot ni sa définition exacte.",
"bonus": 0
},
{
"id": 7,
"nom": "Luxe :money_with_wings:",
"description": "Trouve un objet vendu le plus cher possible en 15 minutes.",
"bonus": 0
},
{
"id": 8,
"nom": "Bonne affaire :gift:",
"description": "Trouve un objet vendu le moins cher possible en 15 minutes.",
"bonus": 0
},
{
"id": 9,
"nom": "Banques :bank:",
"description": "Photographie le plus de banques en 15 minutes.",
"bonus": 0
},
{
"id": 10,
"nom": "Animaux :dog2:",
"description": "Trouve le plus d'espèces animales en 15 minutes.",
"bonus": 0
},
{
"id": 11,
"nom": "Fuis ! :person_running:",
"description": "Quitte ton canton le plus rapidement possible.",
"bonus": 0
},
{
"id": 12,
"nom": "Château-fort :european_castle:",
"description": "Trouver la façade la plus imposante en 15 minutes.",
"bonus": 0
},
{
"id": 13,
"nom": "Cinéphile :cinema:",
"description": "Trouve un cinéma le plus rapidement possible.",
"bonus": 0
},
{
"id": 14,
"nom": "Fièr⋅e :rainbow_flag:",
"description": "Trouve un drapeau du parapluie LGBT le plus rapidement possible.\n\nIl ne peut pas être sur toi, ni fabriqué, ni sur un écran.",
"bonus": 0
},
{
"id": 15,
"nom": "Panneaux",
"description": "Trouve le plus le panneaux de signalisation possible en 15 minutes, qui ne sont pas des panneaux indiquant des directions.",
"bonus": 0
} }
] ]
} }