diff --git a/.github/workflows/dev-image.yml b/.github/workflows/dev-image.yml index 79edcf7..03dab56 100644 --- a/.github/workflows/dev-image.yml +++ b/.github/workflows/dev-image.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Docker Buildx uses: crazy-max/ghaction-docker-buildx@v3 @@ -23,12 +23,32 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - - name: Docker Buildx (build) + name: Docker Buildx (build amd64) run: | docker buildx build \ --cache-from "type=local,src=/tmp/.buildx-cache" \ --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm/v7,linux/arm64 \ + --platform linux/amd64 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:dev \ + --file ./Dockerfile ./ + - + name: Docker Buildx (build arm64) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm64 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:dev \ + --file ./Dockerfile ./ + - + name: Docker Buildx (build armv-7) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm/v7 \ --output "type=image,push=false" \ --tag albertoxamin/bang:dev \ --file ./Dockerfile ./ @@ -51,3 +71,12 @@ jobs: name: Inspect image run: | docker buildx imagetools inspect albertoxamin/bang:dev + + - name: Notify discord + uses: th0th/notify-discord@v0.4.1 + if: ${{ always() }} + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_JOB_NAME: "Docker image :dev" + GITHUB_JOB_STATUS: ${{ job.status }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 8c53638..90e072d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Docker Buildx uses: crazy-max/ghaction-docker-buildx@v3 @@ -23,12 +23,22 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - - name: Docker Buildx (build) + name: Docker Buildx (build amd64 arm64) run: | docker buildx build \ --cache-from "type=local,src=/tmp/.buildx-cache" \ --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm/v7 \ + --platform linux/amd64,linux/arm64 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:latest \ + --file ./Dockerfile ./ + - + name: Docker Buildx (build armv-7) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm/v7 \ --output "type=image,push=false" \ --tag albertoxamin/bang:latest \ --file ./Dockerfile ./ @@ -39,11 +49,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Docker Buildx (push) + name: Docker Buildx (push all) run: | docker buildx build \ --cache-from "type=local,src=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm/v7 \ + --platform linux/amd64,linux/arm64,linux/arm/v7 \ --output "type=image,push=true" \ --tag albertoxamin/bang:latest \ --file ./Dockerfile ./ @@ -51,3 +61,12 @@ jobs: name: Inspect image run: | docker buildx imagetools inspect albertoxamin/bang:latest + + - name: Notify discord + uses: th0th/notify-discord@v0.4.1 + if: ${{ always() }} + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_JOB_NAME: "Docker image main :latest" + GITHUB_JOB_STATUS: ${{ job.status }} diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml new file mode 100644 index 0000000..a6e632c --- /dev/null +++ b/.github/workflows/test-backend.yml @@ -0,0 +1,37 @@ +name: Python package + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7] + defaults: + run: + working-directory: ./backend + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Test with pytest + run: | + python -m pytest -p no:warnings + + - name: Notify discord + uses: th0th/notify-discord@v0.4.1 + if: ${{ always() }} + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_JOB_NAME: "Backend tests" + GITHUB_JOB_STATUS: ${{ job.status }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d84f4b7..ca2c18b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,12 +1,53 @@ name: Test Pull requests on: pull_request: + jobs: - test_build: + buildx: runs-on: ubuntu-latest - env: - DOCKER_BUILDKIT: '1' steps: - - uses: actions/checkout@v2 - - name: Build the Unified Docker image - run: docker build . --file Dockerfile + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Docker Buildx + uses: crazy-max/ghaction-docker-buildx@v3 + - + name: Cache Docker layers + uses: actions/cache@v2 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - + name: Docker Buildx (test build amd64) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm64 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:test \ + --file ./Dockerfile ./ + - + name: Docker Buildx (test build arm64) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm64 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:test \ + --file ./Dockerfile ./ + - + name: Docker Buildx (test build armv-7) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm/v7 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:test \ + --file ./Dockerfile ./ diff --git a/Dockerfile b/Dockerfile index 06a1600..fa47374 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,25 @@ +# build Vue frontend FROM node:lts-alpine as builder COPY ./frontend . RUN npm install RUN npm run build -FROM python:3.7.10-stretch +# now we should have a dist folder containing the static website + +FROM python:3.7.10-stretch as pybuilder +WORKDIR /code +COPY ./backend /code/ +RUN pip install --user -r requirements.txt +# We get the dependencies with the full python image so we can compile the one with missing binaries +ENV UseRobots=false + +FROM python:3.7.10-slim-stretch as app +# copy the dependencies from the pybuilder +COPY --from=pybuilder /root/.local /root/.local +# copy the backend python files from the pybuilder +COPY --from=pybuilder /code /dist +# copy the frontend static files from the builder COPY --from=builder ./dist /dist/ -COPY ./backend /dist/ WORKDIR /dist -RUN pip install -r requirements.txt EXPOSE 5001 -ENTRYPOINT ["python", "/dist/__init__.py"] + +ENTRYPOINT ["python", "/dist/server.py"] diff --git a/README.md b/README.md index 06c18e1..bee2b63 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ # bang + +This is the repo for the PewPew! game, which is a replica of BANG!. +BANG! is a trademark owned by DVGiochi. + +[Frontend Readme](./frontend/README.md) + +[Backend Readme](./backend/Readme.md) \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py deleted file mode 100644 index 3b53851..0000000 --- a/backend/__init__.py +++ /dev/null @@ -1,494 +0,0 @@ -import os -import json -import random -from typing import List -import eventlet -import socketio - -from bang.game import Game -from bang.players import Player, PendingAction - -import sys -sys.setrecursionlimit(10**6) # this should prevents bots from stopping - -sio = socketio.Server(cors_allowed_origins="*") -static_files={ - '/': {'content_type': 'text/html', 'filename': 'index.html'}, - '/game': {'content_type': 'text/html', 'filename': 'index.html'}, - '/help': {'content_type': 'text/html', 'filename': 'index.html'}, - '/status': {'content_type': 'text/html', 'filename': 'index.html'}, - # '/robots.txt': {'content_type': 'text/html', 'filename': 'robots.txt'}, - '/favicon.ico': {'filename': 'favicon.ico'}, - '/img/icons': './img/icons', - '/manifest.json': {'filename': 'manifest.json'}, - '/css': './css', - '/media': './media', - '/js': './js', -} -for file in [f for f in os.listdir('.') if '.js' in f or '.map' in f or '.html' in f]: - static_files[f'/{file}'] = f'./{file}' - -app = socketio.WSGIApp(sio, static_files=static_files) -games: List[Game] = [] -online_players = 0 -blacklist: List[str] = [] - -def advertise_lobbies(): - sio.emit('lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'password': g.password} for g in games if not g.started and len(g.players) < 10 and not g.is_hidden]) - sio.emit('spectate_lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'locked': g.password != ''} for g in games if g.started]) - -@sio.event -def connect(sid, environ): - global online_players - online_players += 1 - print('connect ', sid) - sio.enter_room(sid, 'lobby') - sio.emit('players', room='lobby', data=online_players) - -@sio.event -def get_online_players(sid): - global online_players - sio.emit('players', room='lobby', data=online_players) - -@sio.event -def set_username(sid, username): - ses = sio.get_session(sid) - if not isinstance(ses, Player): - sio.save_session(sid, Player(username, sid, sio)) - print(f'{sid} is now {username}') - advertise_lobbies() - elif ses.game == None or not ses.game.started: - print(f'{sid} changed username to {username}') - prev = ses.name - if len([p for p in ses.game.players if p.name == username]) > 0: - ses.name = f"{username}_{random.randint(0,100)}" - else: - ses.name = username - sio.emit('chat_message', room=ses.game.name, data=f'_change_username|{prev}|{ses.name}') - sio.emit('me', data=ses.name, room=sid) - ses.game.notify_room() - -@sio.event -def get_me(sid, room): - if isinstance(sio.get_session(sid), Player): - sio.emit('me', data=sio.get_session(sid).name, room=sid) - if sio.get_session(sid).game: - sio.get_session(sid).game.notify_room() - else: - sio.save_session(sid, Player('player', sid, sio)) - de_games = [g for g in games if g.name == room['name']] - if len(de_games) == 1 and not de_games[0].started: - join_room(sid, room) - elif len(de_games) == 1 and de_games[0].started: - print('room exists') - if room['username'] != None and any([p.name == room['username'] for p in de_games[0].players if p.is_bot]): - print('getting inside the bot') - bot = [p for p in de_games[0].players if p.is_bot and p.name == room['username'] ][0] - bot.sid = sid - bot.is_bot = False - sio.enter_room(sid, de_games[0].name) - sio.save_session(sid, bot) - de_games[0].notify_room(sid) - eventlet.sleep(0.1) - de_games[0].notify_all() - sio.emit('role', room=sid, data=json.dumps(bot.role, default=lambda o: o.__dict__)) - bot.notify_self() - if len(bot.available_characters) > 0: - bot.set_available_character(bot.available_characters) - else: #spectate - de_games[0].spectators.append(sio.get_session(sid)) - sio.get_session(sid).game = de_games[0] - sio.enter_room(sid, de_games[0].name) - de_games[0].notify_room(sid) - de_games[0].notify_event_card(sid) - de_games[0].notify_scrap_pile(sid) - de_games[0].notify_all() - de_games[0].notify_event_card() - else: - create_room(sid, room['name']) - if sio.get_session(sid).game == None: - sio.emit('me', data={'error':'Wrong password/Cannot connect'}, room=sid) - else: - sio.emit('me', data=sio.get_session(sid).name, room=sid) - if room['username'] == None or any([p.name == room['username'] for p in sio.get_session(sid).game.players]): - sio.emit('change_username', room=sid) - else: - sio.emit('chat_message', room=sio.get_session(sid).game.name, data=f"_change_username|{sio.get_session(sid).name}|{room['username']}") - sio.get_session(sid).name = room['username'] - sio.emit('me', data=sio.get_session(sid).name, room=sid) - if not sio.get_session(sid).game.started: - sio.get_session(sid).game.notify_room() - -@sio.event -def disconnect(sid): - global online_players - online_players -= 1 - if sio.get_session(sid): - sio.emit('players', room='lobby', data=online_players) - if sio.get_session(sid).game and sio.get_session(sid).disconnect(): - sio.close_room(sio.get_session(sid).game.name) - games.pop(games.index(sio.get_session(sid).game)) - print('disconnect ', sid) - advertise_lobbies() - -@sio.event -def create_room(sid, room_name): - if sio.get_session(sid).game == None: - while len([g for g in games if g.name == room_name]): - room_name += f'_{random.randint(0,100)}' - sio.leave_room(sid, 'lobby') - sio.enter_room(sid, room_name) - g = Game(room_name, sio) - g.add_player(sio.get_session(sid)) - if room_name in blacklist: - g.is_hidden = True - games.append(g) - print(f'{sid} created a room named {room_name}') - advertise_lobbies() - -@sio.event -def private(sid): - g = sio.get_session(sid).game - g.set_private() - advertise_lobbies() - -@sio.event -def toggle_expansion(sid, expansion_name): - g = sio.get_session(sid).game - g.toggle_expansion(expansion_name) - -@sio.event -def toggle_comp(sid): - sio.get_session(sid).game.toggle_competitive() - -@sio.event -def toggle_replace_with_bot(sid): - sio.get_session(sid).game.toggle_disconnect_bot() - -@sio.event -def join_room(sid, room): - room_name = room['name'] - i = [g.name for g in games].index(room_name) - if games[i].password != '' and games[i].password != room['password'].upper(): - return - if not games[i].started: - print(f'{sid} joined a room named {room_name}') - sio.leave_room(sid, 'lobby') - sio.enter_room(sid, room_name) - while len([p for p in games[i].players if p.name == sio.get_session(sid).name]): - sio.get_session(sid).name += f'_{random.randint(0,100)}' - sio.emit('me', data=sio.get_session(sid).name, room=sid) - games[i].add_player(sio.get_session(sid)) - advertise_lobbies() - else: - games[i].spectators.append(sio.get_session(sid)) - sio.get_session(sid).game = games[i] - sio.get_session(sid).pending_action = PendingAction.WAIT - sio.enter_room(sid, games[0].name) - games[i].notify_room(sid) - eventlet.sleep(0.5) - games[i].notify_room(sid) - games[i].notify_all() - -@sio.event -def chat_message(sid, msg): - ses: Player = sio.get_session(sid) - if len(msg) > 0: - if msg[0] == '/': - if '/addbot' in msg and not ses.game.started: - if len(msg.split()) > 1: - # for _ in range(int(msg.split()[1])): - # ses.game.add_player(Player(f'AI_{random.randint(0,1000)}', 'bot', sio, bot=True)) - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'Only 1 bot at the time'}) - else: - bot = Player(f'AI_{random.randint(0,10)}', 'bot', sio, bot=True) - while any([p for p in ses.game.players if p.name == bot.name]): - bot = Player(f'AI_{random.randint(0,10)}', 'bot', sio, bot=True) - ses.game.add_player(bot) - bot.bot_spin() - return - elif '/removebot' in msg and not ses.game.started: - if any([p.is_bot for p in ses.game.players]): - [p for p in ses.game.players if p.is_bot][-1].disconnect() - return - elif '/togglecomp' in msg and ses.game: - ses.game.toggle_competitive() - return - if '/debug' in msg: - cmd = msg.split() - if len(cmd) == 2 and 'DEPLOY_KEY' in os.environ and cmd[1] == os.environ['DEPLOY_KEY']: # solo chi ha la deploy key può attivare la modalità debug - ses.game.debug = not ses.game.debug - ses.game.notify_room() - elif ses == ses.game.players[0]: # solo l'owner può attivare la modalità debug - ses.game.debug = not ses.game.debug - ses.game.notify_room() - if ses.game.debug: - sio.emit('chat_message', room=sid, data={'color': f'red','text':f'debug mode is now active, only the owner of the room can disable it with /debug'}) - return - if not ses.game.debug: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'debug mode is not active, only the owner of the room can enable it with /debug'}) - elif '/set_chars' in msg and not ses.game.started: - cmd = msg.split() - if len(cmd) == 2 and int(cmd[1]) > 0: - ses.game.characters_to_distribute = int(cmd[1]) - elif '/suicide' in msg and ses.game.started and ses.lives > 0: - ses.lives = 0 - ses.notify_self() - elif '/nextevent' in msg and ses.game.started: - ses.game.deck.flip_event() - elif '/notify' in msg and ses.game.started: - cmd = msg.split() - if len(cmd) >= 3: - if cmd[1] in ses.game.players_map: - ses.game.get_player_named(cmd[1]).notify_card(ses, { - 'name': ' '.join(cmd[2:]), - 'icon': '🚨', - 'suit': 4, - 'number': ' '.join(cmd[2:]) - }) - else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/show_cards' in msg and ses.game.started: - cmd = msg.split() - if len(cmd) == 2: - if cmd[1] in ses.game.players_map: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and is looking at {cmd[1]} hand'}) - for c in ses.game.get_player_named(cmd[1]).hand: - ses.notify_card(ses, c) - eventlet.sleep(0.3) - else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/ddc' in msg and ses.game.started: # debug destroy cards usage: [/ddc *] [/ddc username] - cmd = msg.split() - if len(cmd) == 2: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode destroyed {cmd[1]} cards'}) - if cmd[1] == "*": - for p in ses.game.players_map: - ses.game.get_player_named(p).hand = [] - ses.game.get_player_named(p).equipment = [] - ses.game.get_player_named(p).notify_self() - elif cmd[1] in ses.game.players_map: - ses.game.get_player_named(cmd[1]).hand = [] - ses.game.get_player_named(cmd[1]).equipment = [] - ses.game.get_player_named(cmd[1]).notify_self() - else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/dsh' in msg and ses.game.started: #debug set health usage [/dsh * hp] [/dsh username hp] - cmd = msg.split() - if len(cmd) == 3: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and is changing {cmd[1]} health'}) - if cmd[1] == "*": - for p in ses.game.players_map: - ses.game.get_player_named(p).lives = int(cmd[2]) - ses.game.get_player_named(p).notify_self() - elif cmd[1] in ses.game.players_map: - ses.game.get_player_named(cmd[1]).lives = int(cmd[2]) - ses.game.get_player_named(cmd[1]).notify_self() - else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/togglebot' in msg and ses.game: - ses.game.toggle_disconnect_bot() - elif '/cancelgame' in msg and ses.game.started: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} stopped the current game'}) - ses.game.reset() - elif '/startgame' in msg and not ses.game.started: - ses.game.start_game() - elif '/setbotspeed' in msg: - ses.game.bot_speed = float(msg.split()[1]) - elif '/addex' in msg and not ses.game.started: - cmd = msg.split() - if len(cmd) == 2: - cmd[1] = cmd[1].replace('foc', 'fistful_of_cards') - if cmd[1] not in ses.game.available_expansions: - ses.game.available_expansions.append(cmd[1]) - ses.game.notify_room() - else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/setcharacter' in msg: - import bang.characters as characters - cmd = msg.split() - if len(cmd) >= 2: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed character'}) - chs = characters.all_characters(ses.game.expansions) - ses.character = [c for c in chs if c.name == ' '.join(cmd[1:])][0] - ses.real_character = ses.character - ses.notify_self() - elif '/setevent' in msg and ses.game and ses.game.deck: #add event before the position /setevent (position) 0 (name) Peyote - cmd = msg.split() - if len(cmd) >= 3: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed event'}) - import bang.expansions.fistful_of_cards.card_events as ce - import bang.expansions.high_noon.card_events as ceh - chs = [] - chs.extend(ce.get_all_events()) - chs.append(ce.get_endgame_card()) - chs.extend(ceh.get_all_events()) - chs.append(ceh.get_endgame_card()) - ses.game.deck.event_cards.insert(int(cmd[1]), [c for c in chs if c!=None and c.name == ' '.join(cmd[2:])][0]) - ses.game.notify_event_card() - elif '/removecard' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and removed a card'}) - cmd = msg.split() - if len(cmd) == 2: - if int(cmd[1]) < len(ses.hand): - ses.hand.pop(int(cmd[1])) - else: - ses.equipment.pop(int(cmd[1])-len(ses.hand)) - ses.notify_self() - elif '/getcard' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got a card'}) - import bang.cards as cs - cmd = msg.split() - if len(cmd) >= 2: - cards = cs.get_starting_deck(ses.game.expansions) - card_names = ' '.join(cmd[1:]).split(',') - for cn in card_names: - ses.hand.append([c for c in cards if c.name == cn][0]) - ses.notify_self() - elif '/gameinfo' in msg: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'info: {ses.game.__dict__}'}) - elif '/meinfo' in msg: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'info: {ses.__dict__}'}) - elif '/mebot' in msg: - ses.is_bot = not ses.is_bot - ses.bot_spin() - elif '/arcadekick' in msg and ses.game.started: - if len([p for p in ses.game.players if p.pending_action != PendingAction.WAIT]) == 0: - sio.emit('chat_message', room=ses.game.name, data={'color': f'','text':f'KICKING THE ARCADE CABINET'}) - ses.game.next_turn() - else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} COMMAND NOT FOUND'}) - else: - color = sid.encode('utf-8').hex()[-3:] - sio.emit('chat_message', room=ses.game.name, data={'color': f'#{color}','text':f'[{ses.name}]: {msg}'}) - -@sio.event -def get_all_rooms(sid, deploy_key): - if 'DEPLOY_KEY' in os.environ and deploy_key == os.environ['DEPLOY_KEY']: - sio.emit('all_rooms', room=sid, data=[{ - 'name': g.name, - 'hidden': g.is_hidden, - 'players': [{'name':p.name, 'bot': p.is_bot, 'health': p.lives, 'sid': p.sid} for p in g.players], - 'password': g.password, - 'expansions': g.expansions, - 'started': g.started, - 'current_turn': g.turn, - 'incremental_turn': g.incremental_turn, - 'debug': g.debug, - 'spectators': len(g.spectators) - } for g in games]) - -@sio.event -def kick(sid, data): - if 'DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']: - sio.emit('kicked', room=data['sid']) - -@sio.event -def hide_toogle(sid, data): - if 'DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']: - game = [g for g in games if g.name==data['room']] - if len(games) > 0: - game[0].is_hidden = not game[0].is_hidden - if game[0].is_hidden: - if not data['room'] in blacklist: - blacklist.append(data['room']) - elif data['room'] in blacklist: - blacklist.remove(data['room']) - advertise_lobbies() - -@sio.event -def start_game(sid): - ses: Player = sio.get_session(sid) - ses.game.start_game() - advertise_lobbies() - -@sio.event -def set_character(sid, name): - ses: Player = sio.get_session(sid) - ses.set_character(name) - -@sio.event -def refresh(sid): - ses: Player = sio.get_session(sid) - ses.notify_self() - -@sio.event -def draw(sid, pile): - ses: Player = sio.get_session(sid) - ses.draw(pile) - -@sio.event -def pick(sid): - ses: Player = sio.get_session(sid) - ses.pick() - -@sio.event -def end_turn(sid): - ses: Player = sio.get_session(sid) - ses.end_turn() - -@sio.event -def play_card(sid, data): - ses: Player = sio.get_session(sid) - ses.play_card(data['index'], data['against'], data['with']) - -@sio.event -def respond(sid, data): - ses: Player = sio.get_session(sid) - ses.respond(data) - -@sio.event -def choose(sid, card_index): - ses: Player = sio.get_session(sid) - ses.choose(card_index) - -@sio.event -def scrap(sid, card_index): - ses: Player = sio.get_session(sid) - ses.scrap(card_index) - -@sio.event -def chuck_lose_hp_draw(sid): - ses: Player = sio.get_session(sid) - ses.chuck_lose_hp_draw() - -@sio.event -def holyday_special(sid, data): - ses: Player = sio.get_session(sid) - ses.holyday_special(data) - -@sio.event -def get_cards(sid): - import bang.cards as c - cards = c.get_starting_deck(['dodge_city']) - cards_dict = {} - for ca in cards: - if ca.name not in cards_dict: - cards_dict[ca.name] = ca - cards = [cards_dict[i] for i in cards_dict] - sio.emit('cards_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) - -@sio.event -def get_characters(sid): - import bang.characters as ch - cards = ch.all_characters(['dodge_city']) - sio.emit('characters_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) - -@sio.event -def get_highnooncards(sid): - import bang.expansions.high_noon.card_events as ceh - chs = [] - chs.extend(ceh.get_all_events()) - chs.append(ceh.get_endgame_card()) - sio.emit('highnooncards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) - -@sio.event -def get_foccards(sid): - import bang.expansions.fistful_of_cards.card_events as ce - chs = [] - chs.extend(ce.get_all_events()) - chs.append(ce.get_endgame_card()) - sio.emit('foccards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) - -if __name__ == '__main__': - eventlet.wsgi.server(eventlet.listen(('', 5001)), app) diff --git a/backend/bang/cards.py b/backend/bang/cards.py index 0b98427..41f85c6 100644 --- a/backend/bang/cards.py +++ b/backend/bang/cards.py @@ -42,7 +42,10 @@ class Card(ABC): self.must_be_used = False def __str__(self): - char = ['♦️', '♣️', '♥️', '♠️'][int(self.suit)] + if str(self.suit).isnumeric(): + char = ['♦️', '♣️', '♥️', '♠️'][int(self.suit)] + else: + char = self.suit return f'{self.name} {char}{self.number}' return super().__str__() @@ -71,6 +74,7 @@ class Card(ABC): return False else: player.equipment.append(self) + self.can_be_used_now = False if against: player.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{player.name}|{self.name}|{against}') @@ -84,7 +88,7 @@ class Card(ABC): pass def is_duplicate_card(self, player): - return self.name in [c.name for c in player.equipment] + return self.name in [c.name for c in player.equipment] or self.name in [c.name for c in player.gold_rush_equipment] def check_suit(self, game, accepted): import bang.expansions.high_noon.card_events as ceh @@ -207,15 +211,16 @@ class Bang(Card): def play_card(self, player, against, _with=None): import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh - if player.game.check_event(ceh.Sermone): + if player.game.check_event(ceh.Sermone) and not self.number == 42: # 42 gold rush return False - if player.has_played_bang and (not any([isinstance(c, Volcanic) for c in player.equipment]) or player.game.check_event(ce.Lazo)) and against != None: + if ((player.has_played_bang and not self.number == 42) and (not any([isinstance(c, Volcanic) for c in player.equipment]) or player.game.check_event(ce.Lazo)) and against != None): # 42 gold rush: return False elif against != None: import bang.characters as chars super().play_card(player, against=against) - player.bang_used += 1 - player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else player.bang_used > 1 + if not self.number == 42: # 42 gold rush + player.bang_used += 1 + player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else player.bang_used > 1 if player.character.check(player.game, chars.WillyTheKid): player.has_played_bang = False player.game.attack(player, against, double=player.character.check(player.game, chars.SlabTheKiller)) @@ -230,18 +235,36 @@ class Birra(Card): # self.desc = "Gioca questa carta per recuperare un punto vita. Non puoi andare oltre al limite massimo del tuo personaggio. Se stai per perdere l'ultimo punto vita puoi giocare questa carta anche nel turno dell'avversario. La birra non ha più effetto se ci sono solo due giocatori" # self.desc_eng = "Play this card to regain a life point. You cannot heal more than your character's maximum limit. If you are about to lose your last life point, you can also play this card on your opponent's turn. Beer no longer takes effect if there are only two players" - def play_card(self, player, against, _with=None): + def play_card(self, player, against=None, _with=None, skipChecks=False): import bang.expansions.high_noon.card_events as ceh if player.game.check_event(ceh.IlReverendo): return False - if len(player.game.get_alive_players()) != 2: + if not skipChecks: + import bang.expansions.gold_rush.characters as grch + madamYto = [p for p in player.game.get_alive_players() if p.character.check(player.game, grch.MadamYto) and self.number != 42] + for p in madamYto: + p.hand.append(player.game.deck.draw(True)) + p.notify_self() + if 'gold_rush' in player.game.expansions and self.number != 42: + from bang.players import PendingAction + player.available_cards = [{ + 'name': 'Pepita', + 'icon': '💵️', + 'alt_text': '1', + 'noDesc': True + }, self] + player.choose_text = 'choose_birra_function' + player.pending_action = PendingAction.CHOOSE + player.notify_self() + return True + if (len(player.game.get_alive_players()) != 2 or self.number == 42) and player.lives < player.max_lives: super().play_card(player, against=against) player.lives = min(player.lives+1, player.max_lives) import bang.expansions.dodge_city.characters as chd if player.character.check(player.game, chd.TequilaJoe): player.lives = min(player.lives+1, player.max_lives) return True - elif len(player.game.get_alive_players()) == 2: + elif len(player.game.get_alive_players()) == 2 or player.lives == player.max_lives: player.sio.emit('chat_message', room=player.game.name, data=f'_spilled_beer|{player.name}|{self.name}') return True @@ -255,9 +278,10 @@ class CatBalou(Card): # self.desc = "Fai scartare una carta a un qualsiasi giocatore, scegli a caso dalla mano, oppure fra quelle che ha in gioco" # self.desc_eng = "Choose and discard a card from any other player." self.need_target = True + self.can_target_self = True def play_card(self, player, against, _with=None): - if against != None and (len(player.game.get_player_named(against).hand) + len(player.game.get_player_named(against).equipment)) > 0: + if against != None and (len(player.game.get_player_named(against).hand) + len(player.game.get_player_named(against).equipment)) > 0 and (player.name != against or len(player.equipment) > 0): if self.name == 'Cat Balou': super().play_card(player, against=against) from bang.players import PendingAction @@ -281,7 +305,7 @@ class Diligenza(Card): player.sio.emit('chat_message', room=player.game.name, data=f'_diligenza|{player.name}|{self.name}') for i in range(2): - player.hand.append(player.game.deck.draw()) + player.hand.append(player.game.deck.draw(True)) return True @@ -371,11 +395,12 @@ class Panico(Card): super().__init__(suit, 'Panico!', number, range=1) self.icon = '😱' self.need_target = True + self.can_target_self = True # self.desc = "Pesca una carta da un giocatore a distanza 1, scegli a caso dalla mano, oppure fra quelle che ha in gioco" # self.desc_eng = "Steal a card from a player at distance 1" def play_card(self, player, against, _with=None): - if against != None and (len(player.game.get_player_named(against).hand) + len(player.game.get_player_named(against).equipment)) > 0: + if against != None and (len(player.game.get_player_named(against).hand) + len(player.game.get_player_named(against).equipment)) > 0 and (player.name != against or len(player.equipment) > 0): super().play_card(player, against=against) from bang.players import PendingAction player.pending_action = PendingAction.CHOOSE @@ -415,7 +440,7 @@ class WellsFargo(Card): player.sio.emit('chat_message', room=player.game.name, data=f'_wellsfargo|{player.name}|{self.name}') for i in range(3): - player.hand.append(player.game.deck.draw()) + player.hand.append(player.game.deck.draw(True)) return True diff --git a/backend/bang/characters.py b/backend/bang/characters.py index 374a242..47c66c4 100644 --- a/backend/bang/characters.py +++ b/backend/bang/characters.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from bang.expansions import * from typing import List class Character(ABC): @@ -19,6 +20,13 @@ class Character(ABC): return False return isinstance(self, character) + def special(self, player, data): + import bang.expansions.high_noon.card_events as ceh + if player.game.check_event(ceh.Sbornia): + return False + player.sio.emit('chat_message', room=player.game.name, data=f'_use_special|{player.name}|{self.name}') + return True + class BartCassidy(Character): def __init__(self): super().__init__("Bart Cassidy", max_lives=4) @@ -158,4 +166,6 @@ def all_characters(expansions: List[str]): ] if 'dodge_city' in expansions: base_chars.extend(DodgeCity.get_characters()) + if 'gold_rush' in expansions: + base_chars.extend(GoldRush.get_characters()) return base_chars \ No newline at end of file diff --git a/backend/bang/deck.py b/backend/bang/deck.py index 3f21054..26a207a 100644 --- a/backend/bang/deck.py +++ b/backend/bang/deck.py @@ -3,6 +3,7 @@ import random import bang.cards as cs import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh +import bang.expansions.gold_rush.shop_cards as grc class Deck: def __init__(self, game): @@ -21,20 +22,27 @@ class Deck: self.all_cards_str.append(c.name) self.game = game self.event_cards: List[ce.CardEvent] = [] - endgame_cards = [] + endgame_cards: List[ce.CardEvent] = [] if 'fistful_of_cards' in game.expansions: - self.event_cards.extend(ce.get_all_events()) + self.event_cards.extend(ce.get_all_events(game.rng)) endgame_cards.append(ce.get_endgame_card()) if 'high_noon' in game.expansions: - self.event_cards.extend(ceh.get_all_events()) + self.event_cards.extend(ceh.get_all_events(game.rng)) endgame_cards.append(ceh.get_endgame_card()) if len(self.event_cards) > 0: - random.shuffle(self.event_cards) + game.rng.shuffle(self.event_cards) self.event_cards = self.event_cards[:12] self.event_cards.insert(0, None) self.event_cards.insert(0, None) # 2 perchè iniziale, e primo flip dallo sceriffo - self.event_cards.append(random.choice(endgame_cards)) - random.shuffle(self.cards) + self.event_cards.append(game.rng.choice(endgame_cards)) + game.rng.shuffle(self.cards) + self.shop_deck: List[grc.ShopCard] = [] + self.shop_cards: List[grc.ShopCard] = [] + if 'gold_rush' in game.expansions: + self.shop_cards = [None, None, None] + self.shop_deck = grc.get_cards() + game.rng.shuffle(self.shop_deck) + self.fill_gold_rush_shop() self.scrap_pile: List[cs.Card] = [] print(f'Deck initialized with {len(self.cards)} cards') @@ -43,6 +51,16 @@ class Deck: self.event_cards.append(self.event_cards.pop(0)) self.game.notify_event_card() + def fill_gold_rush_shop(self): + if not any([c == None for c in self.shop_cards]): + return + for i in range(3): + if self.shop_cards[i] == None: + print(f'replacing gr-card {i}') + self.shop_cards[i] = self.shop_deck.pop(0) + self.shop_cards[i].reset_card() + self.game.notify_gold_rush_shop() + def peek(self, n_cards: int) -> list: return self.cards[:n_cards] @@ -73,7 +91,7 @@ class Deck: def reshuffle(self): self.cards = self.scrap_pile[:-1].copy() - random.shuffle(self.cards) + self.game.rng.shuffle(self.cards) self.scrap_pile = self.scrap_pile[-1:] def draw_from_scrap_pile(self) -> cs.Card: @@ -86,6 +104,7 @@ class Deck: return self.draw() def scrap(self, card: cs.Card, ignore_event = False): + if card.number == 42: return card.reset_card() if self.game.check_event(ce.MinieraAbbandonata) and not ignore_event: self.put_on_top(card) diff --git a/backend/bang/expansions/__init__.py b/backend/bang/expansions/__init__.py index eab027e..07b515a 100644 --- a/backend/bang/expansions/__init__.py +++ b/backend/bang/expansions/__init__.py @@ -7,3 +7,8 @@ class DodgeCity(): def get_cards(): from bang.expansions.dodge_city import cards return cards.get_starting_deck() + +class GoldRush(): + def get_characters(): + from bang.expansions.gold_rush import characters + return characters.all_characters() diff --git a/backend/bang/expansions/dodge_city/cards.py b/backend/bang/expansions/dodge_city/cards.py index c46cbed..f503bb3 100644 --- a/backend/bang/expansions/dodge_city/cards.py +++ b/backend/bang/expansions/dodge_city/cards.py @@ -41,7 +41,7 @@ class Schivata(Mancato): return False def use_card(self, player): - player.hand.append(player.game.deck.draw()) + player.hand.append(player.game.deck.draw(True)) player.notify_self() class RagTime(Panico): @@ -225,7 +225,7 @@ class Derringer(Pugnale): def play_card(self, player, against, _with=None): if self.can_be_used_now: - player.hand.append(player.game.deck.draw()) + player.hand.append(player.game.deck.draw(True)) return super().play_card(player, against=against) else: if not self.is_duplicate_card(player): @@ -236,7 +236,7 @@ class Derringer(Pugnale): return False def use_card(self, player): - player.hand.append(player.game.deck.draw()) + player.hand.append(player.game.deck.draw(True)) player.notify_self() class Borraccia(Card): @@ -440,4 +440,5 @@ def get_starting_deck() -> List[Card]: ] for c in cards: c.expansion_icon = '🐄️' + c.expansion = 'dodge_city' return cards diff --git a/backend/bang/expansions/dodge_city/characters.py b/backend/bang/expansions/dodge_city/characters.py index 5567f44..0cf8545 100644 --- a/backend/bang/expansions/dodge_city/characters.py +++ b/backend/bang/expansions/dodge_city/characters.py @@ -85,6 +85,21 @@ class ChuckWengam(Character): # self.desc_eng = "On his turn he may decide to lose 1 HP to draw 2 cards from the deck." self.icon = '💰' + def special(self, player, data): + if super().special(player, data): + if player.lives > 1 and player.is_my_turn: + import bang.expansions.gold_rush.shop_cards as grc + player.lives -= 1 + if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + player.gold_nuggets += 1 + if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + player.hand.append(player.game.deck.draw(True)) + player.hand.append(player.game.deck.draw(True)) + player.hand.append(player.game.deck.draw(True)) + player.notify_self() + return True + return False + class PatBrennan(Character): def __init__(self): super().__init__("Pat Brennan", max_lives=4) @@ -106,6 +121,19 @@ class DocHolyday(Character): # self.desc_eng = "He can discard 2 cards to play a bang." self.icon = '✌🏻' + def special(self, player, data): + if super().special(player, data): + from bang.players import PendingAction + if player.special_use_count < 1 and player.pending_action == PendingAction.PLAY: + player.special_use_count += 1 + cards = sorted(data['cards'], reverse=True) + for c in cards: + player.game.deck.scrap(player.hand.pop(c), True) + player.notify_self() + player.game.attack(player, data['against']) + return True + return False + def all_characters() -> List[Character]: cards = [ PixiePete(), @@ -126,6 +154,7 @@ def all_characters() -> List[Character]: ] for c in cards: c.expansion_icon = '🐄️' + c.expansion = 'dodge_city' return cards #Apache Kid: il suo effetto non conta nei duelli diff --git a/backend/bang/expansions/fistful_of_cards/card_events.py b/backend/bang/expansions/fistful_of_cards/card_events.py index 6266753..ce7bbf9 100644 --- a/backend/bang/expansions/fistful_of_cards/card_events.py +++ b/backend/bang/expansions/fistful_of_cards/card_events.py @@ -101,7 +101,7 @@ def get_endgame_card(): end_game.expansion = 'fistful-of-cards' return end_game -def get_all_events(): +def get_all_events(rng=random): cards = [ Agguato(), Cecchino(), @@ -118,7 +118,7 @@ def get_all_events(): RouletteRussa(), Vendetta(), ] - random.shuffle(cards) + rng.shuffle(cards) for c in cards: c.expansion = 'fistful-of-cards' return cards \ No newline at end of file diff --git a/backend/bang/expansions/gold_rush/characters.py b/backend/bang/expansions/gold_rush/characters.py new file mode 100644 index 0000000..8b62df1 --- /dev/null +++ b/backend/bang/expansions/gold_rush/characters.py @@ -0,0 +1,98 @@ +from typing import List +from bang.characters import Character + +class DonBell(Character): + def __init__(self): + super().__init__("Don Bell", max_lives=4) + # A fine turno estrae, ❤️ o ♦️ gioca di nuovo + self.icon = '🔔️' + +class DutchWill(Character): + def __init__(self): + super().__init__("Dutch Will", max_lives=4) + # Pesca 2 ne scarta 1 e prende 1 pepita + self.icon = '🧐️' + +class JackyMurieta(Character): + def __init__(self): + super().__init__("Jacky Murieta", max_lives=4) + # puo pagare 2 pepite per sparare 1 bang extra + self.icon = '💆‍♂️️' + + def special(self, player, data): + if super().special(player, data): + if player.gold_nuggets >= 2 and player.is_my_turn: + player.gold_nuggets -= 2 + player.has_played_bang = False + player.bang_used -= 1 + player.notify_self() + return True + return False + +class JoshMcCloud(Character): + def __init__(self): + super().__init__("Josh McCloud", max_lives=4) + # puo pagare 2 pepite per pescare il primo equipaggiamento dalla pila gold rush + self.icon = '⛅️' + + def special(self, player, data): + if super().special(player, data): + if player.gold_nuggets >= 2 and player.is_my_turn: + player.gold_nuggets -= 2 + card = player.game.deck.shop_deck.pop(0) + if card.play_card(player): + player.game.deck.shop_deck.append(card) + player.notify_self() + return True + return False + +class MadamYto(Character): + def __init__(self): + super().__init__("Madam Yto", max_lives=4) + # quando viene giocata 1 birra pesca 1 carta + self.icon = '💃️' + +class PrettyLuzena(Character): + def __init__(self): + super().__init__("Pretty Luzena", max_lives=4) + # una volta per turno ha 1 sconto di 1 pepita sugli equipaggiamenti + self.icon = '👛️' + +class RaddieSnake(Character): + def __init__(self): + super().__init__("Raddie Snake", max_lives=4) + # può scartare 1 pepita per pescare 1 carta (2 volte per turno) + self.icon = '🐍️' + + def special(self, player, data): + if super().special(player, data): + if player.gold_nuggets >= 1 and player.is_my_turn and player.special_use_count < 2: + player.gold_nuggets -= 1 + player.special_use_count += 1 + player.hand.append(player.game.deck.draw(True)) + player.notify_self() + return True + return False + +class SimeonPicos(Character): + def __init__(self): + super().__init__("Simeon Picos", max_lives=4) + # ottiene 1 pepita ogni volta che perde 1 punto vita + self.icon = '🏇️' + + +def all_characters() -> List[Character]: + cards = [ + DonBell(), + DutchWill(), + JackyMurieta(), + JoshMcCloud(), + MadamYto(), + PrettyLuzena(), + RaddieSnake(), + SimeonPicos(), + ] + for c in cards: + c.expansion_icon = '🤑️' + c.expansion = 'gold_rush' + return cards diff --git a/backend/bang/expansions/gold_rush/shop_cards.py b/backend/bang/expansions/gold_rush/shop_cards.py new file mode 100644 index 0000000..4757a88 --- /dev/null +++ b/backend/bang/expansions/gold_rush/shop_cards.py @@ -0,0 +1,261 @@ +from bang.cards import * +import bang.roles as r +import bang.players as pl + +class ShopCardKind(IntEnum): + BROWN = 0 # Se l’equipaggiamento ha il bordo marrone, applicane subito l’effetto e poi scartalo. + BLACK = 1 # Se l’equipaggiamento ha il bordo nero, tienilo scoperto di fronte a te. + +class ShopCard(Card): + def __init__(self, name:str, cost:int, kind:ShopCardKind): + super().__init__(suit='💵', number=cost, name=name) + self.kind = kind + self.expansion_icon = '🤑️' + self.expansion = 'gold_rush' + + def play_card(self, player, against, _with=None): + if self.kind == ShopCardKind.BROWN: + player.sio.emit('chat_message', room=player.game.name, data=f'_purchase_card|{player.name}|{self.name}') + return True + elif self.kind == ShopCardKind.BLACK: # equip it + if not self.is_duplicate_card(player): + self.reset_card() + self.can_be_used_now = True + player.gold_rush_equipment.append(self) + player.sio.emit('chat_message', room=player.game.name, data=f'_purchase_card|{player.name}|{self.name}') + return True + else: + return False + + def reset_card(self): + if self.kind == ShopCardKind.BLACK: + self.can_be_used_now = False + +class Bicchierino(ShopCard): + def __init__(self): + super().__init__('Bicchierino', 1, ShopCardKind.BROWN) + self.icon = '🍸️' + + def play_card(self, player, against=None, _with=None): + player.available_cards = [{ + 'name': p.name, + 'icon': p.role.icon if(player.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', + 'alt_text': ''.join(['❤️']*p.lives)+''.join(['💀']*(p.max_lives-p.lives)), + 'is_character': True, + 'noDesc': True + } for p in player.game.get_alive_players()] + player.choose_text = 'choose_bicchierino' + player.pending_action = pl.PendingAction.CHOOSE + player.notify_self() + return super().play_card(player, against, _with) + +class Bottiglia(ShopCard): + def __init__(self): + super().__init__('Bottiglia', 2, ShopCardKind.BROWN) + self.icon = '🍾️' + + def play_card(self, player, against=None, _with=None): + # bang, birra, panico + player.available_cards = [Bang(1,42), Birra(1,42), Panico(1,42)] + for i in range(len(player.available_cards)): + player.available_cards[i].must_be_used = True + player.choose_text = 'choose_bottiglia' + player.pending_action = pl.PendingAction.CHOOSE + player.notify_self() + return super().play_card(player, against, _with) + +class Complice(ShopCard): + def __init__(self): + super().__init__('Complice', 2, ShopCardKind.BROWN) + self.icon = '😉️' + + def play_card(self, player, against=None, _with=None): + # emporio, duello, Cat balou + player.available_cards = [Emporio(1,42), Duello(1,42), CatBalou(1,42)] + for i in range(len(player.available_cards)): + player.available_cards[i].must_be_used = True + player.choose_text = 'choose_complice' + player.pending_action = pl.PendingAction.CHOOSE + player.notify_self() + return super().play_card(player, against, _with) + +class CorsaAllOro(ShopCard): + def __init__(self): + super().__init__("Corsa All Oro_gr", 5, ShopCardKind.BROWN) + self.icon = '🤑️' + + def play_card(self, player, against=None, _with=None): + player.lives = player.max_lives + player.play_turn() + return super().play_card(player, against, _with) + +class Rum(ShopCard): + def __init__(self): + super().__init__("Rum", 3, ShopCardKind.BROWN) + self.icon = '🍷️' + + def play_card(self, player, against=None, _with=None): + # Estrai 4 carte e ottieni 1 hp per ogni seme diverso + import bang.characters as c + suits = set() + num = 5 if player.character.check(player.game, c.LuckyDuke) else 4 + for i in range(num): + c = player.game.deck.pick_and_scrap() + player.sio.emit('chat_message', room=player.game.name, data=f'_flipped|{player.name}|{c.name}|{c.num_suit()}') + suits.add(c.suit) + player.lives = min(player.lives+len(suits), player.max_lives) + return super().play_card(player, against, _with) + +class UnionPacific(ShopCard): + def __init__(self): + super().__init__("Union Pacific", 4, ShopCardKind.BROWN) + self.icon = '🚆️' + + def play_card(self, player, against=None, _with=None): + player.sio.emit('chat_message', room=player.game.name, + data=f'_UnionPacific|{player.name}|{self.name}') + for i in range(4): + player.hand.append(player.game.deck.draw(True)) + return super().play_card(player, against, _with) + +class Calumet(ShopCard): + def __init__(self): + super().__init__("Calumet", 3, ShopCardKind.BLACK) + self.icon = '🚭️' + + def play_card(self, player, against=None, _with=None): + return super().play_card(player, against, _with) + # ti rende immuni ai quadri + +class Cinturone(ShopCard): + def __init__(self): + super().__init__("Cinturone", 2, ShopCardKind.BLACK) + self.icon = '🥡' + + def play_card(self, player, against=None, _with=None): + return super().play_card(player, against, _with) + # max carte a fine turno 8 + +class FerroDiCavallo(ShopCard): + def __init__(self): + super().__init__("Ferro di Cavallo", 2, ShopCardKind.BLACK) + self.icon = '🎠' + + def play_card(self, player, against=None, _with=None): + return super().play_card(player, against, _with) + # estrai come luky duke + +class Piccone(ShopCard): + def __init__(self): + super().__init__("Piccone", 4, ShopCardKind.BLACK) + self.icon = '⛏️' + + def play_card(self, player, against=None, _with=None): + return super().play_card(player, against, _with) + # peschi una carta in piu a inizio turno + +class Ricercato(ShopCard): + def __init__(self): + super().__init__("Ricercato", 2, ShopCardKind.BLACK) + self.icon = '🤠️' + self.can_target_self = True + + def play_card(self, player, against=None, _with=None): + player.sio.emit('chat_message', room=player.game.name, data=f'_purchase_card|{player.name}|{self.name}') + player.available_cards = [{ + 'name': p.name, + 'icon': p.role.icon if(player.game.initial_players == 3) else '🤠', + 'alt_text': ''.join(['❤️']*p.lives)+''.join(['💀']*(p.max_lives-p.lives)), + 'is_character': True, + 'noDesc': True + } for p in player.game.get_alive_players() if p != player and not isinstance(p.role, r.Sheriff)] + player.available_cards.append({'name': player.name, 'number':0,'icon': 'you', 'is_character': True}) + player.choose_text = 'choose_ricercato' + player.pending_action = pl.PendingAction.CHOOSE + player.notify_self() + return True + # la giochi su un altro giocatore, ricompensa di 2 carte e 1 pepita a chi lo uccide + +class Setaccio(ShopCard): + def __init__(self): + super().__init__("Setaccio", 3, ShopCardKind.BLACK) + self.icon = '🥘️' + + def play_card(self, player, against=None, _with=None): + if not self.can_be_used_now: + return super().play_card(player, against, _with) + else: + if player.gold_nuggets >= 1 and player.setaccio_count < 2: + player.sio.emit('chat_message', room=player.game.name, data=f'_play_card|{player.name}|{self.name}') + player.gold_nuggets -= 1 + player.setaccio_count += 1 + player.hand.append(player.game.deck.draw(True)) + player.notify_self() + return True + return False + # paghi 1 pepita per pescare 1 carta durante il tuo turno (max 2 volte per turno) + +class Stivali(ShopCard): + def __init__(self): + super().__init__("Stivali", 3, ShopCardKind.BLACK) + self.icon = '🥾️' + + def play_card(self, player, against=None, _with=None): + return super().play_card(player, against, _with) + # peschi una carta ogni volta che vieni ferito + +class Talismano(ShopCard): + def __init__(self): + super().__init__("Talismano", 3, ShopCardKind.BLACK) + self.icon = '🧿' + + def play_card(self, player, against=None, _with=None): + return super().play_card(player, against, _with) + # ottieni una pepita ogni volta che vieni ferito + +class Zaino(ShopCard): + def __init__(self): + super().__init__("Zaino", 3, ShopCardKind.BLACK) + self.icon = '🎒️' + + def play_card(self, player, against=None, _with=None): + if not self.can_be_used_now: + return super().play_card(player, against, _with) + else: + if player.gold_nuggets >= 2: + player.sio.emit('chat_message', room=player.game.name, data=f'_play_card|{player.name}|{self.name}') + player.gold_nuggets -= 2 + player.lives = min(player.lives + 1, player.max_lives) + player.notify_self() + return True + return False + # paga 2 pepite per recuperare 1 vita + +def get_cards() -> List[Card]: + cards = [ + Bicchierino(), + Bicchierino(), + Bicchierino(), + Bottiglia(), + Bottiglia(), + Bottiglia(), + Complice(), + Complice(), + Complice(), + CorsaAllOro(), + Rum(), + Rum(), + UnionPacific(), + Calumet(), + Cinturone(), + FerroDiCavallo(), + Piccone(), + Ricercato(), + Ricercato(), + Ricercato(), + Setaccio(), + Stivali(), + Talismano(), + Zaino(), + ] + return cards diff --git a/backend/bang/expansions/high_noon/card_events.py b/backend/bang/expansions/high_noon/card_events.py index 7f79fca..5826827 100644 --- a/backend/bang/expansions/high_noon/card_events.py +++ b/backend/bang/expansions/high_noon/card_events.py @@ -96,7 +96,7 @@ def get_endgame_card(): end_game.expansion = 'high-noon' return end_game -def get_all_events(): +def get_all_events(rng=random): cards = [ Benedizione(), Maledizione(), @@ -113,7 +113,7 @@ def get_all_events(): Manette(), NuovaIdentita(), ] - random.shuffle(cards) + rng.shuffle(cards) for c in cards: c.expansion = 'high-noon' return cards \ No newline at end of file diff --git a/backend/bang/game.py b/backend/bang/game.py index af15d1c..ce0eeec 100644 --- a/backend/bang/game.py +++ b/backend/bang/game.py @@ -7,11 +7,13 @@ import eventlet import bang.players as pl import bang.characters as characters +import bang.expansions.dodge_city.characters as chd from bang.deck import Deck import bang.roles as roles import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh - +import bang.expansions.gold_rush.shop_cards as grc +import bang.expansions.gold_rush.characters as grch class Game: def __init__(self, name, sio:socketio): super().__init__() @@ -26,8 +28,8 @@ class Game: self.waiting_for = 0 self.initial_players = 0 self.password = '' - self.expansions = [] - self.available_expansions = ['dodge_city', 'fistful_of_cards', 'high_noon'] + self.expansions: List[str] = [] + self.available_expansions = ['dodge_city', 'fistful_of_cards', 'high_noon', 'gold_rush'] self.shutting_down = False self.is_competitive = False self.disconnect_bot = True @@ -38,22 +40,28 @@ class Game: self.incremental_turn = 0 self.did_resuscitate_deadman = False self.is_handling_death = False - self.pending_winners = [] + self.pending_winners: List[pl.Player] = [] self.someone_won = False self.attack_in_progress = False self.characters_to_distribute = 2 # personaggi da dare a inizio partita self.debug = self.name == 'debug' self.is_changing_pwd = False self.is_hidden = False + self.rng = random.Random() + self.rpc_log = [] + self.is_replay = False def reset(self): - print('resetting lobby') + print(f'{self.name}: resetting lobby') self.players.extend(self.spectators) self.spectators = [] for bot in [p for p in self.players if p.is_bot]: - bot.game = None + if bot.was_player: + bot.is_bot = False + else: + bot.game = None self.players = [p for p in self.players if not p.is_bot] - print(self.players) + print(f'{self.name}: players: {self.players}') self.started = False self.is_handling_death = False self.waiting_for = 0 @@ -66,6 +74,54 @@ class Game: eventlet.sleep(0.5) self.notify_room() + def replay(self, log): + from tests.dummy_socket import DummySocket + self.players = [] + self.is_hidden = True + self.is_replay = True + self.replay_speed = 1 + for i in range(len(log)): + print('replay:', i, 'of', len(log)) + cmd = log[i].split(';') + if cmd[1] == 'players': + self.expansions = json.loads(cmd[4].replace("'",'"')) + pnames = json.loads(cmd[3].replace("'",'"')) + for p in pnames: + self.add_player(pl.Player(p, p, DummySocket(self.sio), bot=False)) + continue + if cmd[1] == 'start_game': + self.start_game(int(cmd[2])) + continue + player = [p for p in self.players if p.name == cmd[0]][0] + if cmd[1] == 'set_character': + if player.character != None and isinstance(player.real_character, chd.VeraCuster): + player.set_available_character([p.character for p in self.get_alive_players() if p != player]) + player.set_character(cmd[2]) + if cmd[1] == 'draw': + player.draw(cmd[2]) + if cmd[1] == 'pick': + player.pick() + if cmd[1] == 'end_turn': + player.end_turn() + if cmd[1] == 'play_card': + data = json.loads(cmd[2]) + player.play_card(data['index'], data['against'], data['with']) + if cmd[1] == 'respond': + player.respond(int(cmd[2])) + if cmd[1] == 'choose': + player.choose(int(cmd[2])) + if cmd[1] == 'scrap': + player.scrap(int(cmd[2])) + if cmd[1] == 'special': + player.special(json.loads(cmd[2])) + if cmd[1] == 'gold_rush_discard': + player.gold_rush_discard() + if cmd[1] == 'buy_gold_rush_card': + player.buy_gold_rush_card(int(cmd[2])) + # if cmd[1] == 'chat_message': + # chat_message(None, cmd[2], player) + eventlet.sleep(max(self.replay_speed, 0.1)) + def notify_room(self, sid=None): if len([p for p in self.players if p.character == None]) != 0 or sid: self.sio.emit('room', room=self.name if not sid else sid, data={ @@ -80,15 +136,37 @@ class Game: }) self.sio.emit('debug', room=self.name, data=self.debug) if self.debug: - commands = ['/debug', '/set_chars', '/suicide', '/nextevent', '/notify', '/show_cards', '/ddc', '/dsh', '/togglebot', '/cancelgame', '/startgame', '/setbotspeed', '/addex', '/setcharacter', '/setevent', '/removecard', '/getcard', '/meinfo', '/gameinfo', '/mebot'] + commands = [ + {'cmd':'/debug', 'help':'Toggles the debug mode'}, + {'cmd':'/set_chars', 'help':'Set how many characters to distribute - sample /set_chars 3'}, + {'cmd':'/suicide', 'help':'Kills you'}, + {'cmd':'/nextevent', 'help':'Flip the next event card'}, + {'cmd':'/notify', 'help':'Send a message to a player - sample /notify player hi!'}, + {'cmd':'/show_cards', 'help':'View the hand of another - sample /show_cards player'}, + {'cmd':'/ddc', 'help':'Destroy all cards - sample /ddc player'}, + {'cmd':'/dsh', 'help':'Set health - sample /dsh player'}, + # {'cmd':'/togglebot', 'help':''}, + {'cmd':'/cancelgame', 'help':'Stops the current game'}, + {'cmd':'/startgame', 'help':'Force starts the game'}, + {'cmd':'/setbotspeed', 'help':'Changes the bot response time - sample /setbotspeed 0.5'}, + # {'cmd':'/addex', 'help':''}, + {'cmd':'/setcharacter', 'help':'Changes your current character - sample /setcharacter Willy The Kid'}, + {'cmd':'/setevent', 'help':'Changes the event deck - sample /setevent 0 Manette'}, + {'cmd':'/removecard', 'help':'Remove a card from hand/equip - sample /removecard 0'}, + {'cmd':'/getcard', 'help':'Get a brand new card - sample /getcard Birra'}, + {'cmd':'/meinfo', 'help':'Get player data'}, + {'cmd':'/gameinfo', 'help':'Get game data'}, + {'cmd':'/mebot', 'help':'Toggles bot mode'}, + {'cmd':'/getnuggets', 'help':'Adds nuggets to yourself - sample /getnuggets 5'}, + {'cmd':'/startwithseed', 'help':'start the game with custom seed'}] self.sio.emit('commands', room=self.name, data=commands) else: - self.sio.emit('commands', room=self.name, data=['/debug']) + self.sio.emit('commands', room=self.name, data=[{'cmd':'/debug', 'help':'Toggles the debug mode'}]) self.sio.emit('spectators', room=self.name, data=len(self.spectators)) def toggle_expansion(self, expansion_name): if not self.started: - print('toggling', expansion_name) + print(f'{self.name}: toggling', expansion_name) if expansion_name in self.expansions: self.expansions.remove(expansion_name) else: @@ -113,7 +191,7 @@ class Game: self.expansions.append('dodge_city') player.join_game(self) self.players.append(player) - print(f'Added player {player.name} to game') + print(f'{self.name}: Added player {player.name} to game') self.notify_room() self.sio.emit('chat_message', room=self.name, data=f'_joined|{player.name}') @@ -133,16 +211,14 @@ class Game: self.notify_room() if len([p for p in self.players if p.character == None]) == 0: for i in range(len(self.players)): - print(self.name) - print(self.players[i].name) - print(self.players[i].character) + print(self.name, self.players[i].name, self.players[i].character) self.sio.emit('chat_message', room=self.name, data=f'_choose_character|{self.players[i].name}|{self.players[i].character.name}') self.players[i].prepare() for k in range(self.players[i].max_lives): self.players[i].hand.append(self.deck.draw()) self.players[i].notify_self() current_roles = [x.role.name for x in self.players] - random.shuffle(current_roles) + self.rng.shuffle(current_roles) cr = '' for x in current_roles: if (x not in cr): @@ -152,14 +228,21 @@ class Game: def choose_characters(self): n = self.characters_to_distribute - char_cards = random.sample(characters.all_characters(self.expansions), len(self.players)*n) + char_cards = self.rng.sample(characters.all_characters(self.expansions), len(self.players)*n) for i in range(len(self.players)): self.players[i].set_available_character(char_cards[i * n : i * n + n]) - def start_game(self): - print('GAME IS STARING') + def start_game(self, SEED=None): if self.started: return + print(f'{self.name}: GAME IS STARING') + if SEED == None: + import time + SEED = int(time.time()) + print(f'{self.name}: SEED IS {SEED}') + self.SEED = SEED + self.rpc_log = [f';players;{len(self.players)};{[p.name for p in self.players]};{self.expansions}', f';start_game;{SEED}'] + self.rng = random.Random(SEED) self.players_map = {c.name: i for i, c in enumerate(self.players)} self.sio.emit('chat_message', room=self.name, data=f'_starting') self.sio.emit('start', room=self.name) @@ -170,6 +253,8 @@ class Game: self.initial_players = len(self.players) self.distribute_roles() self.choose_characters() + if 'gold_rush' in self.expansions: + self.notify_gold_rush_shop() def distribute_roles(self): available_roles: List[roles.Role] = [] @@ -184,7 +269,7 @@ class Game: available_roles = available_roles[:len(self.players)] else: available_roles = [roles.Renegade(), roles.Renegade()] - random.shuffle(available_roles) + self.rng.shuffle(available_roles) for i in range(len(self.players)): self.players[i].set_role(available_roles[i]) if isinstance(available_roles[i], roles.Sheriff) or (len(available_roles) == 3 and isinstance(available_roles[i], roles.Vice)): @@ -208,7 +293,7 @@ class Game: if self.waiting_for == 0: attacker.pending_action = pl.PendingAction.PLAY attacker.notify_self() - self.attack_in_progress = False + self.attack_in_progress = False if self.pending_winners and not self.someone_won: return self.announces_winners() @@ -226,7 +311,7 @@ class Game: if self.waiting_for == 0: attacker.pending_action = pl.PendingAction.PLAY attacker.notify_self() - self.attack_in_progress = False + self.attack_in_progress = False if self.pending_winners and not self.someone_won: return self.announces_winners() @@ -271,7 +356,13 @@ class Game: player.notify_self() pls = self.get_alive_players() nextPlayer = pls[(pls.index(self.players[self.turn])+(len(pls)-len(self.available_cards))) % len(pls)] - if nextPlayer == self.players[self.turn]: + if len(self.available_cards) == 1: + nextPlayer.hand.append(self.available_cards.pop()) + nextPlayer.notify_self() + self.sio.emit('emporio', room=self.name, data='{"name":"","cards":[]}') + self.players[self.turn].pending_action = pl.PendingAction.PLAY + self.players[self.turn].notify_self() + elif nextPlayer == self.players[self.turn]: self.sio.emit('emporio', room=self.name, data='{"name":"","cards":[]}') self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].notify_self() @@ -287,32 +378,38 @@ class Game: return self.players[self.players_map[name]] def responders_did_respond_resume_turn(self, did_lose=False): - print('did_lose', did_lose) + print(f'{self.name}: did_lose', did_lose) if self.player_bangs > 0 and self.check_event(ce.PerUnPugnoDiCarte): self.player_bangs -= 1 if self.player_bangs >= 1: - print('bang again') + print(f'{self.name}: bang again') if self.players[self.turn].get_banged(self.deck.event_cards[0]): self.players[self.turn].notify_self() else: self.responders_did_respond_resume_turn() else: - print('ok play turn now') + print(f'{self.name}: ok play turn now') self.player_bangs = 0 self.players[self.turn].play_turn() elif self.is_russian_roulette_on and self.check_event(ce.RouletteRussa): pls = self.get_alive_players() if did_lose: target_pl = pls[(pls.index(self.players[self.turn]) + self.player_bangs) % len(pls)] - print('stop roulette') + print(f'{self.name}: stop roulette') target_pl.lives -= 1 + if len([c for c in target_pl.equipment if isinstance(c, grc.Talismano)]) > 0: + target_pl.gold_nuggets += 1 + if target_pl.character.check(self, grch.SimeonPicos): + target_pl.gold_nuggets += 1 + if len([c for c in target_pl.equipment if isinstance(c, grc.Stivali)]) > 0: + target_pl.hand.append(self.deck.draw(True)) target_pl.notify_self() self.is_russian_roulette_on = False self.players[self.turn].play_turn() else: self.player_bangs += 1 target_pl = pls[(pls.index(self.players[self.turn]) + self.player_bangs) % len(pls)] - print(f'next in line {target_pl.name}') + print(f'{self.name}: next in line {target_pl.name}') if target_pl.get_banged(self.deck.event_cards[0]): target_pl.notify_self() else: @@ -322,11 +419,12 @@ class Game: if self.ready_count == self.waiting_for: self.waiting_for = 0 self.ready_count = 0 + self.attack_in_progress = False if self.pending_winners and not self.someone_won: return self.announces_winners() if self.dalton_on: self.dalton_on = False - print(f'notifying {self.players[self.turn].name} about his turn') + print(f'{self.name}: notifying {self.players[self.turn].name} about his turn') self.players[self.turn].play_turn() else: self.players[self.turn].pending_action = pl.PendingAction.PLAY @@ -334,19 +432,22 @@ class Game: def announces_winners(self, winners=None): if (winners is None): - print('WE HAVE A WINNER - pending winners') + print(f'{self.name}: WE HAVE A WINNER - pending winners') else: - print('WE HAVE A WINNER') - for p in self.get_alive_players(): + print(f'{self.name}: WE HAVE A WINNER') + for p in self.players: if winners is None: p.win_status = p in self.pending_winners else: p.win_status = p in winners - if p.win_status: + if p.win_status and not (isinstance(p.role, roles.Renegade) and p.is_dead): if not self.someone_won: self.someone_won = True - self.sio.emit('chat_message', room=self.name, data=f'_won|{p.name}') + self.sio.emit('chat_message', room=self.name, data=f'_won|{p.name}|{p.role.name}') p.notify_self() + if hasattr(self.sio, 'is_fake'): + print('announces_winners(): Running for tests, you will have to call reset manually!') + return for i in range(5): self.sio.emit('chat_message', room=self.name, data=f'_lobby_reset|{5-i}') eventlet.sleep(1) @@ -361,7 +462,7 @@ class Game: if self.players[self.turn].is_dead: pl = sorted(self.get_dead_players(), key=lambda x:x.death_turn)[0] if self.check_event(ce.DeadMan) and not self.did_resuscitate_deadman and pl == self.players[self.turn]: - print(f'{self.players[self.turn]} is dead, revive') + print(f'{self.name}: {self.players[self.turn]} is dead, revive') self.did_resuscitate_deadman = True pl.is_dead = False pl.is_ghost = False @@ -370,16 +471,16 @@ class Game: pl.hand.append(self.deck.draw()) pl.notify_self() elif self.check_event(ceh.CittaFantasma): - print(f'{self.players[self.turn]} is dead, event ghost') + print(f'{self.name}: {self.players[self.turn]} is dead, event ghost') self.players[self.turn].is_ghost = True else: - print(f'{self.players[self.turn]} is dead, next turn') + print(f'{self.name}: {self.players[self.turn]} is dead, next turn') return self.next_turn() self.player_bangs = 0 if isinstance(self.players[self.turn].role, roles.Sheriff) or ((self.initial_players == 3 and isinstance(self.players[self.turn].role, roles.Vice) and not self.players[self.turn].is_ghost) or (self.initial_players == 3 and any([p for p in self.players if p.is_dead and p.role.name == 'Vice']) and isinstance(self.players[self.turn].role, roles.Renegade))): self.deck.flip_event() if len(self.deck.event_cards) > 0 and self.deck.event_cards[0] != None: - print(f'flip new event {self.deck.event_cards[0].name}') + print(f'{self.name}: flip new event {self.deck.event_cards[0].name}') self.sio.emit('chat_message', room=self.name, data={'color': f'orange','text':f'_flip_event|{self.deck.event_cards[0].name}'}) if self.check_event(ce.DeadMan): self.did_resuscitate_deadman = False @@ -395,9 +496,10 @@ class Game: if len(most_hurt) > 0: hurt_players = [p for p in self.players if p.lives == min(most_hurt)] for p in hurt_players: - p.lives += 1 - self.sio.emit('chat_message', room=self.name, data=f'_doctor_heal|{p.name}') - p.notify_self() + if p.lives != p.max_lives: + p.lives += 1 + self.sio.emit('chat_message', room=self.name, data=f'_doctor_heal|{p.name}') + p.notify_self() elif self.check_event(ceh.IDalton): self.waiting_for = 0 self.ready_count = 0 @@ -417,14 +519,14 @@ class Game: else: self.responders_did_respond_resume_turn() else: - print(f'notifying {self.players[self.turn].name} about his turn') + print(f'{self.name}: notifying {self.players[self.turn].name} about his turn') self.players[self.turn].play_turn() def next_turn(self): if self.shutting_down: return - print(f'{self.players[self.turn].name} invoked next turn') + print(f'{self.name}: {self.players[self.turn].name} invoked next turn') if self.pending_winners and not self.someone_won: - return self.announces_winners() + return self.announces_winners() pls = self.get_alive_players() if len(pls) > 0: if self.check_event(ceh.CorsaAllOro): @@ -441,8 +543,14 @@ class Game: else: self.sio.emit('event_card', room=room, data=None) + def notify_gold_rush_shop(self, sid=None): + if 'gold_rush' in self.expansions and self.deck and self.deck.shop_cards and len(self.deck.shop_cards) > 0: + room = self.name if sid == None else sid + print(f'{self.name}: gold_rush_shop room={room}, data={self.deck.shop_cards}') + self.sio.emit('gold_rush_shop', room=room, data=json.dumps(self.deck.shop_cards, default=lambda o: o.__dict__)) + def notify_scrap_pile(self, sid=None): - print('scrap') + print(f'{self.name}: scrap') room = self.name if sid == None else sid if self.deck.peek_scrap_pile(): self.sio.emit('scrap', room=room, data=self.deck.peek_scrap_pile().__dict__) @@ -450,7 +558,7 @@ class Game: self.sio.emit('scrap', room=room, data=None) def handle_disconnect(self, player: pl.Player): - print(f'player {player.name} left the game {self.name}') + print(f'{self.name}: player {player.name} left the game') if player in self.spectators: self.spectators.remove(player) self.sio.emit('spectators', room=self.name, data=len(self.spectators)) @@ -462,7 +570,7 @@ class Game: if len([p for p in self.players if not p.is_bot]) == 0: eventlet.sleep(5) if len([p for p in self.players if not p.is_bot]) == 0: - print(f'no players left in game {self.name}, shutting down') + print(f'{self.name}: no players left in game, shutting down') self.shutting_down = True self.players = [] self.spectators = [] @@ -470,6 +578,7 @@ class Game: return True eventlet.sleep(15) # he may reconnect if player.is_bot: + player.was_player = False if len(player.available_characters) > 0: player.set_available_character(player.available_characters) player.bot_spin() @@ -479,7 +588,7 @@ class Game: # player.lives = 0 # self.players.remove(player) if len([p for p in self.players if not p.is_bot]) == 0: - print(f'no players left in game {self.name}, shutting down') + print(f'{self.name}: no players left in game, shutting down') self.shutting_down = True self.players = [] self.spectators = [] @@ -491,18 +600,26 @@ class Game: if not player in self.players or player.is_ghost: return self.is_handling_death = True import bang.expansions.dodge_city.characters as chd - print(player.attacker) + print(f'{self.name}: the killer is {player.attacker}') + if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Ricercato)]) > 0 and player.attacker and player.attacker in self.players: + player.attacker.gold_nuggets += 1 + player.attacker.hand.append(self.deck.draw(True)) + player.attacker.hand.append(self.deck.draw(True)) + player.attacker.notify_self() + # se lo sceriffo uccide il proprio vice if player.attacker and player.attacker in self.players and isinstance(player.attacker.role, roles.Sheriff) and isinstance(player.role, roles.Vice): for i in range(len(player.attacker.hand)): self.deck.scrap(player.attacker.hand.pop(), True) for i in range(len(player.attacker.equipment)): self.deck.scrap(player.attacker.equipment.pop(), True) + for i in range(len(player.attacker.gold_rush_equipment)): + self.deck.shop_deck.append(player.attacker.gold_rush_equipment.pop()) player.attacker.notify_self() elif player.attacker and player.attacker in self.players and (isinstance(player.role, roles.Outlaw) or self.initial_players == 3): for i in range(3): player.attacker.hand.append(self.deck.draw(True)) player.attacker.notify_self() - print(f'player {player.name} died') + print(f'{self.name}: player {player.name} died') if self.waiting_for > 0 and player.pending_action == pl.PendingAction.RESPOND: self.responders_did_respond_resume_turn() player.pending_action = pl.PendingAction.WAIT @@ -529,17 +646,22 @@ class Game: p.notify_self() # self.players_map = {c.name: i for i, c in enumerate(self.players)} if self.started: - print('Check win status') + print(f'{self.name}: Check win status') attacker_role = None if player.attacker and player.attacker in self.players: attacker_role = player.attacker.role winners = [p for p in self.players if p.role != None and p.role.on_player_death(self.get_alive_players(), initial_players=self.initial_players, dead_role=player.role, attacker_role=attacker_role)] - #print(f'win check: ready-{self.ready_count} waiting-{self.waiting_for} winners:{len(winners)}') if not self.attack_in_progress and len(winners) > 0 and not self.someone_won: return self.announces_winners(winners) elif len(winners) > 0 and not self.someone_won: # non tutti hanno risposto, ma ci sono vincitori. self.pending_winners = winners + for i in range(len(player.gold_rush_equipment)): + self.deck.shop_deck.append(player.gold_rush_equipment.pop()) # vulture sam doesnt get these cards + + #il giocatore quando muore perde tutte le pepite se non è pistolero ombra + player.gold_nuggets = 0 + vulture = [p for p in self.get_alive_players() if p.character.check(self, characters.VultureSam)] if len(vulture) == 0: for i in range(len(player.hand)): @@ -578,26 +700,25 @@ class Game: herb[i].hand.append(self.deck.draw(True)) herb[i].hand.append(self.deck.draw(True)) herb[i].notify_self() + self.is_handling_death = False if corpse.is_my_turn: corpse.is_my_turn = False corpse.notify_self() self.next_turn() - def check_event(self, ev): if self.deck == None or len(self.deck.event_cards) == 0: return False return isinstance(self.deck.event_cards[0], ev) - def get_visible_players(self, player: pl.Player): + def get_visible_players(self, player: pl.Player): # returns a dictionary because we need to add the distance pls = self.get_alive_players() if len(pls) == 0 or player not in pls: return [] i = pls.index(player) - sight = player.get_sight() mindist = 99 if not self.check_event(ce.Agguato) else 1 return [{ 'name': pls[j].name, - 'dist': min([abs(i - j), (i+ abs(j-len(pls))), (j+ abs(i-len(pls))), mindist]) + pls[j].get_visibility() - (player.get_sight(countWeapon=False)-1), + 'dist': min([abs(i - j), (i+ abs(j-len(pls))), (j+ abs(i-len(pls))), mindist]) + pls[j].get_visibility(), 'lives': pls[j].lives, 'max_lives': pls[j].max_lives, 'is_sheriff': isinstance(pls[j].role, roles.Sheriff), @@ -620,8 +741,10 @@ class Game: 'name': p.name, 'ncards': len(p.hand), 'equipment': [e.__dict__ for e in p.equipment], + 'gold_rush_equipment': [e.__dict__ for e in p.gold_rush_equipment], 'lives': p.lives, 'max_lives': p.max_lives, + 'gold_nuggets': p.gold_nuggets, 'is_sheriff': isinstance(p.role, roles.Sheriff), 'is_my_turn': p.is_my_turn, 'pending_action': p.pending_action, diff --git a/backend/bang/players.py b/backend/bang/players.py index af66efa..530b28d 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -10,7 +10,10 @@ import bang.characters as chars import bang.expansions.dodge_city.characters as chd import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh +import bang.expansions.gold_rush.shop_cards as grc +import bang.expansions.gold_rush.characters as grch import eventlet +from typing import List class PendingAction(IntEnum): PICK = 0 @@ -33,8 +36,8 @@ class Player: self.reset() def reset(self): - self.hand: cs.Card = [] - self.equipment: cs.Card = [] + self.hand: List[cs.Card] = [] + self.equipment: List[cs.Card] = [] self.role: r.Role = None self.character: chars.Character = None self.real_character: chars.Character = None @@ -47,7 +50,7 @@ class Player: self.can_play_ranch = True self.is_playing_ranch = False self.pending_action: PendingAction = None - self.available_characters = [] + self.available_characters: List[chars.Character] = [] self.was_shot = False self.on_pick_cb = None self.on_failed_response_cb = None @@ -70,10 +73,15 @@ class Player: self.death_turn = 0 self.noStar = False self.can_play_vendetta = True + self.can_play_again_don_bell = True self.is_giving_life = False self.choose_text = 'choose_card_to_get' self.using_rimbalzo = 0 # 0 no, 1 scegli giocatore, 2 scegli carta self.bang_used = 0 + self.gold_nuggets = 0 + self.gold_rush_equipment: List[grc.ShopCard] = [] + self.was_player = False + self.setaccio_count = 0 def join_game(self, game): self.game = game @@ -91,7 +99,11 @@ class Player: def set_character(self, character: str): print(self.available_characters, character) if self.character == None: - self.character = next(x for x in self.available_characters if x.name == character) + try: + self.character = next(x for x in self.available_characters if x.name == character) + except: + # fix for wrong character encoding in the first part of some characters like Jose delgrado + self.character = next(x for x in self.available_characters if x.name.split()[1] == character.split()[1]) if 'high_noon' in self.game.expansions: # questo viene utilizzato per la carta nuova identità self.not_chosen_character = next(x for x in self.available_characters if x.name != character) @@ -128,7 +140,9 @@ class Player: self.sio.emit('characters', room=self.sid, data=json.dumps( available, default=lambda o: o.__dict__)) else: - self.set_character(available[randrange(0, len(available))].name) + char_name = available[randrange(0, len(available))].name + self.game.rpc_log.append(f'{self.name};set_character;{char_name}') + self.set_character(char_name) def notify_card(self, player, card, message=''): try: @@ -169,13 +183,21 @@ class Player: self.hand.append(self.game.deck.draw(True)) if self.lives <= 0 and self.max_lives > 0 and not self.is_dead: print('dying, attacker', self.attacker) - if self.character.check(self.game, chars.SidKetchum) and len(self.hand) > 1: + if self.gold_nuggets >= 2 and len([c for c in self.gold_rush_equipment if isinstance(c, grc.Zaino)]) > 0: + for i in range(len(self.gold_rush_equipment)): + if isinstance(self.gold_rush_equipment[i], grc.Zaino): + self.gold_rush_equipment[i].play_card(self, None) + return # play card will notify the player + if self.character.check(self.game, chars.SidKetchum) and len(self.hand) > 1 and self.lives == 0: + if self.game.players[self.game.turn] != self: + self.game.players[self.game.turn].pending_action = PendingAction.WAIT + self.game.players[self.game.turn].notify_self() + self.scrapped_cards = 0 + self.previous_pending_action = self.pending_action + self.pending_action = PendingAction.CHOOSE + self.choose_text = 'choose_sid_scrap' + self.available_cards = self.hand self.lives += 1 - #TODO Sid dovrebbe poter decidere cosa scartare - self.game.deck.scrap(self.hand.pop( - randrange(0, len(self.hand))), True) - self.game.deck.scrap(self.hand.pop( - randrange(0, len(self.hand))), True) ser = self.__dict__.copy() ser.pop('game') ser.pop('sio') @@ -186,6 +208,9 @@ class Player: if self.attacker: ser['attacker'] = self.attacker.name ser['sight'] = self.get_sight() + ser['can_gold_rush_discard'] = len([p for p in self.game.get_alive_players() if p != self and len([e for e in p.gold_rush_equipment if e.number <= self.gold_nuggets + 1]) > 0]) > 0 + if self.character: + ser['gold_rush_discount'] = 1 if self.character.check(self.game, grch.PrettyLuzena) and self.special_use_count < 1 else 0 ser['lives'] = max(ser['lives'], 0) if self.lives <= 0 and self.max_lives > 0 and not self.is_dead: @@ -215,32 +240,49 @@ class Player: else: return if self.pending_action == PendingAction.PICK: + self.game.rpc_log.append(f'{self.name};pick;') self.pick() elif self.pending_action == PendingAction.DRAW: + self.game.rpc_log.append(f'{self.name};draw;') self.draw('') elif self.pending_action == PendingAction.PLAY: non_blocked_cards = [card for card in self.hand if (not self.game.check_event(ceh.Manette) or card.suit == self.committed_suit_manette)] - equippables = [c for c in non_blocked_cards if (c.is_equipment or c.usable_next_turn) and not isinstance(c, cs.Prigione) and not any([type(c) == type(x) for x in self.equipment])] - misc = [c for c in non_blocked_cards if (isinstance(c, cs.WellsFargo) or isinstance(c, cs.Indiani) or isinstance(c, cs.Gatling) or isinstance(c, cs.Diligenza) or isinstance(c, cs.Emporio) or (isinstance(c, cs.Birra) and self.lives < self.max_lives and not self.game.check_event(ceh.IlReverendo)) or (c.need_with and len(self.hand) > 1 and not c.need_target and not (isinstance(c, csd.Whisky) and self.lives == self.max_lives))) - and not (not c.can_be_used_now and self.game.check_event(ce.IlGiudice))] - need_target = [c for c in non_blocked_cards if c.need_target and c.can_be_used_now and not (c.need_with and len(self.hand) < 2) and not ( - (self.game.check_event(ceh.Sermone) or self.has_played_bang and not (any([isinstance(c, cs.Volcanic) for c in self.equipment]) and type(c) == type(cs.Bang) - ) and not self.game.check_event(ce.Lazo))) and not ( isinstance(c, cs.Prigione) and self.game.check_event(ce.IlGiudice))] + equippables = [c for c in non_blocked_cards if (c.is_equipment or c.usable_next_turn) and not isinstance(c, cs.Prigione) and not any([type(c) == type(x) and not (c.is_weapon and c.must_be_used) for x in self.equipment])] + misc = [c for c in non_blocked_cards if not c.need_target and (isinstance(c, cs.WellsFargo) or isinstance(c, cs.Indiani) or isinstance(c, cs.Gatling) or isinstance(c, cs.Diligenza) or isinstance(c, cs.Emporio) or ((isinstance(c, cs.Birra) and self.lives < self.max_lives or c.must_be_used) and not self.game.check_event(ceh.IlReverendo)) or (c.need_with and len(self.hand) > 1 and not (isinstance(c, csd.Whisky) and self.lives == self.max_lives))) + and not (not c.can_be_used_now and self.game.check_event(ce.IlGiudice)) and not c.is_equipment] + need_target = [c for c in non_blocked_cards if c.need_target and c.can_be_used_now and not (c.need_with and len(self.hand) < 2) and not (type(c) == type(cs.Bang) and (self.game.check_event(ceh.Sermone) or (self.has_played_bang and (not any([isinstance(c, cs.Volcanic) for c in self.equipment]) or self.game.check_event(ce.Lazo))))) and not (isinstance(c, cs.Prigione) and self.game.check_event(ce.IlGiudice)) or isinstance(c, cs.Duello) or isinstance(c, cs.CatBalou) or isinstance(c, csd.Pugno)] green_cards = [c for c in self.equipment if not self.game.check_event(ce.Lazo) and not isinstance(c, cs.Mancato) and c.usable_next_turn and c.can_be_used_now] + if self.game.debug: + print(f'hand: {self.hand}') + print(f'non_blocked: {non_blocked_cards}') + print(f'equippables: {equippables}') + print(f'misc: {misc}') + print(f'need_target: {need_target}') + print(f'green_cards: {green_cards}') + if self.gold_nuggets > 0 and any([c.number <= self.gold_nuggets for c in self.game.deck.shop_cards]): + for i in range(len(self.game.deck.shop_cards)): + if self.game.deck.shop_cards[i].number <= self.gold_nuggets: + self.game.rpc_log.append(f'{self.name};buy_gold_rush_card;{i}') + self.buy_gold_rush_card(i) + return if len(equippables) > 0 and not self.game.check_event(ce.IlGiudice): for c in equippables: if self.play_card(self.hand.index(c)): return elif len(misc) > 0: for c in misc: - if c.need_with and self.play_card(self.hand.index(c), _with=sample([j for j in range(len(self.hand)) if j != self.hand.index(c)], 1)[0]): + if c.need_with and len(self.hand) > 1 and self.play_card(self.hand.index(c), _with=sample([j for j in range(len(self.hand)) if j != self.hand.index(c)], 1)[0]): return elif self.play_card(self.hand.index(c)): return elif len(need_target) > 0: for c in need_target: _range = self.get_sight() if c.name == 'Bang!' or c.name == "Pepperbox" else c.range - others = [p for p in self.game.get_visible_players(self) if _range >= p['dist'] and not (isinstance(self.role, r.Vice) and p['is_sheriff']) and p['lives'] > 0 and not ((isinstance(c, cs.CatBalou) or isinstance(c, cs.Panico)) and p['cards'] == 0) and not (p['is_sheriff'] and isinstance(c, cs.Prigione))] + others = [p for p in self.game.get_visible_players(self) if _range >= p['dist'] and not (isinstance(self.role, r.Vice) and p['is_sheriff'] and not c.must_be_used) and p['lives'] > 0 and not + ((isinstance(c, cs.CatBalou) or isinstance(c, cs.Panico)) and p['cards'] == 0) + and not (p['is_sheriff'] and isinstance(c, cs.Prigione))] + if (isinstance(c, cs.Panico) or isinstance(c, cs.Panico))and len(self.equipment) > 0: + others.append({'name': self.name, 'is_sheriff': isinstance(self.role, r.Sheriff)}) if len(others) == 0 or c not in self.hand: continue target = others[randrange(0, len(others))] @@ -249,7 +291,7 @@ class Player: if not c.need_with: if self.play_card(self.hand.index(c), against=target['name']): return - else: + elif len(self.hand) > 1: if self.play_card(self.hand.index(c), against=target['name'], _with=sample([j for j in range(len(self.hand)) if j != self.hand.index(c)], 1)[0]): return elif len(green_cards) > 0: @@ -270,42 +312,60 @@ class Player: return break maxcards = self.lives if not self.character.check(self.game, chd.SeanMallory) else 10 + if maxcards == self.lives and len([c for c in self.gold_rush_equipment if isinstance(c, grc.Cinturone)]) > 0: + maxcards = 8 if len(self.hand) > maxcards: + self.game.rpc_log.append(f'{self.name};scrap;{0}') self.scrap(0) else: + self.game.rpc_log.append(f'{self.name};end_turn') self.end_turn() elif self.pending_action == PendingAction.RESPOND: did_respond = False for i in range(len(self.hand)): if self.hand[i].can_be_used_now and (self.hand[i].name in self.expected_response or self.character.check(self.game, chd.ElenaFuente)): + self.game.rpc_log.append(f'{self.name};respond;{i}') self.respond(i) did_respond = True break for i in range(len(self.equipment)): if not self.game.check_event(ce.Lazo) and self.equipment[i].name in self.expected_response: + self.game.rpc_log.append(f'{self.name};respond;{len(self.hand)+i}') self.respond(len(self.hand)+i) did_respond = True break if not did_respond: + self.game.rpc_log.append(f'{self.name};respond;{-1}') self.respond(-1) elif self.pending_action == PendingAction.CHOOSE: if not self.target_p: - self.choose(randrange(0, len(self.available_cards))) + card_index = randrange(0, len(self.available_cards)) + self.game.rpc_log.append(f'{self.name};choose;{card_index}') + self.choose(card_index) else: target = self.game.get_player_named(self.target_p) if len(target.hand)+len(target.equipment) == 0: self.pending_action = PendingAction.PLAY self.notify_self() else: - self.choose(randrange(0, len(target.hand)+len(target.equipment))) + try: + card_index = randrange(0, len(target.hand)+len(target.equipment)) + self.game.rpc_log.append(f'{self.name};choose;{card_index}') + self.choose(card_index) + except: + self.game.rpc_log.append(f'{self.name};choose;{0}') + self.choose(0) - def play_turn(self, can_play_vendetta = True, again = False): + + def play_turn(self, can_play_vendetta = True, again = False, can_play_again_don_bell=True): if (self.lives == 0 or self.is_dead) and not self.is_ghost: return self.end_turn(forced=True) self.scrapped_cards = 0 + self.setaccio_count = 0 self.can_play_ranch = True self.is_playing_ranch = False self.can_play_vendetta = can_play_vendetta + self.can_play_again_don_bell = can_play_again_don_bell if not again: self.sio.emit('chat_message', room=self.game.name, data=f'_turn|{self.name}') @@ -319,6 +379,12 @@ class Player: if self.game.check_event(ceh.MezzogiornoDiFuoco): self.attacker = None self.lives -= 1 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + self.gold_nuggets += 1 + if self.character.check(self.game, grch.SimeonPicos): + self.gold_nuggets += 1 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + self.hand.append(self.game.deck.draw(True)) if self.character.check(self.game, chars.BartCassidy) and self.lives > 0: self.hand.append(self.game.deck.draw(True)) self.sio.emit('chat_message', room=self.game.name, data=f'_special_bart_cassidy|{self.name}') @@ -332,6 +398,7 @@ class Player: 'name': p.name, 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', 'alt_text': ''.join(['❤️']*p.lives)+''.join(['💀']*(p.max_lives-p.lives)), + 'is_character': True, 'noDesc': True } for p in self.game.get_alive_players() if p != self and p.lives < p.max_lives] self.available_cards.append({'icon': '❌', 'noDesc': True}) @@ -360,6 +427,7 @@ class Player: 'name': p['name'], 'icon': p['role'].icon if(self.game.initial_players == 3) else '⭐️' if p['is_sheriff'] else '🤠', 'alt_text': ''.join(['❤️']*p['lives'])+''.join(['💀']*(p['max_lives']-p['lives'])), + 'is_character': True, 'desc': p['name'] } for p in self.game.get_visible_players(self) if p['dist'] <= self.get_sight()] self.available_cards.append({'icon': '❌', 'noDesc': True}) @@ -370,6 +438,7 @@ class Player: self.available_cards = [{ 'name': p.name, 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', + 'is_character': True, 'noDesc': True } for p in self.game.get_alive_players() if len(p.equipment) > 0 and p != self] self.available_cards.append({'icon': '❌', 'noDesc': True}) @@ -389,6 +458,12 @@ class Player: self.choose_text = 'choose_card_to_get' self.pending_action = PendingAction.CHOOSE self.notify_self() + elif self.character.check(self.game, grch.DutchWill): + self.is_drawing = True + self.available_cards = [self.game.deck.draw() for i in range(2)] + self.choose_text = 'choose_card_to_get' + self.pending_action = PendingAction.CHOOSE + self.notify_self() elif self.character.check(self.game, chd.PatBrennan) and type(pile) == str and pile != self.name and pile in self.game.players_map and len(self.game.get_player_named(pile).equipment) > 0: self.is_drawing = True self.available_cards = self.game.get_player_named(pile).equipment @@ -398,45 +473,34 @@ class Player: self.notify_self() else: self.pending_action = PendingAction.PLAY - if pile == 'scrap' and self.character.check(self.game, chars.PedroRamirez): - self.hand.append(self.game.deck.draw_from_scrap_pile()) - if not self.game.check_event(ceh.Sete): - self.hand.append(self.game.deck.draw()) - self.sio.emit('chat_message', room=self.game.name, - data=f'_draw_from_scrap|{self.name}') - elif type(pile) == str and pile != self.name and pile in self.game.players_map and self.character.check(self.game, chars.JesseJones) and len(self.game.get_player_named(pile).hand) > 0: - self.hand.append(self.game.get_player_named(pile).hand.pop( - randrange(0, len(self.game.get_player_named(pile).hand)))) - self.game.get_player_named(pile).notify_self() - self.sio.emit('chat_message', room=self.game.name, - data=f'_draw_from_player|{self.name}|{pile}') - if not self.game.check_event(ceh.Sete): - self.hand.append(self.game.deck.draw()) - elif self.character.check(self.game, chd.BillNoface): - self.hand.append(self.game.deck.draw()) - if not self.game.check_event(ceh.Sete): - for i in range(self.max_lives-self.lives): - self.hand.append(self.game.deck.draw()) - else: - if self.character.check(self.game, chd.PixiePete): - self.hand.append(self.game.deck.draw()) - for i in range(2): + num = 2 if not self.character.check(self.game, chd.BillNoface) else self.max_lives-self.lives+1 + if self.character.check(self.game, chd.PixiePete): num += 1 + if (len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0): num += 1 + if self.game.check_event(ceh.IlTreno) or (self.is_ghost and self.game.check_event(ceh.CittaFantasma)): num += 1 + elif self.game.check_event(ceh.Sete): num -= 1 + for i in range(num): + if i == 0 and pile == 'scrap' and self.character.check(self.game, chars.PedroRamirez): + self.hand.append(self.game.deck.draw_from_scrap_pile()) + self.sio.emit('chat_message', room=self.game.name, data=f'_draw_from_scrap|{self.name}') + elif i == 0 and type(pile) == str and pile != self.name and pile in self.game.players_map and self.character.check(self.game, chars.JesseJones) and len(self.game.get_player_named(pile).hand) > 0: + self.hand.append(self.game.get_player_named(pile).hand.pop( randrange(0, len(self.game.get_player_named(pile).hand)))) + self.game.get_player_named(pile).notify_self() + self.sio.emit('chat_message', room=self.game.name, data=f'_draw_from_player|{self.name}|{pile}') + elif i == 1: card: cs.Card = self.game.deck.draw() - self.hand.append(card) - if i == 1 and (self.character.check(self.game, chars.BlackJack) or self.game.check_event(ce.LeggeDelWest)): + if (self.character.check(self.game, chars.BlackJack) or self.game.check_event(ce.LeggeDelWest)): for p in self.game.get_alive_players(): if p != self: p.notify_card(self, card, 'blackjack_special' if self.character.check(self.game, chars.BlackJack) else 'foc.leggedelwest') if self.game.check_event(ce.LeggeDelWest): card.must_be_used = True - if card.check_suit(self.game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]) and self.character.check(self.game, chars.BlackJack): + if self.character.check(self.game, chars.BlackJack) and card.check_suit(self.game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): self.hand.append(self.game.deck.draw()) - if self.game.check_event(ceh.Sete): - return self.notify_self() - if self.game.check_event(ceh.IlTreno) or (self.is_ghost and self.game.check_event(ceh.CittaFantasma)): + self.hand.append(card) + else: self.hand.append(self.game.deck.draw()) self.manette() - self.notify_self() + self.notify_self() def manette(self): if self.game.check_event(ceh.Manette): @@ -453,6 +517,8 @@ class Player: if self.pending_action != PendingAction.PICK: return pickable_cards = 1 + self.character.pick_mod + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.FerroDiCavallo)]) > 0: + pickable_cards += 1 if self.is_my_turn: for i in range(len(self.equipment)): if i < len(self.equipment) and isinstance(self.equipment[i], cs.Dinamite): @@ -464,6 +530,14 @@ class Player: data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') if picked.check_suit(self.game, [cs.Suit.SPADES]) and 2 <= picked.number <= 9 and pickable_cards == 0: self.lives -= 3 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + self.gold_nuggets += 3 + if self.character.check(self.game, grch.SimeonPicos): + self.gold_nuggets += 3 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + self.hand.append(self.game.deck.draw()) + self.hand.append(self.game.deck.draw()) + self.hand.append(self.game.deck.draw()) self.attacker = None self.game.deck.scrap(self.equipment.pop(i), True) self.sio.emit('chat_message', room=self.game.name, data=f'_explode|{self.name}') @@ -524,10 +598,24 @@ class Player: return playable_cards def play_card(self, hand_index: int, against=None, _with=None): + if self.is_bot: + data = { + "index": hand_index, + "against": against, + "with": _with + } + self.game.rpc_log.append(f'{self.name};play_card;{json.dumps(data)}') + print(self.name, 'wants to play card ', hand_index, ' against:', against, ' with:', _with) if not self.is_my_turn or self.pending_action != PendingAction.PLAY or self.game.is_handling_death: + print('but cannot') return - if not (0 <= hand_index < len(self.hand) + len(self.equipment)): + if not (0 <= hand_index < len(self.hand) + len(self.equipment) + len(self.gold_rush_equipment)): + print('but the card index is out of range') return + elif len(self.hand) + len(self.equipment) <= hand_index < len(self.hand) + len(self.equipment) + len(self.gold_rush_equipment) and len(self.gold_rush_equipment): + print('which is a gold rush black card') + card: grc.ShopCard = self.gold_rush_equipment[hand_index - len(self.hand) - len(self.equipment)] + return card.play_card(self) card: cs.Card = self.hand.pop(hand_index) if hand_index < len(self.hand) else self.equipment.pop(hand_index-len(self.hand)) withCard: cs.Card = None if _with != None: @@ -535,19 +623,20 @@ class Player: print(self.name, 'is playing ', card, ' against:', against, ' with:', _with) did_play_card = False event_blocks_card = (self.game.check_event(ce.IlGiudice) and (card.is_equipment or (card.usable_next_turn and not card.can_be_used_now))) or (self.game.check_event(ce.Lazo) and card.usable_next_turn and card.can_be_used_now) or (self.game.check_event(ceh.Manette) and card.suit != self.committed_suit_manette and not (card.usable_next_turn and card.can_be_used_now)) - if not(against != None and isinstance(self.game.get_player_named(against).character, chd.ApacheKid) and card.check_suit(self.game, [cs.Suit.DIAMONDS])) and not event_blocks_card: - if against == self.name and not isinstance(card, csd.Tequila): + if not(against != None and (self.game.get_player_named(against).character.check(self.game, chd.ApacheKid) or len([c for c in self.game.get_player_named(against).gold_rush_equipment if isinstance(c, grc.Calumet)]) > 0) and card.check_suit(self.game, [cs.Suit.DIAMONDS])) or (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) and not event_blocks_card: + if against == self.name and not isinstance(card, csd.Tequila) and not isinstance(card, cs.Panico) and not isinstance(card, cs.CatBalou): did_play_card = False else: did_play_card = card.play_card(self, against, withCard) - if not card.is_equipment and not card.usable_next_turn or event_blocks_card: + if not card.is_equipment and not card.usable_next_turn and not (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) or event_blocks_card: if did_play_card: self.game.deck.scrap(card, True) else: self.hand.insert(hand_index, card) if withCard: self.hand.insert(_with, withCard) - elif card.usable_next_turn and card.can_be_used_now: + self.sio.emit('cant_play_card', room=self.sid) + elif (card.usable_next_turn and card.can_be_used_now) or (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK): if did_play_card: self.game.deck.scrap(card, True) else: @@ -568,7 +657,9 @@ class Player: if self.target_p and self.target_p != '': # panico, cat balou target = self.game.get_player_named(self.target_p) card = None - if card_index >= len(target.hand): + if (target.name == self.name): + card = self.equipment.pop(card_index) + elif card_index >= len(target.hand): card = target.equipment.pop(card_index - len(target.hand)) else: card = target.hand.pop(card_index) @@ -588,6 +679,74 @@ class Player: while self.target_p == self.name or len(self.game.players[self.game.players_map[self.target_p]].hand) + len(self.game.players[self.game.players_map[self.target_p]].equipment) == 0: self.target_p = self.game.players[(self.game.players_map[self.target_p]+1)%len(self.game.players)].name self.notify_self() + elif self.choose_text == 'choose_ricercato': + player = self.game.get_player_named(self.available_cards[card_index]['name']) + player.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{self.name}|Ricercato|{player.name}') + if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Ricercato)]) > 0: + self.game.deck.shop_deck.append(grc.Ricercato()) + else: + player.gold_rush_equipment.append(grc.Ricercato()) + player.notify_self() + self.pending_action = PendingAction.PLAY + self.notify_self() + elif self.choose_text == 'choose_sid_scrap': + self.scrapped_cards += 1 + self.game.deck.scrap(self.hand.pop(card_index), True) + if self.scrapped_cards == 2: + self.available_cards = [] + self.pending_action = self.previous_pending_action + if self.game.players[self.game.turn] != self: + self.game.players[self.game.turn].pending_action = PendingAction.PLAY + self.game.players[self.game.turn].notify_self() + self.notify_self() + elif self.choose_text == 'choose_bicchierino': + player = self.game.get_player_named(self.available_cards[card_index]['name']) + self.sio.emit('chat_message', room=self.game.name, data=f'_play_card_for|{self.name}|{"Bicchierino"}|{player.name}') + player.lives = min(player.lives+1, player.max_lives) + self.pending_action = PendingAction.PLAY + self.notify_self() + elif self.choose_text == 'choose_birra_function': + if card_index == 0: + self.gold_nuggets += 1 + self.sio.emit('chat_message', room=self.game.name, data=f'_get_nugget|{self.name}') + else: + cs.Birra(1,1).play_card(self, skipChecks=True) + self.pending_action = PendingAction.PLAY + self.notify_self() + elif self.choose_text == 'choose_bottiglia': + self.sio.emit('chat_message', room=self.game.name, data=f'_play_card|{self.name}|{"Bottiglia"}') + if isinstance(self.available_cards[card_index], cs.Birra): + self.lives = min(self.lives+1, self.max_lives) + else: + self.hand.append(self.available_cards[card_index]) + self.pending_action = PendingAction.PLAY + self.notify_self() + elif self.choose_text == 'choose_complice': + self.sio.emit('chat_message', room=self.game.name, data=f'_play_card|{self.name}|{"Bottiglia"}') + self.hand.append(self.available_cards[card_index]) + self.pending_action = PendingAction.PLAY + self.notify_self() + elif self.choose_text == 'gold_rush_discard': + if card_index == len(self.available_cards) - 1: + self.pending_action = PendingAction.PLAY + else: + player = self.game.get_player_named(self.available_cards[card_index]['name']) + self.available_cards = [c for c in player.gold_rush_equipment if c.number+1 <= self.gold_nuggets] + self.available_cards.append({'icon': '❌', 'noDesc': True}) + self.choose_text = 'gold_rush_discard_2|' + player.name + self.notify_self() + elif 'gold_rush_discard_2' in self.choose_text: + if card_index == len(self.available_cards) - 1: + self.pending_action = PendingAction.PLAY + else: + self.gold_nuggets -= self.available_cards[card_index].number + 1 + player = self.game.get_player_named(self.choose_text.split('|')[1]) + player.gold_rush_equipment.remove(self.available_cards[card_index]) + self.game.deck.shop_deck.append(self.available_cards[card_index]) + self.sio.emit('chat_message', room=self.game.name, data=f'_gold_rush_pay_discard|{self.name}|{player.name}|{self.available_cards[card_index].name}') + player.notify_self() + self.pending_action = PendingAction.PLAY + self.notify_self() elif self.game.check_event(ceh.NuovaIdentita) and self.choose_text == 'choose_nuova_identita': if card_index == 1: # the other character self.character = self.not_chosen_character @@ -606,6 +765,12 @@ class Player: player = self.game.get_player_named(self.available_cards[card_index]['name']) player.lives += 1 self.lives -= 1 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + self.gold_nuggets += 1 + if self.character.check(self.game, grch.SimeonPicos): + self.gold_nuggets += 1 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + self.hand.append(self.game.deck.draw()) player.notify_self() self.sio.emit('chat_message', room=self.game.name, data=f'_fratelli_sangue|{self.name}|{player.name}') except: pass @@ -677,19 +842,47 @@ class Player: self.notify_self() # specifico per personaggio elif self.is_drawing and self.character.check(self.game, chars.KitCarlson): - self.hand.append(self.available_cards.pop(card_index)) + card: cs.Card = self.available_cards.pop(card_index) + if len(self.available_cards) == 1: #ho pescato la seconda carta + if self.game.check_event(ce.LeggeDelWest): + card.must_be_used = True + self.hand.append(card) pickable_stop = 1 - if self.game.check_event(ceh.Sete): pickable_stop = 2 - if self.game.check_event(ceh.IlTreno): pickable_stop = 0 + if self.game.check_event(ceh.Sete): pickable_stop += 1 + if self.game.check_event(ceh.IlTreno) or len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0: + pickable_stop -= 1 if len(self.available_cards) == pickable_stop: - if len(self.available_cards) > 0: + if len(self.available_cards) > 0: #la carta non scelta la rimettiamo in cima al mazzo self.game.deck.put_on_top(self.available_cards.pop()) + if len(self.available_cards) > 0: #se sono rimaste carte le scartiamo + self.game.deck.scrap(self.available_cards.pop()) + #se c'è sia treno che piccone pesco un'altra carta + if self.game.check_event(ceh.IlTreno) and len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0: + self.hand.append(self.game.deck.draw()) self.is_drawing = False self.pending_action = PendingAction.PLAY self.manette() self.notify_self() # specifico per personaggio + elif self.is_drawing and self.character.check(self.game, grch.DutchWill): + if not self.game.check_event(ceh.Sete): + self.hand.append(self.available_cards.pop(card_index)) #prendo la carta scelta + else: + self.game.deck.scrap(self.available_cards.pop(0), True) #non pesco carte + self.game.deck.scrap(self.available_cards.pop(0), True) #scarto l'altra + #legge del west non si applica perchè la seconda carta viene scartata + if self.game.check_event(ceh.IlTreno): + self.hand.append(self.game.deck.draw()) + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0: + self.hand.append(self.game.deck.draw()) + self.gold_nuggets += 1 + self.is_drawing = False + self.pending_action = PendingAction.PLAY + self.manette() + self.notify_self() + # specifico per personaggio elif self.is_drawing and self.character.check(self.game, chd.PatBrennan): + #non pesca per niente dal mazzo self.is_drawing = False card = self.available_cards.pop(card_index) card.reset_card() @@ -704,6 +897,8 @@ class Player: def barrel_pick(self): pickable_cards = 1 + self.character.pick_mod + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.FerroDiCavallo)]) > 0: + pickable_cards += 1 if len([c for c in self.equipment if isinstance(c, cs.Barile)]) > 0 and self.character.check(self.game, chars.Jourdonnais): pickable_cards = 2 while pickable_cards > 0: @@ -734,6 +929,8 @@ class Player: pickable_cards = 1 + self.character.pick_mod if len([c for c in self.equipment if isinstance(c, cs.Barile)]) > 0 and self.character.check(self.game, chars.Jourdonnais): pickable_cards = 2 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.FerroDiCavallo)]) > 0: + pickable_cards += 1 while pickable_cards > 0: pickable_cards -= 1 picked: cs.Card = self.game.deck.pick_and_scrap() @@ -812,7 +1009,7 @@ class Player: def get_indians(self, attacker): self.attacker = attacker - if self.character.check(self.game, chd.ApacheKid): return False + if self.character.check(self.game, chd.ApacheKid) or len([c for c in self.gold_rush_equipment if isinstance(c, grc.Calumet)]) > 0: return False if not self.game.is_competitive and len([c for c in self.hand if isinstance(c, cs.Bang) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato))]) == 0: print('Cant defend') self.take_damage_response() @@ -850,6 +1047,7 @@ class Player: if self.character.check(self.game, chd.MollyStark) and not self.is_my_turn: self.hand.append(self.game.deck.draw(True)) self.lives += 1 if not self.character.check(self.game, chd.TequilaJoe) else 2 + self.lives = min(self.lives, self.max_lives) self.game.deck.scrap(self.hand.pop(i), True) self.sio.emit('chat_message', room=self.game.name, data=f'_beer_save|{self.name}') @@ -868,6 +1066,16 @@ class Player: self.sio.emit('chat_message', room=self.game.name, data=f'_special_el_gringo|{self.name}|{self.attacker.name}') self.attacker.notify_self() + if self.attacker and 'gold_rush' in self.game.expansions: + if (isinstance(self.attacker, Player)): + self.attacker.gold_nuggets += 1 + self.attacker.notify_self() + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + self.gold_nuggets += 1 + if self.character.check(self.game, grch.SimeonPicos): + self.gold_nuggets += 1 + if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + self.hand.append(self.game.deck.draw(True)) self.heal_if_needed() self.mancato_needed = 0 self.expected_response = [] @@ -894,11 +1102,12 @@ class Player: card = self.hand.pop(hand_index) if hand_index < len(self.hand) else self.equipment.pop(hand_index-len(self.hand)) #hand_index < len(self.hand) with the '<=' due to the hand.pop if self.character.check(self.game, chd.MollyStark) and hand_index <= len(self.hand) and not self.is_my_turn and self.event_type != 'duel': - if self.attacker.character.check(self.game, chars.SlabTheKiller) and isinstance(self.hand[hand_index], cs.Mancato): + if hasattr(self.attacker,'character') and self.attacker.character.check(self.game, chars.SlabTheKiller) and isinstance(self.hand[hand_index], cs.Mancato): self.molly_discarded_cards += 1 else: self.hand.append(self.game.deck.draw(True)) card.use_card(self) + print(f'{self.game.name}: {self.name} responded with {card.name}') self.sio.emit('chat_message', room=self.game.name, data=f'_respond|{self.name}|{card.name}') self.game.deck.scrap(card, True) self.notify_self() @@ -936,9 +1145,11 @@ class Player: if self.mancato_needed <= 0: self.attacker = None - def get_sight(self, countWeapon=True): + def get_sight(self, countWeapon=True): #come vedo io gli altri if not self.character: return 0 + if self.game.check_event(ce.Lazo): + return 1 + self.character.sight_mod aim = 0 range = 0 for card in self.equipment: @@ -946,19 +1157,18 @@ class Player: range += card.range else: aim += card.sight_mod - if self.game.check_event(ce.Lazo): - return 1 + self.character.sight_mod - return max(1, range) + aim + self.character.sight_mod + return max(1,range) + aim + (self.character.sight_mod if not self.game.check_event(ceh.Sbornia) else 0) - def get_visibility(self): + def get_visibility(self): #come mi vedono gli altri if not self.character or not self.game or not self.game.players[self.game.turn].character: return 0 covers = 0 + ch_vis_mod = self.character.visibility_mod if not self.game.check_event(ceh.Sbornia) else 0 if self.game.check_event(ce.Lazo) or self.game.players[self.game.turn].character.check(self.game, chd.BelleStar): - return self.character.visibility_mod + return ch_vis_mod for card in self.equipment: covers += card.vis_mod - return self.character.visibility_mod + covers + return ch_vis_mod + covers def scrap(self, card_index): if self.is_my_turn or self.character.check(self.game, chars.SidKetchum): @@ -974,51 +1184,105 @@ class Player: self.game.deck.scrap(card) self.notify_self() - def holyday_special(self, data): - if self.character.check(self.game, chd.DocHolyday) and self.special_use_count < 1 and self.pending_action == PendingAction.PLAY: - self.special_use_count += 1 - cards = sorted(data['cards'], reverse=True) - for c in cards: - self.game.deck.scrap(self.hand.pop(c), True) - self.notify_self() - self.game.attack(self, data['against']) + def special(self, data): + self.character.special(self, data) - def chuck_lose_hp_draw(self): - if self.character.check(self.game, chd.ChuckWengam) and self.lives > 1 and self.is_my_turn: - self.lives -= 1 - self.hand.append(self.game.deck.draw(True)) - self.hand.append(self.game.deck.draw(True)) + def gold_rush_discard(self): + self.available_cards = [{ + 'name': p.name, + 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', + 'is_character': True, + 'alt_text': ''.join(['🎴️'] * len(p.gold_rush_equipment)), + 'noDesc': True + } for p in self.game.get_alive_players() if p != self and len([e for e in p.gold_rush_equipment if e.number + 1 <= self.gold_nuggets]) > 0] + self.available_cards.append({'icon': '❌', 'noDesc': True}) + self.choose_text = 'gold_rush_discard' + self.pending_action = PendingAction.CHOOSE + self.notify_self() + + def buy_gold_rush_card(self, index): + print(f'{self.name} wants to buy gr-card index {index} in room {self.game.name}') + card: cs.Card = self.game.deck.shop_cards[index] + discount = 0 + if self.character.check(self.game, grch.PrettyLuzena) and self.special_use_count < 1: + discount = 1 + if self.pending_action == PendingAction.PLAY and self.gold_nuggets >= card.number - discount: + self.gold_nuggets -= card.number - discount + if self.character.check(self.game, grch.PrettyLuzena) and self.special_use_count < 1: + self.special_use_count += 1 + if card.play_card(self): + self.game.deck.shop_deck.append(card) + self.game.deck.shop_cards[index] = None + self.game.deck.fill_gold_rush_shop() self.notify_self() + def check_can_end_turn(self): + must_be_used_cards = [c for c in self.hand if c.must_be_used] + if self.game.check_event(ce.LeggeDelWest) and len(must_be_used_cards) > 0: + card = must_be_used_cards[0] + print(f'Legge del west card: {card.name}') + print(self.has_played_bang and not (any([isinstance(c, cs.Volcanic) for c in self.equipment]) and type(card) == type(cs.Bang))) + if card.suit == cs.Suit.DIAMONDS and card.need_target and len([p for p in self.game.get_alive_players() if (not p.character.check(self.game, chd.ApacheKid) and not any([isinstance(c, grc.Calumet) for c in p.gold_rush_equipment]))]) == 0: + if isinstance(card, cs.Bang): + return True + else: + return len(self.equipment) == 0 # se non ho carte equipaggiamento + elif (isinstance(card, cs.Bang) or (isinstance(card, cs.Mancato) and self.character.check(self.game, chars.CalamityJanet))) and self.has_played_bang and not any([isinstance(c, cs.Volcanic) for c in self.equipment]) or len([p for p in self.game.get_visible_players(self) if self.get_sight() >= p['dist']]) == 0: + return True + elif isinstance(card, cs.Mancato) or (card.need_with and len(self.hand) < 2): + return True + elif isinstance(card, cs.Panico) and len([p for p in self.game.get_visible_players(self) if self.get_sight(False) >= p['dist']]) == 0 and len(self.equipment) == 0: + return True + elif isinstance(card, csd.Pugno) and len([p for p in self.game.get_visible_players(self) if self.get_sight(False) >= p['dist']]) == 0: + return True + elif isinstance(card, cs.Prigione) and len([p for p in self.game.get_visible_players(self) if not p['is_sheriff']]) == 0: + return True + elif not card.is_weapon and len([c for c in self.equipment if c.name == card.name]) > 0: + return True + return False + return True + def end_turn(self, forced=False): print(f"{self.name} wants to end his turn") if not self.is_my_turn and not forced: return maxcards = self.lives if not self.character.check(self.game, chd.SeanMallory) else 10 + if maxcards == self.lives and len([c for c in self.gold_rush_equipment if isinstance(c, grc.Cinturone)]) > 0: + maxcards = 8 if len(self.hand) > maxcards and not forced: - print( - f"{self.name}: I have to many cards in my hand and I can't end the turn") + print(f"{self.name}: I have to many cards in my hand and I can't end the turn") + elif not self.check_can_end_turn(): + print(f"{self.name}: I must play the legge del west card") elif self.pending_action == PendingAction.PLAY or forced: - if not forced and self.game.check_event(ce.Vendetta) and self.can_play_vendetta: - picked: cs.Card = self.game.deck.pick_and_scrap() - self.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') - if picked.check_suit(self.game, [cs.Suit.HEARTS]): - self.play_turn(can_play_vendetta=False) - return - self.is_my_turn = False - self.has_played_bang = False for i in range(len(self.equipment)): if self.equipment[i].usable_next_turn and not self.equipment[i].can_be_used_now: self.equipment[i].can_be_used_now = True for i in range(len(self.hand)): if self.hand[i].must_be_used: self.hand[i].must_be_used = False + self.has_played_bang = False + ##Vendetta## + if not forced and self.game.check_event(ce.Vendetta) and self.can_play_vendetta: + picked: cs.Card = self.game.deck.pick_and_scrap() + self.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + if picked.check_suit(self.game, [cs.Suit.HEARTS]): + self.play_turn(can_play_vendetta=False) + return + ##Don Bell## + if not forced and self.character.check(self.game, grch.DonBell) and self.can_play_again_don_bell: + picked: cs.Card = self.game.deck.pick_and_scrap() + self.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + if picked.check_suit(self.game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): + self.play_turn(can_play_vendetta=False, can_play_again_don_bell=False) + return + ##Ghost## if self.is_dead and self.is_ghost and self.game.check_event(ceh.CittaFantasma): self.is_ghost = False for i in range(len(self.hand)): self.game.deck.scrap(self.hand.pop(), True) for i in range(len(self.equipment)): self.game.deck.scrap(self.equipment.pop(), True) + self.is_my_turn = False self.committed_suit_manette = None self.pending_action = PendingAction.WAIT self.notify_self() diff --git a/backend/bang/roles.py b/backend/bang/roles.py index 66d8405..32eb94d 100644 --- a/backend/bang/roles.py +++ b/backend/bang/roles.py @@ -62,7 +62,9 @@ class Outlaw(Role): return True elif initial_players == 3 and attacker_role != None: return isinstance(dead_role, Vice) and isinstance(attacker_role, Outlaw) - elif initial_players != 3 and not any([isinstance(p.role, Sheriff) for p in alive_players]): + elif (initial_players != 3 and (not any([isinstance(p.role, Sheriff) for p in alive_players])) + and (any([isinstance(p.role, Outlaw) for p in alive_players]) + or any([isinstance(p.role, Renegade) for p in alive_players]) and len(alive_players) > 1)): print("The Outlaw won!") return True return False @@ -82,7 +84,7 @@ class Renegade(Role): return True elif initial_players == 3 and attacker_role != None: return isinstance(dead_role, Outlaw) and isinstance(attacker_role, Renegade) - elif initial_players != 3 and len(alive_players) == 1 and alive_players[0].role == self: + elif initial_players != 3 and len(alive_players) == 1 and isinstance(alive_players[0].role, Renegade): print("The Renegade won!") return True return False diff --git a/backend/requirements.txt b/backend/requirements.txt index 343b795..aaee48e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,7 +1,10 @@ -certifi==2021.5.30 -dnspython==1.16.0 -eventlet==0.31.0 -greenlet==1.1.0 +certifi==2021.10.8 +dnspython==2.2.1 +eventlet==0.33.0 +greenlet==1.1.2 python-engineio==3.14.2 python-socketio==4.6.1 six==1.16.0 +pytest==7.0.1 +requests==2.27.1 +discord-webhook==0.15.0 \ No newline at end of file diff --git a/backend/server.py b/backend/server.py new file mode 100644 index 0000000..a04a0c3 --- /dev/null +++ b/backend/server.py @@ -0,0 +1,584 @@ +import os +import json +import random +from typing import List +import eventlet +import socketio + +from bang.game import Game +from bang.players import Player, PendingAction + +import requests +from discord_webhook import DiscordWebhook + +import sys +sys.setrecursionlimit(10**6) # this should prevents bots from stopping + +sio = socketio.Server(cors_allowed_origins="*") + +static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'}, + '/game': {'content_type': 'text/html', 'filename': 'index.html'}, + '/help': {'content_type': 'text/html', 'filename': 'index.html'}, + '/status': {'content_type': 'text/html', 'filename': 'index.html'}, + # '/robots.txt': {'content_type': 'text/html', 'filename': 'robots.txt'}, + '/favicon.ico': {'filename': 'favicon.ico'}, + '/img/icons': './img/icons', + '/manifest.json': {'filename': 'manifest.json'}, + '/css': './css', + '/media': './media', + '/js': './js', + } +if "UseRobots" in os.environ and os.environ['UseRobots'].upper() == "TRUE": + static_files['/robots.txt'] = {'content_type': 'text/html', 'filename': 'robots.txt'} + +for file in [f for f in os.listdir('.') if '.js' in f or '.map' in f or '.html' in f]: + static_files[f'/{file}'] = f'./{file}' + +app = socketio.WSGIApp(sio, static_files=static_files) +games: List[Game] = [] +online_players = 0 +blacklist: List[str] = [] + +def advertise_lobbies(): + sio.emit('lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'locked': g.password != ''} for g in games if not g.started and len(g.players) < 10 and not g.is_hidden]) + sio.emit('spectate_lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'locked': g.password != ''} for g in games if g.started]) + +@sio.event +def connect(sid, environ): + global online_players + online_players += 1 + print('connect ', sid) + sio.enter_room(sid, 'lobby') + sio.emit('players', room='lobby', data=online_players) + +@sio.event +def get_online_players(sid): + global online_players + sio.emit('players', room='lobby', data=online_players) + +@sio.event +def set_username(sid, username): + ses = sio.get_session(sid) + if not isinstance(ses, Player): + sio.save_session(sid, Player(username, sid, sio)) + print(f'{sid} is now {username}') + advertise_lobbies() + elif ses.game == None or not ses.game.started: + print(f'{sid} changed username to {username}') + prev = ses.name + if len([p for p in ses.game.players if p.name == username]) > 0: + ses.name = f"{username}_{random.randint(0,100)}" + else: + ses.name = username + sio.emit('chat_message', room=ses.game.name, data=f'_change_username|{prev}|{ses.name}') + sio.emit('me', data=ses.name, room=sid) + ses.game.notify_room() + +@sio.event +def get_me(sid, room): + if isinstance(sio.get_session(sid), Player): + sio.emit('me', data=sio.get_session(sid).name, room=sid) + if sio.get_session(sid).game: + sio.get_session(sid).game.notify_room() + else: + sio.save_session(sid, Player('player', sid, sio)) + de_games = [g for g in games if g.name == room['name']] + if len(de_games) == 1 and not de_games[0].started: + join_room(sid, room) + elif len(de_games) == 1 and de_games[0].started: + print('room exists') + if room['username'] != None and any([p.name == room['username'] for p in de_games[0].players if p.is_bot]): + print('getting inside the bot') + bot = [p for p in de_games[0].players if p.is_bot and p.name == room['username'] ][0] + bot.sid = sid + bot.is_bot = False + sio.enter_room(sid, de_games[0].name) + sio.save_session(sid, bot) + de_games[0].notify_room(sid) + eventlet.sleep(0.1) + de_games[0].notify_all() + sio.emit('role', room=sid, data=json.dumps(bot.role, default=lambda o: o.__dict__)) + bot.notify_self() + if len(bot.available_characters) > 0: + bot.set_available_character(bot.available_characters) + else: #spectate + de_games[0].spectators.append(sio.get_session(sid)) + sio.get_session(sid).game = de_games[0] + sio.enter_room(sid, de_games[0].name) + de_games[0].notify_room(sid) + de_games[0].notify_event_card(sid) + de_games[0].notify_scrap_pile(sid) + de_games[0].notify_all() + de_games[0].notify_gold_rush_shop() + de_games[0].notify_event_card() + else: + create_room(sid, room['name']) + if sio.get_session(sid).game == None: + sio.emit('me', data={'error':'Wrong password/Cannot connect'}, room=sid) + else: + sio.emit('me', data=sio.get_session(sid).name, room=sid) + if room['username'] == None or any([p.name == room['username'] for p in sio.get_session(sid).game.players]): + sio.emit('change_username', room=sid) + else: + sio.emit('chat_message', room=sio.get_session(sid).game.name, data=f"_change_username|{sio.get_session(sid).name}|{room['username']}") + sio.get_session(sid).name = room['username'] + sio.emit('me', data=sio.get_session(sid).name, room=sid) + if not sio.get_session(sid).game.started: + sio.get_session(sid).game.notify_room() + +@sio.event +def disconnect(sid): + global online_players + online_players -= 1 + if sio.get_session(sid): + sio.emit('players', room='lobby', data=online_players) + if sio.get_session(sid).game and sio.get_session(sid).disconnect(): + sio.close_room(sio.get_session(sid).game.name) + games.pop(games.index(sio.get_session(sid).game)) + print('disconnect ', sid) + advertise_lobbies() + +@sio.event +def create_room(sid, room_name): + if sio.get_session(sid).game == None: + while len([g for g in games if g.name == room_name]): + room_name += f'_{random.randint(0,100)}' + sio.leave_room(sid, 'lobby') + sio.enter_room(sid, room_name) + g = Game(room_name, sio) + g.add_player(sio.get_session(sid)) + if room_name in blacklist: + g.is_hidden = True + games.append(g) + print(f'{sid} created a room named {room_name}') + advertise_lobbies() + +@sio.event +def private(sid): + g = sio.get_session(sid).game + g.set_private() + advertise_lobbies() + +@sio.event +def toggle_expansion(sid, expansion_name): + g = sio.get_session(sid).game + g.toggle_expansion(expansion_name) + +@sio.event +def toggle_comp(sid): + sio.get_session(sid).game.toggle_competitive() + +@sio.event +def toggle_replace_with_bot(sid): + sio.get_session(sid).game.toggle_disconnect_bot() + +@sio.event +def join_room(sid, room): + room_name = room['name'] + i = [g.name for g in games].index(room_name) + if games[i].password != '' and games[i].password != room['password'].upper(): + return + if not games[i].started: + print(f'{sid} joined a room named {room_name}') + sio.leave_room(sid, 'lobby') + sio.enter_room(sid, room_name) + while len([p for p in games[i].players if p.name == sio.get_session(sid).name]): + sio.get_session(sid).name += f'_{random.randint(0,100)}' + sio.emit('me', data=sio.get_session(sid).name, room=sid) + games[i].add_player(sio.get_session(sid)) + advertise_lobbies() + else: + games[i].spectators.append(sio.get_session(sid)) + sio.get_session(sid).game = games[i] + sio.get_session(sid).pending_action = PendingAction.WAIT + sio.enter_room(sid, games[0].name) + games[i].notify_room(sid) + eventlet.sleep(0.5) + games[i].notify_room(sid) + games[i].notify_all() + +""" +Sockets for the status page +""" + +@sio.event +def get_all_rooms(sid, deploy_key): + if 'DEPLOY_KEY' in os.environ and deploy_key == os.environ['DEPLOY_KEY']: + sio.emit('all_rooms', room=sid, data=[{ + 'name': g.name, + 'hidden': g.is_hidden, + 'players': [{'name':p.name, 'bot': p.is_bot, 'health': p.lives, 'sid': p.sid} for p in g.players], + 'password': g.password, + 'expansions': g.expansions, + 'started': g.started, + 'current_turn': g.turn, + 'incremental_turn': g.incremental_turn, + 'debug': g.debug, + 'spectators': len(g.spectators) + } for g in games]) + +@sio.event +def kick(sid, data): + if 'DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']: + sio.emit('kicked', room=data['sid']) + +@sio.event +def hide_toogle(sid, data): + if 'DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']: + game = [g for g in games if g.name==data['room']] + if len(games) > 0: + game[0].is_hidden = not game[0].is_hidden + if game[0].is_hidden: + if not data['room'] in blacklist: + blacklist.append(data['room']) + elif data['room'] in blacklist: + blacklist.remove(data['room']) + advertise_lobbies() + +""" +Sockets for the game +""" + +@sio.event +def start_game(sid): + ses: Player = sio.get_session(sid) + ses.game.start_game() + advertise_lobbies() + +@sio.event +def set_character(sid, name): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};set_character;{name}') + ses.set_character(name) + +@sio.event +def refresh(sid): + ses: Player = sio.get_session(sid) + ses.notify_self() + +@sio.event +def draw(sid, pile): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};draw;{pile}') + ses.draw(pile) + +@sio.event +def pick(sid): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};pick') + ses.pick() + +@sio.event +def end_turn(sid): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};end_turn') + ses.end_turn() + +@sio.event +def play_card(sid, data): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};play_card;{json.dumps(data)}') + ses.play_card(data['index'], data['against'], data['with']) + +@sio.event +def respond(sid, card_index): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};respond;{card_index}') + ses.respond(card_index) + +@sio.event +def choose(sid, card_index): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};choose;{card_index}') + ses.choose(card_index) + +@sio.event +def scrap(sid, card_index): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};scrap;{card_index}') + ses.scrap(card_index) + +@sio.event +def special(sid, data): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};play_card;{json.dumps(data)}') + ses.special(data) + +@sio.event +def gold_rush_discard(sid): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};gold_rush_discard;') + ses.gold_rush_discard() + +@sio.event +def buy_gold_rush_card(sid, data:int): + ses: Player = sio.get_session(sid) + ses.game.rpc_log.append(f'{ses.name};buy_gold_rush_card;{data}') + ses.buy_gold_rush_card(data) + +@sio.event +def chat_message(sid, msg, pl=None): + ses: Player = sio.get_session(sid) if pl is None else pl + ses.game.rpc_log.append(f'{ses.name};chat_message;{msg}') + if len(msg) > 0: + if msg[0] == '/': + commands = msg.split(';') + for msg in commands: + if '/addbot' in msg and not ses.game.started: + if len(msg.split()) > 1: + # for _ in range(int(msg.split()[1])): + # ses.game.add_player(Player(f'AI_{random.randint(0,1000)}', 'bot', sio, bot=True)) + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'Only 1 bot at the time'}) + else: + bot = Player(f'AI_{random.randint(0,10)}', 'bot', sio, bot=True) + while any([p for p in ses.game.players if p.name == bot.name]): + bot = Player(f'AI_{random.randint(0,10)}', 'bot', sio, bot=True) + ses.game.add_player(bot) + bot.bot_spin() + return + if '/report' in msg and not ses.game.is_replay: + data = "\n".join(ses.game.rpc_log[:-1]).strip() + response = requests.post("https://www.toptal.com/developers/hastebin/documents", data) + key = json.loads(response.text).get('key') + if "DISCORD_WEBHOOK" in os.environ and len(os.environ['DISCORD_WEBHOOK']) > 0: + webhook = DiscordWebhook(url=os.environ['DISCORD_WEBHOOK'], content=f'New bug report, replay at https://www.toptal.com/developers/hastebin/{key}') + response = webhook.execute() + sio.emit('chat_message', room=sid, data={'color': f'green','text':f'Report OK'}) + else: + print("WARNING: DISCORD_WEBHOOK not found") + print(f'New bug report, replay at https://www.toptal.com/developers/hastebin/{key}') + return + if '/replay' in msg and not '/replayspeed' in msg: + _cmd = msg.split() + if len(_cmd) == 2: + replay_id = _cmd[1] + response = requests.get(f"https://www.toptal.com/developers/hastebin/raw/{replay_id}") + log = response.text.splitlines() + ses.game.spectators.append(ses) + ses.game.replay(log) + return + if '/replayspeed' in msg: + _cmd = msg.split() + if len(_cmd) == 2: + ses.game.replay_speed = float(_cmd[1]) + return + if '/startwithseed' in msg and not ses.game.started: + if len(msg.split()) > 1: + ses.game.start_game(int(msg.split()[1])) + return + elif '/removebot' in msg and not ses.game.started: + if any([p.is_bot for p in ses.game.players]): + [p for p in ses.game.players if p.is_bot][-1].disconnect() + return + elif '/togglecomp' in msg and ses.game: + ses.game.toggle_competitive() + return + if '/debug' in msg: + cmd = msg.split() + if len(cmd) == 2 and 'DEPLOY_KEY' in os.environ and cmd[1] == os.environ['DEPLOY_KEY']: # solo chi ha la deploy key può attivare la modalità debug + ses.game.debug = not ses.game.debug + ses.game.notify_room() + elif ses == ses.game.players[0]: # solo l'owner può attivare la modalità debug + ses.game.debug = not ses.game.debug + ses.game.notify_room() + if ses.game.debug: + sio.emit('chat_message', room=sid, data={'color': f'red','text':f'debug mode is now active, only the owner of the room can disable it with /debug'}) + return + if not ses.game.debug: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'debug mode is not active, only the owner of the room can enable it with /debug'}) + elif '/set_chars' in msg and not ses.game.started: + cmd = msg.split() + if len(cmd) == 2 and int(cmd[1]) > 0: + ses.game.characters_to_distribute = int(cmd[1]) + elif '/suicide' in msg and ses.game.started and ses.lives > 0: + ses.lives = 0 + ses.notify_self() + elif '/nextevent' in msg and ses.game.started: + ses.game.deck.flip_event() + elif '/notify' in msg and ses.game.started: + cmd = msg.split() + if len(cmd) >= 3: + if cmd[1] in ses.game.players_map: + ses.game.get_player_named(cmd[1]).notify_card(ses, { + 'name': ' '.join(cmd[2:]), + 'icon': '🚨', + 'suit': 4, + 'number': ' '.join(cmd[2:]) + }) + else: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) + elif '/show_cards' in msg and ses.game.started: + cmd = msg.split() + if len(cmd) == 2: + if cmd[1] in ses.game.players_map: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and is looking at {cmd[1]} hand'}) + for c in ses.game.get_player_named(cmd[1]).hand: + ses.notify_card(ses, c) + eventlet.sleep(0.3) + else: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) + elif '/ddc' in msg and ses.game.started: # debug destroy cards usage: [/ddc *] [/ddc username] + cmd = msg.split() + if len(cmd) == 2: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode destroyed {cmd[1]} cards'}) + if cmd[1] == "*": + for p in ses.game.players_map: + ses.game.get_player_named(p).hand = [] + ses.game.get_player_named(p).equipment = [] + ses.game.get_player_named(p).notify_self() + elif cmd[1] in ses.game.players_map: + ses.game.get_player_named(cmd[1]).hand = [] + ses.game.get_player_named(cmd[1]).equipment = [] + ses.game.get_player_named(cmd[1]).notify_self() + else: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) + elif '/dsh' in msg and ses.game.started: #debug set health usage [/dsh * hp] [/dsh username hp] + cmd = msg.split() + if len(cmd) == 3: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and is changing {cmd[1]} health'}) + if cmd[1] == "*": + for p in ses.game.players_map: + ses.game.get_player_named(p).lives = int(cmd[2]) + ses.game.get_player_named(p).notify_self() + elif cmd[1] in ses.game.players_map: + ses.game.get_player_named(cmd[1]).lives = int(cmd[2]) + ses.game.get_player_named(cmd[1]).notify_self() + else: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) + elif '/togglebot' in msg and ses.game: + ses.game.toggle_disconnect_bot() + elif '/cancelgame' in msg and ses.game.started: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} stopped the current game'}) + ses.game.reset() + elif '/startgame' in msg and not ses.game.started: + ses.game.start_game() + elif '/setbotspeed' in msg: + ses.game.bot_speed = float(msg.split()[1]) + elif '/addex' in msg and not ses.game.started: + cmd = msg.split() + if len(cmd) == 2: + cmd[1] = cmd[1].replace('foc', 'fistful_of_cards') + if cmd[1] not in ses.game.available_expansions: + ses.game.available_expansions.append(cmd[1]) + ses.game.notify_room() + else: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) + elif '/setcharacter' in msg: + import bang.characters as characters + cmd = msg.split() + if len(cmd) >= 2: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed character'}) + chs = characters.all_characters(ses.game.expansions) + ses.character = [c for c in chs if c.name == ' '.join(cmd[1:])][0] + ses.real_character = ses.character + ses.notify_self() + elif '/setevent' in msg and ses.game and ses.game.deck: #add event before the position /setevent (position) 0 (name) Peyote + cmd = msg.split() + if len(cmd) >= 3: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed event'}) + import bang.expansions.fistful_of_cards.card_events as ce + import bang.expansions.high_noon.card_events as ceh + chs = [] + chs.extend(ce.get_all_events()) + chs.append(ce.get_endgame_card()) + chs.extend(ceh.get_all_events()) + chs.append(ceh.get_endgame_card()) + ses.game.deck.event_cards.insert(int(cmd[1]), [c for c in chs if c!=None and c.name == ' '.join(cmd[2:])][0]) + ses.game.notify_event_card() + elif '/removecard' in msg: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and removed a card'}) + cmd = msg.split() + if len(cmd) == 2: + if int(cmd[1]) < len(ses.hand): + ses.hand.pop(int(cmd[1])) + else: + ses.equipment.pop(int(cmd[1])-len(ses.hand)) + ses.notify_self() + elif '/getcard' in msg: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got a card'}) + import bang.cards as cs + cmd = msg.split() + if len(cmd) >= 2: + cards = cs.get_starting_deck(ses.game.expansions) + card_names = ' '.join(cmd[1:]).split(',') + for cn in card_names: + ses.hand.append([c for c in cards if c.name == cn][0]) + ses.notify_self() + elif '/getnuggets' in msg: + sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got nuggets'}) + import bang.cards as cs + cmd = msg.split() + if len(cmd) == 2: + ses.gold_nuggets += int(cmd[1]) + ses.notify_self() + elif '/gameinfo' in msg: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'info: {dict(filter(lambda x:x[0] != "rpc_log",ses.game.__dict__.items()))}'}) + elif '/meinfo' in msg: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'info: {ses.__dict__}'}) + elif '/mebot' in msg: + ses.is_bot = not ses.is_bot + if (ses.is_bot): + ses.was_player = True + ses.bot_spin() + elif '/arcadekick' in msg and ses.game.started: + if len([p for p in ses.game.players if p.pending_action != PendingAction.WAIT]) == 0: + sio.emit('chat_message', room=ses.game.name, data={'color': f'','text':f'KICKING THE ARCADE CABINET'}) + ses.game.next_turn() + else: + sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} COMMAND NOT FOUND'}) + else: + color = sid.encode('utf-8').hex()[-3:] + sio.emit('chat_message', room=ses.game.name, data={'color': f'#{color}','text':f'[{ses.name}]: {msg}'}) + + +""" +Sockets for the help screen +""" + +@sio.event +def get_cards(sid): + import bang.cards as c + cards = c.get_starting_deck(['dodge_city']) + cards_dict = {} + for ca in cards: + if ca.name not in cards_dict: + cards_dict[ca.name] = ca + cards = [cards_dict[i] for i in cards_dict] + sio.emit('cards_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) + +@sio.event +def get_characters(sid): + import bang.characters as ch + cards = ch.all_characters(['dodge_city', 'gold_rush']) + sio.emit('characters_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) + +@sio.event +def get_highnooncards(sid): + import bang.expansions.high_noon.card_events as ceh + chs = [] + chs.extend(ceh.get_all_events()) + chs.append(ceh.get_endgame_card()) + sio.emit('highnooncards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) + +@sio.event +def get_foccards(sid): + import bang.expansions.fistful_of_cards.card_events as ce + chs = [] + chs.extend(ce.get_all_events()) + chs.append(ce.get_endgame_card()) + sio.emit('foccards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) + +@sio.event +def get_goldrushcards(sid): + import bang.expansions.gold_rush.shop_cards as grc + cards = grc.get_cards() + cards_dict = {} + for ca in cards: + if ca.name not in cards_dict: + cards_dict[ca.name] = ca + cards = [cards_dict[i] for i in cards_dict] + sio.emit('goldrushcards_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) + +if __name__ == '__main__': + eventlet.wsgi.server(eventlet.listen(('', 5001)), app) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/cards_test.py b/backend/tests/cards_test.py new file mode 100644 index 0000000..2aa46d8 --- /dev/null +++ b/backend/tests/cards_test.py @@ -0,0 +1,495 @@ +from random import randint +from bang.characters import Character +from bang.cards import * +from tests.dummy_socket import DummySocket +from bang.deck import Deck +from bang.game import Game +from bang.players import Player, PendingAction + +# test card Barile +def test_barile(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 2)] + p.set_character(p.available_characters[0].name) + barrel_guy = g.players[g.turn] + barrel_guy.draw('') + barrel_guy.hand = [Barile(0,0)] + barrel_guy.play_card(0) + assert isinstance(barrel_guy.equipment[0], Barile) + barrel_guy.end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Volcanic(0,0), Bang(0,0), Bang(0,0)] + g.players[g.turn].play_card(0) + g.players[g.turn].play_card(0, barrel_guy.name) + assert g.players[g.turn].pending_action == PendingAction.WAIT + assert barrel_guy.pending_action == PendingAction.PICK + g.deck.cards[0] = Bang(Suit.HEARTS, 5) + barrel_guy.pick() + assert barrel_guy.pending_action == PendingAction.WAIT + assert barrel_guy.lives == barrel_guy.max_lives + assert g.players[g.turn].pending_action == PendingAction.PLAY + g.players[g.turn].play_card(0, barrel_guy.name) + g.deck.cards[0] = Bang(Suit.SPADES, 5) + barrel_guy.pick() + assert barrel_guy.pending_action == PendingAction.WAIT + assert barrel_guy.lives == barrel_guy.max_lives - 1 + assert g.players[g.turn].pending_action == PendingAction.PLAY + +#test card Volcanic +def test_volcanic(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 3)] + p.set_character(p.available_characters[0].name) + for p in ps: + p.hand = [] + volcanic_guy = g.players[g.turn] + volcanic_guy.draw('') + volcanic_guy.hand = [Volcanic(0,0), Bang(0,0), Bang(0,0)] + volcanic_guy.play_card(0) + assert isinstance(volcanic_guy.equipment[0], Volcanic) + assert volcanic_guy.get_sight() == 1 + volcanic_guy.play_card(0, g.players[(g.turn+1)%3].name) + assert len(volcanic_guy.hand) == 1 + volcanic_guy.play_card(0, g.players[(g.turn+1)%3].name) + assert len(volcanic_guy.hand) == 0 + +# test card Dinamite +def test_dinamite(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + for p in ps: + p.hand = [] + dinamite_guy = g.players[g.turn] + dinamite_guy.draw('') + dinamite_guy.hand = [Dinamite(0,0)] + dinamite_guy.play_card(0) + assert isinstance(dinamite_guy.equipment[0], Dinamite) + dinamite_guy.end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.deck.cards.insert(0, Dinamite(Suit.HEARTS, 5)) + dinamite_guy.pick() + assert len(dinamite_guy.equipment) == 0 + dinamite_guy.draw('') + dinamite_guy.end_turn() + assert len(g.players[g.turn].equipment) == 1 + g.deck.cards.insert(0, Dinamite(Suit.SPADES, 5)) + g.players[g.turn].pick() + assert len(g.players[g.turn].equipment) == 0 + assert g.players[g.turn].lives == 1 + +# test mirino +def test_mirino(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(4)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + mirino_guy = g.players[g.turn] + mirino_guy.draw('') + mirino_guy.hand = [Mirino(0,0)] + assert mirino_guy.get_sight(countWeapon=False) == 1 + mirino_guy.play_card(0) + assert mirino_guy.get_sight(countWeapon=False) == 2 + +# test mustang +def test_mustang(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + mustang_guy = g.players[g.turn] + mustang_guy.draw('') + mustang_guy.hand = [Mustang(0,0)] + assert mustang_guy.get_visibility() == 0 + mustang_guy.play_card(0) + assert mustang_guy.get_visibility() == 1 + +# test Prigione +def test_prigione(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(4)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + sheriff = g.players[g.turn] + sheriff.draw('') + sheriff.hand = [Prigione(0,0)] + sheriff.play_card(0, g.players[(g.turn+1)%4].name) + assert len(sheriff.hand) == 0 + sheriff.end_turn() + g.deck.cards.insert(0, Prigione(Suit.CLUBS, 5)) + skip_check = g.turn + g.players[g.turn].pick() + assert g.turn != skip_check + g.players[g.turn].draw('') + g.players[g.turn].hand = [Prigione(0,0)] + g.players[g.turn].play_card(0, sheriff.name) + assert len(g.players[g.turn].hand) == 1 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%4].name) + g.players[g.turn].end_turn() + g.deck.cards.insert(0, Prigione(Suit.HEARTS, 5)) + skip_check = g.turn + g.players[g.turn].pick() + assert g.turn == skip_check + +# test all weapons ranges +def test_all_weapons(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(4)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [Volcanic(0,0), Schofield(0,0), Remington(0,0), RevCarabine(0,0), Winchester(0,0)] + g.players[g.turn].play_card(0) + assert g.players[g.turn].get_sight() == 1 + g.players[g.turn].play_card(0) + assert g.players[g.turn].get_sight() == 2 + g.players[g.turn].play_card(0) + assert g.players[g.turn].get_sight() == 3 + g.players[g.turn].play_card(0) + assert g.players[g.turn].get_sight() == 4 + g.players[g.turn].play_card(0) + assert g.players[g.turn].get_sight() == 5 + +# test bang +def test_bang(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0), Bang(0,0)] + assert len(g.players[g.turn].hand) == 2 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert len(g.players[g.turn].hand) == 1 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert len(g.players[g.turn].hand) == 1 + +# test birra +def test_birra_2p(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [Birra(0,0)] + g.players[g.turn].lives = 1 + g.players[g.turn].play_card(0) + assert g.players[g.turn].lives == 1 + +# test birra +def test_birra_3p(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + initial_p = g.players[g.turn] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Birra(0,0)] + g.players[g.turn].lives = 1 + g.players[g.turn].play_card(0) + assert g.players[g.turn].lives == 2 + # test beer save + g.players[g.turn].hand = [Birra(0,0)] + g.players[g.turn].lives = 1 + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, initial_p.name) + assert initial_p.lives == 1 + # test non overflow + g.players[g.turn].lives = g.players[g.turn].max_lives + g.players[g.turn].hand = [Birra(0,0)] + g.players[g.turn].play_card(0) + assert g.players[g.turn].lives == g.players[g.turn].max_lives + +# test CatBalou +def test_catbalou(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [CatBalou(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + g.players[g.turn].choose(0) + assert len(g.players[g.turn].hand) == 0 + assert len(g.deck.scrap_pile) == 2 + assert len(g.players[(g.turn+1)%2].hand) == 3 + + +# test Diligenza +def test_diligenza(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(4)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [Diligenza(0,0)] + g.players[g.turn].play_card(0) + assert len(g.players[g.turn].hand) == 2 + +# test Duello +def test_duello(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + for p in ps: + p.hand = [] + g.players[g.turn].draw('') + # winning duello + g.players[g.turn].hand = [Duello(0,0), Duello(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert len(g.players[g.turn].hand) == 1 + assert g.players[(g.turn+1)%2].lives == g.players[(g.turn+1)%2].max_lives - 1 + # losing duello + g.players[(g.turn+1)%2].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[g.turn].pending_action == PendingAction.WAIT + assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND + g.players[(g.turn+1)%2].respond(0) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.WAIT + assert g.players[g.turn].lives == g.players[g.turn].max_lives - 1 + +# test Emporio +def test_emporio(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(7)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + for p in ps: + p.hand = [] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Emporio(0,0)] + g.players[g.turn].play_card(0) + assert g.players[g.turn].pending_action == PendingAction.CHOOSE + g.players[g.turn].choose(0) + print(g.players[g.turn].name) + for i in range(1, len(g.players)-1): + assert g.players[(g.turn+i)%7].pending_action == PendingAction.CHOOSE + g.players[(g.turn+i)%7].choose(0) + for p in ps: + assert len(p.hand) == 1 + assert g.players[g.turn].pending_action == PendingAction.PLAY + +# test Gatling +def test_gatling(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(7)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + # test lose gatling + for p in ps: + p.hand = [] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Gatling(0,0), Gatling(0,0)] + g.players[g.turn].play_card(0) + for p in ps: + if p != g.players[g.turn]: + assert p.lives == p.max_lives - 1 + # test win gatling + for p in ps: + if p != g.players[g.turn]: + p.hand = [Mancato(0,0)] + g.players[g.turn].play_card(0) + assert g.players[g.turn].pending_action == PendingAction.WAIT + for p in ps: + if p != g.players[g.turn]: + p.respond(0) + assert g.players[g.turn].pending_action == PendingAction.PLAY + for p in ps: + if p != g.players[g.turn]: + assert p.lives == p.max_lives - 1 + +# test Indiani +def test_indiani(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(7)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + # test lose indiani + for p in ps: + p.hand = [] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Indiani(0,0), Indiani(0,0)] + g.players[g.turn].play_card(0) + for p in ps: + if p != g.players[g.turn]: + assert p.lives == p.max_lives - 1 + # test win indiani + for p in ps: + if p != g.players[g.turn]: + p.hand = [Bang(0,0)] + g.players[g.turn].play_card(0) + assert g.players[g.turn].pending_action == PendingAction.WAIT + for p in ps: + if p != g.players[g.turn]: + p.respond(0) + assert g.players[g.turn].pending_action == PendingAction.PLAY + for p in ps: + if p != g.players[g.turn]: + assert p.lives == p.max_lives - 1 + +# test Mancato +def test_mancato(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + for p in ps: + p.hand = [Mancato(0,0)] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND + g.players[(g.turn+1)%2].respond(0) + assert g.players[(g.turn+1)%2].lives == g.players[(g.turn+1)%2].max_lives + assert g.players[(g.turn+1)%2].pending_action == PendingAction.WAIT + assert g.players[g.turn].pending_action == PendingAction.PLAY + +# test Panico +def test_panico(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [Panico(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + g.players[g.turn].choose(0) + assert len(g.players[g.turn].hand) == 1 + assert len(g.deck.scrap_pile) == 1 + assert len(g.players[(g.turn+1)%2].hand) == 3 + +# test Saloon +def test_saloon(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(8)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + for p in ps: + p.lives = randint(p.max_lives-1, p.max_lives) + g.players[g.turn].draw('') + g.players[g.turn].hand = [Saloon(0,0)] + g.players[g.turn].play_card(0) + for p in ps: + assert p.lives == p.max_lives + +# test WellsFargo +def test_diligenza(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(4)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].draw('') + g.players[g.turn].hand = [WellsFargo(0,0)] + g.players[g.turn].play_card(0) + assert len(g.players[g.turn].hand) == 3 diff --git a/backend/tests/character_test.py b/backend/tests/character_test.py new file mode 100644 index 0000000..740f757 --- /dev/null +++ b/backend/tests/character_test.py @@ -0,0 +1,367 @@ +from random import randint +from bang.characters import * +from tests.dummy_socket import DummySocket +from bang.deck import Deck +from bang.game import Game +from bang.players import Player, PendingAction +from bang.cards import * + +def test_bartcassidy(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), BartCassidy()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if isinstance(g.players[g.turn].character, BartCassidy): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[(g.turn+1)%2].hand = [] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + assert len(g.players[(g.turn+1)%2].hand) == 0 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert len(g.players[(g.turn+1)%2].hand) == 1 + +def test_blackjack(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(1)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [BlackJack()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + g.players[g.turn].hand = [] + g.deck.cards.insert(1, Bang(Suit.HEARTS, 1)) + g.players[g.turn].draw('') + assert len(g.players[g.turn].hand) == 3 + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.deck.cards.insert(1, Bang(Suit.CLUBS, 1)) + g.players[g.turn].draw('') + assert len(g.players[g.turn].hand) == 2 + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.deck.cards.insert(1, Bang(Suit.DIAMONDS, 1)) + g.players[g.turn].draw('') + assert len(g.players[g.turn].hand) == 3 + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.deck.cards.insert(1, Bang(Suit.SPADES, 1)) + g.players[g.turn].draw('') + assert len(g.players[g.turn].hand) == 2 + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + +def test_calamityjanet(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), CalamityJanet()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if isinstance(g.players[g.turn].character, CalamityJanet): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[(g.turn+1)%2].hand = [Bang(0,0)] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND + g.players[(g.turn+1)%2].respond(0) + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Mancato(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].lives == 1 + +def test_ElGringo(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), ElGringo()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if isinstance(g.players[g.turn].character, ElGringo): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[(g.turn+1)%2].hand = [] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0), Bang(0,0)] + assert len(g.players[(g.turn+1)%2].hand) == 0 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert len(g.players[(g.turn+1)%2].hand) == 1 + assert len(g.players[g.turn].hand) == 0 + +def test_JesseJones(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), JesseJones()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if not isinstance(g.players[g.turn].character, JesseJones): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[(g.turn+1)%2].hand = [Bang(0,0)] + g.players[g.turn].draw('p1' if g.turn == 0 else 'p0') + g.players[g.turn].hand = [Bang(0,0), Bang(0,0)] + assert len(g.players[(g.turn+1)%2].hand) == 0 + assert len(g.players[g.turn].hand) == 2 + +def test_Jourdonnais(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), Jourdonnais()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if isinstance(g.players[g.turn].character, Jourdonnais): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.PICK + +def test_KitCarlson(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), KitCarlson()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if not isinstance(g.players[g.turn].character, KitCarlson): + g.players[g.turn].draw('') + g.players[g.turn].hand = [Mancato(0,0)] + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + assert g.players[g.turn].pending_action == PendingAction.CHOOSE + assert len(g.players[g.turn].available_cards) == 3 + g.players[g.turn].choose(0) + assert len(g.players[g.turn].available_cards) == 2 + g.players[g.turn].choose(1) + assert g.players[g.turn].pending_action == PendingAction.PLAY + +def test_LuckyDuke(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [LuckyDuke(), LuckyDuke()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + g.players[0].equipment = [Prigione(0,0)] + g.players[1].equipment = [Prigione(0,0)] + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + assert g.players[g.turn].pending_action == PendingAction.PICK + g.deck.cards.insert(0, Bang(Suit.SPADES,0)) + g.deck.cards.insert(1, Bang(Suit.HEARTS,0)) + g.players[g.turn].pick() + assert g.players[g.turn].pending_action == PendingAction.DRAW + +def test_PaulRegret(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), PaulRegret()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + pls = g.get_visible_players(g.players[0]) + assert len(pls) == 1 + assert pls[0]['name'] == g.players[1].name + assert pls[0]['dist'] > g.players[0].get_sight() + +def test_PedroRamirez(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), PedroRamirez()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if not isinstance(g.players[g.turn].character, PedroRamirez): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.deck.scrap_pile.append(Bang(0,0)) + g.players[g.turn].hand = [] + g.players[g.turn].draw('scrap') + assert len(g.players[g.turn].hand) == 2 + assert g.players[g.turn].hand[0].number == 0 + assert g.players[g.turn].hand[0].suit == 0 + assert isinstance(g.players[g.turn].hand[0], Bang) + +def test_RoseDoolan(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 2), RoseDoolan()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + g.players[0].equipment = [Mustang(0,0)] + g.players[0].notify_self() + pls = g.get_visible_players(g.players[1]) + print(pls) + assert len(pls) == 1 + assert pls[0]['name'] != g.players[1].name + assert pls[0]['dist'] <= g.players[1].get_sight() + +def test_SidKetchum(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), SidKetchum()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if not isinstance(g.players[g.turn].character, SidKetchum): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].lives = 1 + g.players[g.turn].scrap(0) + assert g.players[g.turn].lives == 1 + g.players[g.turn].scrap(0) + assert g.players[g.turn].lives == 2 + +def test_SlabTheKiller(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), SlabTheKiller()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if not isinstance(g.players[g.turn].character, SlabTheKiller): + g.players[g.turn].draw('') + g.players[g.turn].hand = [Mancato(0,0)] + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND + g.players[(g.turn+1)%2].respond(0) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND + g.players[(g.turn+1)%2].respond(-1) + assert g.players[(g.turn+1)%2].pending_action == PendingAction.WAIT + assert g.players[(g.turn+1)%2].lives == 3 + +def test_SuzyLafayette(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), SuzyLafayette()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + g.players[1].hand = [] + assert len(g.players[1].hand) == 0 + g.players[1].notify_self() + assert len(g.players[1].hand) == 1 + g.players[g.turn].end_turn() + +def test_VultureSam(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), Character('test_char', 4), VultureSam()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if isinstance(g.players[g.turn].character, VultureSam): + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0), Bang(0,0), Bang(0,0), Bang(0,0)] + g.players[g.turn].end_turn() + while not isinstance(g.players[g.turn].character, VultureSam): + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0), Bang(0,0), Bang(0,0), Bang(0,0)] + g.players[g.turn].lives = 0 + g.players[g.turn].notify_self() + assert len(g.players[2].hand) == 8 + return + +def test_WillyTheKid(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(2)] + for p in ps: + g.add_player(p) + g.start_game() + test_chars = [Character('test_char', 4), WillyTheKid()] + for p in ps: + p.available_characters = [test_chars.pop(0)] + p.set_character(p.available_characters[0].name) + if not isinstance(g.players[g.turn].character, WillyTheKid): + g.players[g.turn].draw('') + g.players[g.turn].hand = [] + g.players[g.turn].end_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0), Bang(0,0), Bang(0,0)] + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].lives == 3 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].lives == 2 + g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) + assert g.players[(g.turn+1)%2].lives == 1 \ No newline at end of file diff --git a/backend/tests/deck_test.py b/backend/tests/deck_test.py new file mode 100644 index 0000000..0f7bf2d --- /dev/null +++ b/backend/tests/deck_test.py @@ -0,0 +1,37 @@ +from tests.dummy_socket import DummySocket +from bang.deck import Deck +from bang.game import Game + +def test_card_flip(): + g = Game('test', DummySocket()) + g.deck = Deck(g) + l = len(g.deck.cards) + assert g.deck.pick_and_scrap() != None + assert len(g.deck.cards) == l - 1 + assert len(g.deck.scrap_pile) == 1 + +def test_draw(): + g = Game('test', DummySocket()) + g.deck = Deck(g) + l = len(g.deck.cards) + assert g.deck.draw(True) != None + assert len(g.deck.cards) == l - 1 + assert len(g.deck.scrap_pile) == 0 + +def test_reshuffle(): + g = Game('test', DummySocket()) + g.deck = Deck(g) + l = len(g.deck.cards) + for i in range(80): + assert g.deck.pick_and_scrap() != None + assert len(g.deck.cards) == 79 + assert len(g.deck.scrap_pile) == 1 + +def test_draw_from_scrap(): + g = Game('test', DummySocket()) + g.deck = Deck(g) + l = len(g.deck.cards) + assert g.deck.pick_and_scrap() != None + assert g.deck.draw_from_scrap_pile() != None + assert len(g.deck.cards) == 79 + assert len(g.deck.scrap_pile) == 0 \ No newline at end of file diff --git a/backend/tests/dummy_socket.py b/backend/tests/dummy_socket.py new file mode 100644 index 0000000..554af02 --- /dev/null +++ b/backend/tests/dummy_socket.py @@ -0,0 +1,11 @@ + +class DummySocket(): + def __init__(self, sio=None): + self.true_sio = sio + + def emit(self, event, data=None, to=None, room=None, skip_sid=None, namespace=None, callback=None, **kwargs): + # print(f'event: {event}, data: {data}, to: {to}, room: {room}') + if self.true_sio and event == 'chat_message': + self.true_sio.emit(event, data, to, room, skip_sid, namespace, callback, **kwargs) + return True + is_fake = True \ No newline at end of file diff --git a/backend/tests/game_test.py b/backend/tests/game_test.py new file mode 100644 index 0000000..57cdd0e --- /dev/null +++ b/backend/tests/game_test.py @@ -0,0 +1,91 @@ +from tests.dummy_socket import DummySocket +from bang.deck import Deck +from bang.game import Game +from bang.players import Player, PendingAction +from bang.roles import * +from bang.cards import * + +# test that game can start +def test_game_start(): + sio = DummySocket() + g = Game('test', sio) + p1 = Player('p1', 'p1', sio) + g.add_player(p1) + p2 = Player('p2', 'p2', sio) + g.add_player(p2) + p3 = Player('p3', 'p3', sio) + g.add_player(p3) + assert p1.role == None + assert p2.role == None + assert p3.role == None + assert not g.started + g.start_game() + assert g.started + assert p1.role != None + assert p2.role != None + assert p3.role != None + assert len(p1.available_characters) == g.characters_to_distribute + assert len(p2.available_characters) == g.characters_to_distribute + assert len(p3.available_characters) == g.characters_to_distribute + p1.set_character(p1.available_characters[0].name) + assert p1.character != None + p2.set_character(p2.available_characters[0].name) + assert p2.character != None + p3.set_character(p3.available_characters[0].name) + assert p3.character != None + assert g.players[g.turn].pending_action == PendingAction.DRAW + +# test that dodge_city is added to games with more than 8 players +def test_dodge_city(): + sio = DummySocket() + g = Game('test', sio) + for i in range(9): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + assert 'dodge_city' in g.expansions + +# test that a game with 2 players has only renegade as role +def test_renegade_only(): + sio = DummySocket() + g = Game('test', sio) + p1 = Player('p1', 'p1', sio) + g.add_player(p1) + p2 = Player('p2', 'p2', sio) + g.add_player(p2) + g.start_game() + assert isinstance(g.players[0].role, Renegade) + assert isinstance(g.players[1].role, Renegade) + +# test that a game with 3 player has Renegade, Vice and Outlaw as roles +def test_renegade_vice_outlaw(): + sio = DummySocket() + g = Game('test', sio) + for i in range(3): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + roles = {p.role.name for p in g.players} + assert len(roles) == 3 + +# test that a game with 4 players has all roles except the deputy +def test_4_players_roles(): + sio = DummySocket() + g = Game('test', sio) + for i in range(4): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + roles = {p.role.name for p in g.players} + assert len(roles) == 3 + +# test that a game with 5 players has all roles +def test_5_players_roles(): + sio = DummySocket() + g = Game('test', sio) + for i in range(5): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + roles = {p.role.name for p in g.players} + assert len(roles) == 4 + diff --git a/backend/tests/roles_test.py b/backend/tests/roles_test.py new file mode 100644 index 0000000..ab32b4f --- /dev/null +++ b/backend/tests/roles_test.py @@ -0,0 +1,213 @@ +from bang.characters import Character +from tests.dummy_socket import DummySocket +from bang.deck import Deck +from bang.game import Game +from bang.players import Player, PendingAction +from bang.roles import * +from bang.cards import * + +# test that a game with 3 player the deputy kills renegade and wins +def test_3p_deputy_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(3): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 3 + assert isinstance(g.players[g.turn].role, Vice) + for i in range(3): + g.players[i].lives = 1 + g.players[i].hand = [] + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, against=g.players[roles['Rinnegato']].name) + assert (hasattr(g.players[g.turn], 'win_status') and g.players[g.turn].win_status) + assert not (hasattr(g.players[roles['Rinnegato']], 'win_status') and g.players[roles['Rinnegato']].win_status) + assert not (hasattr(g.players[roles['Fuorilegge']], 'win_status') and g.players[roles['Fuorilegge']].win_status) + +# test that a game with 3 player the renegade kills the outlaw and wins +def test_3p_renegade_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(3): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 3 + assert isinstance(g.players[g.turn].role, Vice) + for i in range(3): + g.players[i].lives = 1 + g.players[i].hand = [] + g.turn = roles['Rinnegato'] + g.play_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, against=g.players[roles['Fuorilegge']].name) + assert (hasattr(g.players[g.turn], 'win_status') and g.players[g.turn].win_status) + assert not (hasattr(g.players[roles['Vice']], 'win_status') and g.players[roles['Vice']].win_status) + assert not (hasattr(g.players[roles['Fuorilegge']], 'win_status') and g.players[roles['Fuorilegge']].win_status) + +# test that a game with 3 player the outlaw kills the deputy and wins +def test_3p_outlaw_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(3): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 3 + assert isinstance(g.players[g.turn].role, Vice) + for i in range(3): + g.players[i].lives = 1 + g.players[i].hand = [] + g.turn = roles['Fuorilegge'] + g.play_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, against=g.players[roles['Vice']].name) + assert (hasattr(g.players[g.turn], 'win_status') and g.players[g.turn].win_status) + assert not (hasattr(g.players[roles['Vice']], 'win_status') and g.players[roles['Vice']].win_status) + assert not (hasattr(g.players[roles['Rinnegato']], 'win_status') and g.players[roles['Rinnegato']].win_status) + +# test that a game with 4 player the outlaw kills the sheriff and win +def test_4p_outlaw_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(4): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 3 + assert isinstance(g.players[g.turn].role, Sheriff) + for i in range(4): + g.players[i].lives = 1 + g.players[i].hand = [] + g.turn = roles['Fuorilegge'] + g.play_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Bang(0,0)] + g.players[g.turn].play_card(0, against=g.players[roles['Sceriffo']].name) + for i in range(4): + if isinstance(g.players[i].role, Outlaw): + assert (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + else: + assert not (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + +# test that a game with 5 player the renegade kills all the other players and wins +def test_5p_renegade_gatling_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(5): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 4 + assert isinstance(g.players[g.turn].role, Sheriff) + g.players[g.turn].is_my_turn = False + for i in range(len(g.players)): + g.players[i].lives = 1 + g.players[i].hand = [] + g.turn = roles['Rinnegato'] + g.play_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Gatling(0,0)] + g.players[g.turn].play_card(0) + for i in range(len(g.players)): + if isinstance(g.players[i].role, Renegade): + print (g.players[i].role.name, 'win_status:', hasattr(g.players[i], 'win_status') and g.players[i].win_status) + assert (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + else: + print(g.players[i].role.name, 'win_status:', (hasattr(g.players[i], 'win_status') and g.players[i].win_status)) + assert not (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + +# test that a game with 5 player the renegade kills all the other players and wins +def test_5p_renegade_indiani_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(5): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 4 + assert isinstance(g.players[g.turn].role, Sheriff) + g.players[g.turn].is_my_turn = False + for i in range(len(g.players)): + g.players[i].lives = 1 + g.players[i].hand = [] + g.turn = roles['Rinnegato'] + g.play_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Indiani(0,0)] + g.players[g.turn].play_card(0) + for i in range(len(g.players)): + if isinstance(g.players[i].role, Renegade): + print (g.players[i].role.name, 'win_status:', hasattr(g.players[i], 'win_status') and g.players[i].win_status) + assert (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + else: + print(g.players[i].role.name, 'win_status:', (hasattr(g.players[i], 'win_status') and g.players[i].win_status)) + assert not (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + +# test that a game with 5 player the renegade kills the sheriff but it isn't the last alive player and the outlaws wins +def test_5p_outlaw_death_win(): + sio = DummySocket() + g = Game('test', sio) + for i in range(5): + p = Player(f'p{i}', f'p{i}', sio) + g.add_player(p) + g.start_game() + for p in g.players: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + roles = {g.players[i].role.name:i for i in range(len(g.players))} + print(roles) + assert len(roles) == 4 + assert isinstance(g.players[g.turn].role, Sheriff) + g.players[g.turn].is_my_turn = False + for i in range(len(g.players)): + g.players[i].lives = 1 + g.players[i].hand = [] + g.players[roles['Vice']].lives = 2 + g.turn = roles['Rinnegato'] + g.play_turn() + g.players[g.turn].draw('') + g.players[g.turn].hand = [Gatling(0,0)] + g.players[g.turn].play_card(0) + for i in range(len(g.players)): + if isinstance(g.players[i].role, Outlaw): + print (g.players[i].role.name, 'win_status:', hasattr(g.players[i], 'win_status') and g.players[i].win_status) + assert (hasattr(g.players[i], 'win_status') and g.players[i].win_status) + assert (hasattr(g.players[i], 'is_dead') and g.players[i].is_dead) + else: + print(g.players[i].role.name, 'win_status:', (hasattr(g.players[i], 'win_status') and g.players[i].win_status)) + assert not (hasattr(g.players[i], 'win_status') and g.players[i].win_status) diff --git a/frontend/package.json b/frontend/package.json index b0fbc9e..e3ae330 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,27 +8,28 @@ "lint": "vue-cli-service lint" }, "dependencies": { - "core-js": "^3.15.1", + "caniuse-lite": "^1.0.30001313", + "core-js": "^3.16.2", "pretty-checkbox-vue": "^1.1.9", "register-service-worker": "^1.7.2", - "socket.io-client": "^4.1.2", + "socket.io-client": "^4.4.1", "vue": "^2.6.14", - "vue-clipboard2": "^0.3.1", - "vue-i18n": "^8.24.5", - "vue-router": "^3.5.2", + "vue-clipboard2": "^0.3.3", + "vue-i18n": "^8.27.0", + "vue-router": "^3.5.3", "vue-socket.io": "^3.0.10" }, "devDependencies": { - "@vue/cli-plugin-babel": "~4.5.13", - "@vue/cli-plugin-eslint": "~4.5.13", - "@vue/cli-plugin-pwa": "~4.5.13", - "@vue/cli-plugin-router": "~4.5.0", - "@vue/cli-service": "~4.5.13", + "@vue/cli-plugin-babel": "~4.5.15", + "@vue/cli-plugin-eslint": "~4.5.15", + "@vue/cli-plugin-pwa": "~4.5.15", + "@vue/cli-plugin-router": "~5.0.1", + "@vue/cli-service": "~4.5.15", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", - "eslint-plugin-vue": "^7.11.1", + "eslint-plugin-vue": "^7.20.0", "vue-template-compiler": "^2.6.14", - "workbox-webpack-plugin": "^6.1.5" + "workbox-webpack-plugin": "^6.5.1" }, "eslintConfig": { "root": true, diff --git a/frontend/src/assets/sounds/cash.mp3 b/frontend/src/assets/sounds/cash.mp3 new file mode 100644 index 0000000..ba09552 Binary files /dev/null and b/frontend/src/assets/sounds/cash.mp3 differ diff --git a/frontend/src/components/Card.vue b/frontend/src/components/Card.vue index c205f67..d1dd002 100644 --- a/frontend/src/components/Card.vue +++ b/frontend/src/components/Card.vue @@ -1,9 +1,9 @@ @@ -21,16 +21,26 @@ export default { if (!this.donotlocalize && this.$t(`cards.${this.card.name}.name`) !== `cards.${this.card.name}.name`) { return this.$t(`cards.${this.card.name}.name`) } + if (this.card.name == "you") { + return this.$t('you') + } return this.card.name }, + emoji(){ + return this.card.icon != "you" ? this.card.icon : this.$t('you') + }, suit() { if (this.card && !isNaN(this.card.suit)) { let x = ['♦️','♣️','♥️','♠️'] return x[this.card.suit]; + } else if (this.card.suit) { + return this.card.suit; } return ''; }, number() { + if (isNaN(this.card.suit)) + return this.card.number if (this.card.number === 1) return 'A' else if (this.card.number === 11) return 'J' else if (this.card.number === 12) return 'Q' @@ -107,6 +117,17 @@ export default { ); border: 2pt solid rgb(50 122 172); } +.card.brown.gold-rush { + box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa; + border: 2pt dotted #9C7340; +} +.card.black.gold-rush { + box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa; + border: 2pt dotted #000; +} +.card.back.gold-rush { + background: repeating-linear-gradient(347deg, #ffb32f, #987e51 ); +} .card h4 { position: absolute; text-align: center; diff --git a/frontend/src/components/Chat.vue b/frontend/src/components/Chat.vue index 2d1e63e..484e1de 100644 --- a/frontend/src/components/Chat.vue +++ b/frontend/src/components/Chat.vue @@ -5,12 +5,15 @@

{{msg.text}}

-

{{msg}}

.

+
+

{{msg.cmd}} {{msg.help}}

+
- +
@@ -23,6 +26,7 @@ import dynamite_sfx from '@/assets/sounds/dynamite.mp3' import prison_sfx from '@/assets/sounds/prison.mp3' import turn_sfx from '@/assets/sounds/turn.mp3' import death_sfx from '@/assets/sounds/death.mp3' +import cash_sfx from '@/assets/sounds/cash.mp3' export default { name: 'Chat', props: { @@ -32,7 +36,7 @@ export default { messages: [], text: '', spectators: 0, - commands: ['/debug'], + commands: [{cmd:'/debug', help:'Toggles the debug mode'}], }), computed: { commandSuggestion() { @@ -40,7 +44,7 @@ export default { if (this.text.length < 1) { return []; } - return this.commands.filter(x => x.slice(0, this.text.length) == this.text); + return this.commands.filter(x => x.cmd.slice(0, this.text.length) == this.text); }, }, sockets: { @@ -54,7 +58,7 @@ export default { } let params = msg.split('|') let type = params.shift().substring(1) - if (["flipped", "respond", "play_card", "play_card_against", "play_card_for", "spilled_beer", "diligenza", "wellsfargo", "saloon", "special_calamity"].indexOf(type) !== -1){ + if (["flipped", "respond", "play_card", "play_card_against", "play_card_for", "spilled_beer", "diligenza", "wellsfargo", "saloon", "special_calamity", 'won'].indexOf(type) !== -1){ params[1] = this.$t(`cards.${params[1]}.name`) } else if (type === "choose_character"){ params.push(this.$t(`cards.${params[1]}.desc`)) @@ -83,6 +87,8 @@ export default { (new Audio(dynamite_sfx)).play(); } else if (type == 'prison_turn') { (new Audio(prison_sfx)).play(); + } else if (type == 'purchase_card') { + (new Audio(cash_sfx)).play(); } else { (new Audio(notification_sfx)).play(); } @@ -115,6 +121,10 @@ export default { } e.preventDefault(); }, + fillCmd(cmd) { + this.text = cmd; + document.getElementById('my-msg').focus(); + } }, } @@ -136,6 +146,9 @@ input { height: 0pt; margin-top: -1.5pt; } +.std-text { + color: var(--font-color); +} .chat { display: flex; flex-direction: column; diff --git a/frontend/src/components/Chooser.vue b/frontend/src/components/Chooser.vue index 751c0f8..adfef5e 100644 --- a/frontend/src/components/Chooser.vue +++ b/frontend/src/components/Chooser.vue @@ -49,10 +49,12 @@ export default { //console.log(card) if (card.noDesc || card.name == null || card.name == "PewPew!") this.desc = "" - else if (card.desc) - this.desc = (this.$i18n.locale=='it'?card.desc:card.desc_eng) else if (card.is_character) this.desc = card.name + else if (card.goal) + this.desc = this.$t(`cards.${card.name}.name`) + else if (card.desc) + this.desc = (this.$i18n.locale=='it'?card.desc:card.desc_eng) else this.desc = this.$t(`cards.${card.name}.desc`) } @@ -62,6 +64,9 @@ export default { if (this.realCancelText == '') { this.realCancelText = this.$t('cancel') } + if (this.cards.length == 1) { + this.showDesc(this.cards[0]) + } if (this.playAudio) { (new Audio(show_sfx)).play(); } diff --git a/frontend/src/components/Deck.vue b/frontend/src/components/Deck.vue index 9da5e6f..dae3ea4 100644 --- a/frontend/src/components/Deck.vue +++ b/frontend/src/components/Deck.vue @@ -1,7 +1,17 @@