GhalaDev commited on
Commit
d20d1b8
·
verified ·
1 Parent(s): 81da6ac

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +293 -0
app.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI BoardGame Forge
3
+ A Gradio web app that turns any theme into a mini card game with
4
+ AI-generated artwork. Built for Hugging Face Spaces.
5
+ """
6
+
7
+ import random
8
+ import urllib.parse
9
+ from io import BytesIO
10
+
11
+ import requests
12
+ import gradio as gr
13
+ from PIL import Image, ImageDraw, ImageFont
14
+
15
+
16
+ # ------------------------------------------------------------
17
+ # Game text generator
18
+ # ------------------------------------------------------------
19
+
20
+ TITLE_TEMPLATES = [
21
+ "{theme} Wars",
22
+ "The {theme} Chronicles",
23
+ "Escape from {theme}",
24
+ "{theme}: The Card Game",
25
+ "Rise of the {theme}",
26
+ "{theme} Showdown",
27
+ "Legends of {theme}",
28
+ "{theme} Uprising",
29
+ ]
30
+
31
+ DESCRIPTION_TEMPLATES = [
32
+ "In this game, players take on the role of characters caught up in a {theme} scenario. Each player uses cards to outsmart their opponents and be the first to win.",
33
+ "Players compete in a world inspired by {theme}, drawing and playing cards to gain the upper hand. Strategy and a little luck decide the winner.",
34
+ "Set in a world inspired by {theme}, each player builds a small hand of powerful cards and battles to reach the win condition first.",
35
+ ]
36
+
37
+ RULES_TEMPLATE = """1. Each player starts with a hand of 4 cards.
38
+ 2. Players take turns playing one card at a time.
39
+ 3. When a card is played, follow the effect written on it.
40
+ 4. The game ends when the deck runs out or a player reaches the target score.
41
+ 5. The player with the most points, or the last one standing, wins the {theme} showdown."""
42
+
43
+ CARD_TYPES = ["Attack", "Defense", "Support", "Trick", "Special", "Boost", "Sabotage"]
44
+
45
+ NAME_PARTS = [
46
+ "Guardian", "Blade", "Shadow", "Spark", "Wanderer", "Sentinel",
47
+ "Rebel", "Trickster", "Champion", "Outcast", "Phantom", "Rogue",
48
+ ]
49
+
50
+ EFFECT_POOL = [
51
+ "Draw 2 extra cards this turn.",
52
+ "Block the next attack played against you.",
53
+ "Steal one card from an opponent's hand.",
54
+ "Skip your opponent's next turn.",
55
+ "Double your score this round.",
56
+ "Swap this card with any card in the discard pile.",
57
+ "Force all players to reveal their hand.",
58
+ "Gain +3 power for the rest of the game.",
59
+ "Discard your hand and draw a fresh one.",
60
+ "Copy the effect of the last card played.",
61
+ ]
62
+
63
+
64
+ def generate_game(theme):
65
+ """Builds a full mini card game (title, description, rules, 4 cards) from a theme."""
66
+ theme_clean = theme.strip().title()
67
+
68
+ title = random.choice(TITLE_TEMPLATES).format(theme=theme_clean)
69
+ description = random.choice(DESCRIPTION_TEMPLATES).format(theme=theme_clean)
70
+ rules = RULES_TEMPLATE.format(theme=theme_clean)
71
+
72
+ chosen_types = random.sample(CARD_TYPES, 4)
73
+ cards = []
74
+ for card_type in chosen_types:
75
+ name = f"{theme_clean} {random.choice(NAME_PARTS)}"
76
+ effect = random.choice(EFFECT_POOL)
77
+ cards.append({"name": name, "type": card_type, "effect": effect})
78
+
79
+ return {"title": title, "description": description, "rules": rules, "cards": cards}
80
+
81
+
82
+ # ------------------------------------------------------------
83
+ # AI artwork generator
84
+ # ------------------------------------------------------------
85
+
86
+ TYPE_COLORS = {
87
+ "Attack": {"bg": (178, 34, 34), "border": (255, 215, 0)},
88
+ "Defense": {"bg": (30, 60, 114), "border": (192, 192, 192)},
89
+ "Support": {"bg": (34, 87, 47), "border": (200, 255, 200)},
90
+ "Trick": {"bg": (75, 0, 130), "border": (218, 165, 32)},
91
+ "Special": {"bg": (139, 69, 19), "border": (255, 223, 0)},
92
+ "Boost": {"bg": (204, 85, 0), "border": (255, 255, 255)},
93
+ "Sabotage": {"bg": (20, 20, 20), "border": (139, 0, 0)},
94
+ }
95
+
96
+
97
+ def get_font(size, bold=True):
98
+ """Loads a common system font. Falls back to a basic font if not found."""
99
+ font_names = (
100
+ ["DejaVuSans-Bold.ttf", "Arial Bold.ttf", "arialbd.ttf"]
101
+ if bold
102
+ else ["DejaVuSans.ttf", "Arial.ttf", "arial.ttf"]
103
+ )
104
+ for name in font_names:
105
+ try:
106
+ return ImageFont.truetype(name, size)
107
+ except Exception:
108
+ continue
109
+ return ImageFont.load_default()
110
+
111
+
112
+ def make_placeholder_art(card):
113
+ """A simple backup image used only if the AI art request fails."""
114
+ colors = TYPE_COLORS.get(card["type"], {"bg": (100, 100, 100)})
115
+ img = Image.new("RGB", (512, 512), colors["bg"])
116
+ draw = ImageDraw.Draw(img)
117
+ font = get_font(40)
118
+ draw.text((40, 236), card["type"] + " Card", font=font, fill=(255, 255, 255))
119
+ return img
120
+
121
+
122
+ def generate_card_art(theme, card):
123
+ """Builds an image prompt and fetches free AI art from Pollinations.ai."""
124
+ prompt = (
125
+ f"{theme} {card['name']}, {card['type']} character, "
126
+ f"fantasy trading card illustration, digital painting, detailed, vibrant colors, centered"
127
+ )
128
+ encoded_prompt = urllib.parse.quote(prompt)
129
+ seed = random.randint(1, 999999)
130
+ url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=512&height=512&seed={seed}&nologo=true"
131
+
132
+ try:
133
+ response = requests.get(url, timeout=60)
134
+ response.raise_for_status()
135
+ return Image.open(BytesIO(response.content)).convert("RGB")
136
+ except Exception as error:
137
+ print(f"Could not fetch AI art for '{card['name']}', using a placeholder instead. Reason: {error}")
138
+ return make_placeholder_art(card)
139
+
140
+
141
+ # ------------------------------------------------------------
142
+ # Card designer
143
+ # ------------------------------------------------------------
144
+
145
+ def wrap_text(text, font, max_width, draw):
146
+ """Breaks a string into multiple lines so it fits inside max_width pixels."""
147
+ words = text.split()
148
+ lines = []
149
+ current = ""
150
+ for word in words:
151
+ test_line = (current + " " + word).strip()
152
+ box = draw.textbbox((0, 0), test_line, font=font)
153
+ if box[2] - box[0] <= max_width:
154
+ current = test_line
155
+ else:
156
+ if current:
157
+ lines.append(current)
158
+ current = word
159
+ if current:
160
+ lines.append(current)
161
+ return lines
162
+
163
+
164
+ def draw_card(card, art_image, theme):
165
+ """Composes the final card image: background, artwork, title bar, type badge, effect text."""
166
+ width, height = 500, 700
167
+ colors = TYPE_COLORS.get(card["type"], {"bg": (60, 60, 60), "border": (255, 255, 255)})
168
+
169
+ card_img = Image.new("RGB", (width, height), colors["bg"])
170
+ draw = ImageDraw.Draw(card_img)
171
+
172
+ draw.rectangle([0, 0, width - 1, height - 1], outline=colors["border"], width=10)
173
+
174
+ title_font = get_font(28)
175
+ draw.rectangle([15, 15, width - 15, 80], fill=(0, 0, 0))
176
+ title_box = draw.textbbox((0, 0), card["name"], font=title_font)
177
+ title_x = (width - (title_box[2] - title_box[0])) / 2
178
+ draw.text((title_x, 25), card["name"], font=title_font, fill=(255, 255, 255))
179
+
180
+ art_box = (20, 90, width - 20, 430)
181
+ art_resized = art_image.resize((art_box[2] - art_box[0], art_box[3] - art_box[1]))
182
+ card_img.paste(art_resized, (art_box[0], art_box[1]))
183
+ draw.rectangle(art_box, outline=colors["border"], width=4)
184
+
185
+ type_font = get_font(20)
186
+ draw.rectangle([20, 440, 200, 480], fill=colors["border"])
187
+ draw.text((30, 448), card["type"], font=type_font, fill=(0, 0, 0))
188
+
189
+ effect_font = get_font(18, bold=False)
190
+ draw.rectangle([20, 500, width - 20, height - 40], fill=(255, 255, 255))
191
+ lines = wrap_text(card["effect"], effect_font, width - 60, draw)
192
+ y = 515
193
+ for line in lines:
194
+ draw.text((35, y), line, font=effect_font, fill=(0, 0, 0))
195
+ y += 24
196
+
197
+ footer_font = get_font(13, bold=False)
198
+ draw.text((25, height - 30), theme.title(), font=footer_font, fill=colors["border"])
199
+
200
+ return card_img
201
+
202
+
203
+ # ------------------------------------------------------------
204
+ # Full pipeline
205
+ # ------------------------------------------------------------
206
+
207
+ def forge_full_game(theme):
208
+ """Full pipeline: theme -> game text -> AI artwork -> designed card images."""
209
+ print(f"Generating game for theme: {theme}")
210
+ game_data = generate_game(theme)
211
+ print(f"Game created: {game_data['title']}")
212
+
213
+ card_images = []
214
+ for i, card in enumerate(game_data["cards"], start=1):
215
+ print(f"Generating artwork for card {i} of 4: {card['name']}")
216
+ art = generate_card_art(theme, card)
217
+ card_img = draw_card(card, art, theme)
218
+ card_images.append(card_img)
219
+
220
+ print("Finished generating game and cards.")
221
+ return game_data, card_images
222
+
223
+
224
+ # ------------------------------------------------------------
225
+ # Gradio web app
226
+ # ------------------------------------------------------------
227
+
228
+ EXAMPLE_THEMES = [
229
+ "Space Cats",
230
+ "Ancient Pirates",
231
+ "Escaping a Corporate Office",
232
+ "Medieval Kingdom",
233
+ "Desert Survivors",
234
+ "Robot Uprising",
235
+ ]
236
+
237
+
238
+ def build_game(theme):
239
+ """Connected to the Generate button. Runs the pipeline and formats the output."""
240
+ if not theme or not theme.strip():
241
+ return "Please enter a theme first.", []
242
+
243
+ game_data, card_images = forge_full_game(theme)
244
+
245
+ info_text = (
246
+ f"## {game_data['title']}\n\n"
247
+ f"**Description:** {game_data['description']}\n\n"
248
+ f"**Rules:**\n\n{game_data['rules']}"
249
+ )
250
+
251
+ gallery_items = [
252
+ (img, card["name"]) for img, card in zip(card_images, game_data["cards"])
253
+ ]
254
+
255
+ return info_text, gallery_items
256
+
257
+
258
+ with gr.Blocks(title="AI BoardGame Forge") as demo:
259
+ gr.Markdown("# AI BoardGame Forge")
260
+ gr.Markdown(
261
+ "Enter a theme, or select one of the example themes below, "
262
+ "then click Generate Game to create a mini card game with AI-generated artwork."
263
+ )
264
+
265
+ theme_input = gr.Textbox(
266
+ label="Theme",
267
+ placeholder="e.g. Space Cats, Ancient Pirates, Escaping a Corporate Office",
268
+ )
269
+
270
+ gr.Examples(
271
+ examples=EXAMPLE_THEMES,
272
+ inputs=theme_input,
273
+ label="Example themes",
274
+ )
275
+
276
+ generate_button = gr.Button("Generate Game")
277
+
278
+ game_info_output = gr.Markdown()
279
+ card_gallery_output = gr.Gallery(
280
+ label="Generated Cards",
281
+ columns=4,
282
+ height="auto",
283
+ )
284
+
285
+ generate_button.click(
286
+ fn=build_game,
287
+ inputs=theme_input,
288
+ outputs=[game_info_output, card_gallery_output],
289
+ )
290
+
291
+
292
+ if __name__ == "__main__":
293
+ demo.launch()