Compare commits

...

15 Commits

Author SHA1 Message Date
Alberto Xamin
4d2a222f93
fix replacement 2024-12-30 17:29:30 +01:00
Alberto Xamin
61fa295f8d
Update docker-image.yml 2024-12-30 17:23:51 +01:00
Alberto Xamin
0a2bbce2da
Update docker-image.yml 2024-12-30 17:13:44 +01:00
Alberto Xamin
6bc10f1bff
Update docker-image.yml 2024-12-30 16:16:12 +01:00
Alberto Xamin
123e569010
speedup image compile 2024-12-30 16:10:12 +01:00
Alberto Xamin
b45d04bf30
allow keeping owner 2024-10-29 16:37:04 +00:00
Alberto Xamin
b2b9eac1cd
really fix bandidos 2024-10-29 15:25:31 +00:00
Alberto Xamin
345e04ef06
fix big spencer 2024-10-22 15:43:24 +01:00
Alberto Xamin
2af8e5ec54
fxi bandidos 2024-10-22 15:41:16 +01:00
Alberto Xamin
3642c0dbb5
fix blood brothers desc 2024-10-22 15:37:25 +01:00
Alberto Xamin
86441f92c8
fix tornado 2024-10-22 15:36:44 +01:00
Alberto Xamin
df65a97225
fix disabled cursor 2024-07-03 10:12:14 +01:00
Alberto Xamin
d2bddb9eda
fix broken card descriptions 2024-06-26 14:08:17 +03:00
Alberto Xamin
dd84d91b43
Merge pull request #507 from albertoxamin/train-robbery
add better info screen
2024-06-15 18:59:41 +03:00
Alberto Xamin
bae85b7508
add better info screen 2024-06-15 18:58:06 +03:00
10 changed files with 393 additions and 46 deletions

View File

@ -4,20 +4,23 @@ on:
branches: main
jobs:
buildx:
build-platform:
runs-on: ubuntu-latest
strategy:
matrix:
platform: [linux/amd64, linux/arm/v7, linux/arm64/v8]
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Cache Docker layers
- name: Cache Docker layers
uses: actions/cache@v4
id: cache
with:
@ -25,27 +28,44 @@ jobs:
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
-
name: Login to DockerHub
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
- name: Prepare Platform Tag
id: platform_tag
run: echo "platform_tag=$(echo ${{ matrix.platform }} | sed 's|/|-|g')" >> $GITHUB_ENV
- name: Build and push platform-specific image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm/v7,linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: albertoxamin/bang:latest
cache-from: type=registry,ref=user/app:latest
tags: albertoxamin/bang:${{ env.platform_tag }}
cache-from: type=registry,ref=albertoxamin/bang:${{ env.platform_tag }}
cache-to: type=inline
- 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 }}
create-manifest:
runs-on: ubuntu-latest
needs: build-platform
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
docker buildx imagetools create \
--tag albertoxamin/bang:latest \
albertoxamin/bang:linux-amd64 \
albertoxamin/bang:linux-arm-v7 \
albertoxamin/bang:linux-arm64-v8

View File

@ -9,11 +9,61 @@ class DodgeCity():
from bang.expansions.dodge_city import cards
return cards.get_starting_deck()
def get_expansion_info(self):
return {
"id": "dodge_city",
"name": "Dodge City",
"cards": [
{"type": "characters", "cards": DodgeCity.get_characters()},
{"type": "cards", "cards": DodgeCity.get_cards()}
]
}
class HighNoon():
def get_events():
from bang.expansions.high_noon import card_events
return card_events.get_all_events() + [card_events.get_endgame_card()]
def get_expansion_info(self):
return {
"id": "high_noon",
"name": "High Noon",
"cards": [
{"type": "events", "cards": HighNoon.get_events()}
]
}
class FistfulOfCards():
def get_events():
from bang.expansions.fistful_of_cards import card_events
return card_events.get_all_events() + [card_events.get_endgame_card()]
def get_expansion_info(self):
return {
"id": "fistful_of_cards",
"name": "Fistful of Cards",
"cards": [
{"type": "events", "cards": FistfulOfCards.get_events()}
]
}
class GoldRush():
def get_characters():
from bang.expansions.gold_rush import characters
return characters.all_characters()
def get_shop_cards():
from bang.expansions.gold_rush import shop_cards
return shop_cards.get_cards()
def get_expansion_info(self):
return {
"id": "gold_rush",
"name": "Gold Rush",
"cards": [
{"type": "characters", "cards": GoldRush.get_characters()},
{"type": "cards", "cards": GoldRush.get_shop_cards()}
]
}
class TheValleyOfShadows():
def get_characters():
from bang.expansions.the_valley_of_shadows import characters
@ -23,7 +73,74 @@ class TheValleyOfShadows():
from bang.expansions.the_valley_of_shadows import cards
return cards.get_starting_deck()
def get_expansion_info(self):
return {
"id": "the_valley_of_shadows",
"name": "The Valley of Shadows",
"cards": [
{"type": "characters", "cards": TheValleyOfShadows.get_characters()},
{"type": "cards", "cards": TheValleyOfShadows.get_cards()}
]
}
class WildWestShow():
def get_characters():
from bang.expansions.wild_west_show import characters
return characters.all_characters()
def get_events():
from bang.expansions.wild_west_show import card_events
return card_events.get_all_events() + [card_events.get_endgame_card()]
def get_expansion_info(self):
return {
"id": "wild_west_show",
"name": "Wild West Show",
"cards": [
{"type": "characters", "cards": WildWestShow.get_characters()},
{"type": "events", "cards": WildWestShow.get_events()}
]
}
class TrainRobbery():
def get_stations():
from bang.expansions.train_robbery import stations
return stations.get_all_stations()
def get_trains():
from bang.expansions.train_robbery import trains
return trains.get_all_cards() + trains.get_locomotives()
def get_expansion_info(self):
return {
"id": "train_robbery",
"name": "Train Robbery",
"cards": [
{"type": "stations", "cards": TrainRobbery.get_stations()},
{"type": "trains", "cards": TrainRobbery.get_trains()}
]
}
def get_expansion_info(expansion_id):
expansion_map = {
"dodge_city": DodgeCity(),
"high_noon": HighNoon(),
"fistful_of_cards": FistfulOfCards(),
"gold_rush": GoldRush(),
"the_valley_of_shadows": TheValleyOfShadows(),
"wild_west_show": WildWestShow(),
"train_robbery": TrainRobbery()
}
expansion_info = expansion_map[expansion_id].get_expansion_info()
for section in expansion_info["cards"]:
unique_cards = []
seen_card = set()
for card in section["cards"]:
if card.name not in seen_card:
unique_cards.append(card)
seen_card.add(card.name)
section["cards"] = unique_cards
return expansion_info

View File

@ -122,6 +122,7 @@ class Game:
self.rpc_log = []
self.is_replay = False
self.replay_speed = 1
self.owner: str | None = None
def shuffle_players(self):
if not self.started:
@ -254,6 +255,7 @@ class Game:
"available_expansions": self.available_expansions,
"is_replay": self.is_replay,
"characters_to_distribute": self.characters_to_distribute,
"owner": self.owner,
},
)
G.sio.emit("debug", room=self.name, data=self.debug)
@ -308,6 +310,8 @@ class Game:
if player.is_admin():
self.feature_flags()
self.players.append(player)
if len(self.players) == 1:
self.owner = player.name
if len(self.players) > 7:
if "dodge_city" not in self.expansions:
self.expansions.append("dodge_city")
@ -474,6 +478,10 @@ class Game:
if p.get_discarded(attacker=attacker, card_name=card_name):
self.waiting_for += 1
p.notify_self()
elif card_name == "Tornado" and len(p.hand) == 0:
self.deck.draw(player=p)
self.deck.draw(player=p)
p.notify_self()
if self.waiting_for == 0:
attacker.pending_action = PendingAction.PLAY
attacker.notify_self()
@ -985,6 +993,8 @@ class Game:
self.deck = None
return True
else:
self.owner = next((p.name for p in self.players if not p.is_bot), None)
self.notify_room()
return False
def player_death(self, player: pl.Player, disconnected=False):

View File

@ -1574,6 +1574,10 @@ class Player:
self.game.deck.scrap(self.hand.pop(card_index), player=self)
self.mancato_needed -= 1
else:
if self.attacker and "gold_rush" in self.game.expansions and not self.is_ghost:
if isinstance(self.attacker, Player):
self.attacker.gold_nuggets += 1
self.attacker.notify_self()
self.lives -= 1
self.mancato_needed = 0
if self.mancato_needed <= 0:
@ -1868,7 +1872,8 @@ class Player:
):
self.expected_response.append(cs.Bang(0, 0).name)
if self.character.check(self.game, chw.BigSpencer):
self.expected_response = []
self.expected_response = self.game.deck.mancato_cards.copy()
self.expected_response.remove(cs.Mancato(0, 0).name)
if any((isinstance(c, trt.Caboose) for c in self.equipment)):
self.expected_response.append([c.name for c in self.equipment if not c.usable_next_turn])
self.on_failed_response_cb = self.take_damage_response
@ -1942,6 +1947,7 @@ class Player:
self.choose_text = "choose_poker"
if card_name == "Bandidos":
self.choose_text = "choose_bandidos"
self.attacker = attacker
self.mancato_needed = min(2, len(self.hand))
self.available_cards.append(
{"name": "-1hp", "icon": "💔", "noDesc": True}

View File

@ -1326,6 +1326,12 @@ def get_trainrobberycards(sid):
}, default=lambda o: o.__dict__)
)
@sio.event
@bang_handler
def get_expansion_info(sid, id):
from bang.expansions import get_expansion_info
sio.emit("expansion_info", room=sid, data=json.dumps(get_expansion_info(id), default=lambda o: o.__dict__))
@sio.event
@bang_handler
def discord_auth(sid, data):

View File

@ -226,6 +226,29 @@ def test_bandidos():
assert p1.pending_action == PendingAction.WAIT
assert p.pending_action == PendingAction.PLAY
def test_bandidos_with_gold_rush():
g = Game('test')
g.expansions = ['gold_rush']
ps = [Player(f'p{i}', f'p{i}') 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)
p = g.players[g.turn]
p1 = g.players[(g.turn+1)%3]
p.draw('')
p.hand = [Bandidos(0,0), Bandidos(0,0)]
p.play_card(0)
assert len(p.hand) == 1
assert p.pending_action == PendingAction.WAIT
assert p1.pending_action == PendingAction.CHOOSE
p1.choose(len(p1.hand))
assert p1.lives == 3
assert p.pending_action == PendingAction.PLAY
assert p.gold_nuggets == 1
# test Poker
def test_poker():
g = Game('test')

View File

@ -41,12 +41,18 @@
</div>
</div>
<transition name="list">
<p v-if="eventCard" class="center-stuff"><b>{{eventDesc}}</b></p>
<p v-if="eventCard" class="center-stuff">🔥 <b>{{eventDesc}}</b> 🎴</p>
</transition>
<transition name="list">
<p v-if="eventCardWildWestShow && !eventCardWildWestShow.back" class="center-stuff">🎪 <b>{{eventDescWildWestShow}}</b> 🎪</p>
</transition>
<transition name="list">
<div v-if="goldRushDesc">
<p class="center-stuff">🤑 <i>{{$t(`cards.${goldRushDesc.name}.desc`)}}</i> 🤑</p>
<p class="center-stuff">🤑 <b>{{goldRushDesc.number - gold_rush_discount}} 💵</b> 🤑</p>
</div>
</transition>
<transition name="list">
<div v-if="stationDesc">
<p class="center-stuff"><i>{{stationDesc}}</i></p>
</div>
@ -216,7 +222,7 @@ export default {
this.stationDesc = this.$t(`cards.${this.currentStations[index].name}.desc`)
const trainPiece = this.trainPieceForStation(index)
if (trainPiece) {
this.stationDesc += '\n\n🚂' + this.$t(`cards.${trainPiece.name}.desc`)
this.stationDesc += '\n\n🚂' + this.$t(`cards.${trainPiece.name}.desc`) + '🚋'
}
},
},

View File

@ -0,0 +1,117 @@
<template>
<div class="popup-overlay" v-if="show" @click="handleOverlayClick">
<div class="popup-content" @click.stop>
<button class="close-button" @click="close">×</button>
<h2>{{ expansion.name }}</h2>
<div v-for="section in expansion.cards" :key="section.type" class="section">
<h3>{{ section.type }}</h3>
<div class="cards-container flexy-cards-wrapper">
<div v-for="card in section.cards" :key="card.name" class="flexy-cards">
<Card :card="card" v-if="section.type !== 'stations'" :class="getClass(expansion, section)"/>
<StationCard :card="card" :price="card.price" v-else-if="section.type === 'stations'"/>
<div style="margin-left:6pt;">
<p>{{$t(`cards.${card.name}.desc`)}}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Card from '@/components/Card.vue';
import StationCard from './StationCard.vue';
export default {
props: {
show: Boolean,
expansion: Object, // Expecting an object with id, name, and cards
},
components: {
Card,
StationCard,
},
methods: {
close() {
this.$emit('close');
},
handleOverlayClick() {
this.close();
},
getClass(expansion, section) {
let classes = ''
if (section.type == 'events') {
classes += 'last-event';
}
if (expansion.id == 'fistful_of_cards') {
classes += ' fistful-of-cards';
} else if (expansion.id == 'high_noon') {
classes += ' high-noon';
} else if (expansion.id == 'gold_rush') {
classes += ' gold-rush';
} else if (expansion.id == 'train_robbery') {
classes += ' train-robbery';
} else if (expansion.id == 'the_valley_of_shadows') {
classes += ' valley-of-shadows';
} else if (expansion.id == 'wild_west_show') {
classes += ' wild-west-show';
}
console.log(classes);
return classes;
}
},
};
</script>
<style scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.popup-content {
position: relative;
background: white;
padding: 20px;
border-radius: 5px;
max-width: 80%;
max-height: 80%;
overflow-y: auto;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
.section {
margin-bottom: 20px;
}
.cards-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card {
box-sizing: border-box;
margin-bottom: 10px;
}
.flexy-cards-wrapper {
display: flex;
flex-flow: wrap;
}
.flexy-cards {
flex: 30%;
display:flex;
}
</style>

View File

@ -195,18 +195,20 @@
<p v-else style="min-height: 19px"></p>
<h3>{{ $t("expansions") }}</h3>
<div class="players-table" style="justify-content: flex-start">
<card
v-for="ex in expansionsStatus"
v-bind:key="ex.id"
:id="ex.id"
:card="ex.card"
:class="{
'cant-play': !ex.enabled,
...ex.card.classes,
}"
:donotlocalize="true"
@click.native="toggleExpansions(ex.id)"
/>
<div v-for="ex in expansionsStatus" :key="ex.id" class="expansion-card" style="position: relative">
<card
:id="ex.id"
:card="ex.card"
:class="{
'cant-play': !ex.enabled,
...ex.card.classes,
}"
style="cursor: pointer"
:donotlocalize="true"
@click.native="toggleExpansions(ex.id)"
/>
<button class="info-button" @click="showExpansionInfo(ex.id)">?</button>
</div>
</div>
<p v-if="isRoomOwner">{{ $t("click_to_toggle") }}</p>
<h3>{{ $t("mods") }}</h3>
@ -391,6 +393,13 @@
:playerRole="deadRoleData.role"
/>
</transition>
<transition name="bounce">
<ExpansionPopup
:show="showPopup"
:expansion="selectedExpansionInfo"
@close="closePopup"
/>
</transition>
</div>
</template>
@ -410,6 +419,7 @@ import AnimatedCard from "./AnimatedCard.vue";
import { emojiMap } from "@/utils/emoji-map.js";
import { expansionsMap } from "@/utils/expansions-map.js";
import AnimatedEffect from './AnimatedEffect.vue';
import ExpansionPopup from '@/components/ExpansionPopup.vue';
const cumulativeOffset = function (element) {
var top = 0,
@ -440,6 +450,7 @@ export default {
DeadRoleNotification,
AnimatedCard,
AnimatedEffect,
ExpansionPopup,
},
data: () => ({
username: "",
@ -470,8 +481,14 @@ export default {
cardsToAnimate: [],
characters_to_distribute: 2,
fullScreenEffects: [],
showPopup: false,
selectedExpansionInfo: {},
owner: undefined,
}),
sockets: {
expansion_info(data) {
this.selectedExpansionInfo = JSON.parse(data);
},
room(data) {
this.lobbyName = data.name;
if (!data.started) {
@ -487,6 +504,7 @@ export default {
this.togglable_expansions = data.available_expansions;
this.expansions = data.expansions;
this.is_replay = data.is_replay;
this.owner = data.owner;
this.characters_to_distribute = data.characters_to_distribute;
this.players = data.players.map((x) => {
return {
@ -713,11 +731,7 @@ export default {
return "";
},
isRoomOwner() {
if (this.players.length > 0) {
let pls = this.players.filter((x) => !x.is_bot);
return pls.length > 0 && pls[0].name == this.username;
}
return false;
return this.owner === this.username;
},
startGameCard() {
if (!this.started && this.players.length > 2 && this.isRoomOwner) {
@ -755,6 +769,14 @@ export default {
},
},
methods: {
showExpansionInfo(id) {
this.showPopup = true;
this.$socket.emit("get_expansion_info", id);
},
closePopup() {
this.showPopup = false;
this.selectedExpansionCards = [];
},
getExpansionCard(id) {
let ex = expansionsMap[id];
ex.classes = {
@ -1050,4 +1072,24 @@ export default {
border-bottom: dashed #ccc2;
}
}
.info-button {
position: absolute;
top: 5px;
right: 5px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
}
.info-button:hover {
background-color: #0056b3;
}
</style>

View File

@ -542,7 +542,7 @@
},
"Fratelli Di Sangue": {
"name": "Blood Brothers",
"desc": "At the begin of their turn, payers can lose 1 hp (except the last one) to give it to another player"
"desc": "At the begin of their turn, players can lose 1 hp (except the last one) to give it to another player"
},
"I Dalton": {
"name": "The Daltons",