From d4c45c02890143fb8cfbc60be9d2b79240d9ad8e Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Sun, 14 Jul 2024 15:49:48 -0500 Subject: Initial commit --- main.py | 79 ++++++++++ my_resource.pyxres | Bin 0 -> 8145 bytes orb_of_fortuity.pyxapp | Bin 0 -> 41227 bytes rounds.py | 270 ++++++++++++++++++++++++++++++++++ states.py | 382 +++++++++++++++++++++++++++++++++++++++++++++++++ util.py | 9 ++ 6 files changed, 740 insertions(+) create mode 100644 main.py create mode 100644 my_resource.pyxres create mode 100644 orb_of_fortuity.pyxapp create mode 100644 rounds.py create mode 100644 states.py create mode 100644 util.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..f24f4c6 --- /dev/null +++ b/main.py @@ -0,0 +1,79 @@ +import random +import states +import string +import pyxel +from rounds import ROUNDS, END_DIALOG + + +class App: + def __init__(self): + pyxel.init(64, 64) + pyxel.load("my_resource.pyxres") + + self.guessed = set() + self.frame = 0 + self.round = -1 + + # Generate a random mix of fake letters + self.glyph_map = {} + for key in list(string.ascii_uppercase): + if random.random() < 0.4: + self.glyph_map[key] = 1 + else: + self.glyph_map[key] = 0 + + self.set_state(states.TitleScreen) + + pyxel.run(self.update, self.draw) + + + def next_round(self, winner=None): + self.guessed = set() + self.round += 1 + pyxel.stop() + if self.round == 5: + pyxel.playm(3, loop=True) + # TODO special for grand prize. + self.set_state( + states.Dialog, dialog=END_DIALOG, + is_end=True, next=states.Credits + ) + else: + current_round = ROUNDS[self.round] + phrase_obj = random.choice(current_round["phrases"]) + self.outcomes = current_round["wheel_outcomes"] + self.phrase = phrase_obj["phrase"] + self.category = phrase_obj["category"] + if winner: + current_round["dialog"].insert(0, + { + "who": "camera", + "text": f"Congrats, {winner}!", + }, + ) + + current_round["dialog"].append( + { + "who": "camera", + "text": f"The category is: {self.category}.", + }, + ) + self.set_state(states.Dialog, dialog=current_round["dialog"]) + song = 2 if self.round < 2 else 1 + pyxel.playm(song, loop=True) + + def set_state(self, new_state_class, **kwargs): + self.state_change_at = self.frame + kwargs["app"] = self + self.state = new_state_class(**kwargs) + + def update(self): + self.frame += 1 + self.state.update(self) + + def draw(self): + pyxel.cls(0) + self.state.draw(self) + + +App() diff --git a/my_resource.pyxres b/my_resource.pyxres new file mode 100644 index 0000000..f9a67da Binary files /dev/null and b/my_resource.pyxres differ diff --git a/orb_of_fortuity.pyxapp b/orb_of_fortuity.pyxapp new file mode 100644 index 0000000..22483ef Binary files /dev/null and b/orb_of_fortuity.pyxapp differ diff --git a/rounds.py b/rounds.py new file mode 100644 index 0000000..561e697 --- /dev/null +++ b/rounds.py @@ -0,0 +1,270 @@ +ROUNDS = [ + { + "dialog": [ + { + "who": "camera", + "text": "Greetings! Wel- come back to your favorite", + }, + { + "who": "camera", + "text": "show ... the ORB of FORTUITY!", + }, + { + "who": "camera", + "text": "Our first contestant is...YOU! ", + }, + { + "who": "you", + "text": "Uh, where am I exactly? I must've fallen", + }, + { + "who": "you", + "text": "asleep watching TV at the dentist.", + }, + { + "who": "camera", + "text": "And our prior Orb Champion, THEM!", + }, + { + "who": "them", + "text": "Glad to be back.", + }, + { + "who": "camera", + "text": "Today our contests are playing for...", + }, + { + "who": "camera", + "text": "... the GRAND PRIZE!", + }, + { + "who": "camera", + "text": "Now it's time for YOU to spin the orb!", + }, + ], + "wheel_outcomes": [ + "$100", "$200", "$300", "$400", + ], + "phrases": [ + { + "phrase": ["DONT", "PANIC"], + "category": "Quotes", + }, + { + "phrase": ["MAKE", "IT", "SO"], + "category": "Quotes", + }, + { + "phrase": ["PHONE", "HOME"], + "category": "Quotes", + }, + ], + }, + { + "dialog": [ + { + "who": "you", + "text": "That felt familiar...", + }, + { + "who": "them", + "text": "That's an odd 'feeling',..", + }, + { + "who": "them", + "text": "considering I am the champion.", + }, + { + "who": "camera", + "text": "We must move on! Four more rounds.", + }, + ], + "wheel_outcomes": [ + "$200", "$400", "$600", "$800", + ], + "phrases": [ + { + "phrase": ["DONT", "BLINK"], + "category": "Ominous phrases", + }, + { + "phrase": ["HELP", "IM", "TRAPPED"], + "category": "Ominous phrases", + }, + { + "phrase": ["LOOK", "BEHIND", "YOU"], + "category": "Ominous phrases", + }, + ], + }, + { + "dialog": [ + { + "who": "you", + "text": "What exactly are the rules to this game?", + }, + { + "who": "them", + "text": "What do you mean by rules?", + }, + { + "who": "you", + "text": "What determines who wins?", + }, + { + "who": "camera", + "text": "Don't worry! Just spin that orb", + }, + { + "who": "camera", + "text": "...and watch out for the penalties!", + }, + { + "who": "you", + "text": "That's the type of rule I want to know about!", + }, + { + "who": "camera", + "text": "Round three!", + }, + ], + "wheel_outcomes": [ + "$300", "$600", "$900", "$1200", + ], + "phrases": [ + { + "phrase": ["BATTLE" "OF", "XEANII"], + "category": "Historic Events", + }, + { + "phrase": ["JOXIAN", "EMPIRE"], + "category": "Historic Events", + }, + { + "phrase": ["OMEGDONI", "ERUPTION"], + "category": "Historic Events", + }, + ], + }, + { + "dialog": [ + { + "who": "you", + "text": "What kind of history was that?", + }, + { + "who": "them", + "text": "Didn't you learn about it in your...", + }, + { + "who": "them", + "text": "ancient history class?", + }, + { + "who": "you", + "text": "I must've been sick that day.", + }, + { + "who": "you", + "text": "Wait, what happened to THEM?", + }, + { + "who": "camera", + "text": "Just some minor technical difficulties.", + }, + { + "who": "camera", + "text": "The production team is working on it.", + }, + { + "who": "camera", + "text": "We're almost done, round four!", + }, + ], + "wheel_outcomes": [ + "$400", "$800", "$1200", "$1600", + ], + "phrases": [ + { + "phrase": ["GLORBIUS", "WORLD", "EATER"], + "category": "\"Things\"", + }, + { + "phrase": ["DEATH", "MADE", "FLESH"], + "category": "\"Things\"", + }, + { + "phrase": ["THE", "WARLOCK", "PLOYON"], + "category": "\"Things\"", + }, + ], + }, + { + "dialog": [ + { + "who": "you", + "text": "Something is very wrong.", + }, + { + "who": "you", + "text": "What language was that?", + }, + { + "who": "camera", + "text": "You'll understand soon enough.", + }, + { + "who": "you", + "text": "I'm not sure I want to play anymore", + }, + { + "who": "camera", + "text": "It's not up to you.", + }, + ], + "wheel_outcomes": [ + "$500", "$1000", "$1500", "$2000", "GRAND PRIZE" + ], + "phrases": [ + { + "phrase": ["MEET", "YOUR", "DOOM"], + "category": "YOUR LAST WORDS", + }, + ], + }, +] + +END_DIALOG = [ + { + "who": "you", + "text": "Did I win?", + }, + { + "who": "you", + "text": "I was doing really well.", + }, + { + "who": "camera", + "text": "After tallying the points....", + }, + { + "who": "camera", + "text": "YOU are not the winner.", + }, + { + "who": "you", + "text": "Does that mean I can leave now?", + }, + { + "who": "camera", + "text": "Thanks for tuning in folks!", + }, + { + "who": "camera", + "text": "We'll see you next time", + }, + { + "who": "camera", + "text": "on ORB OF FORTUITY!", + }, +] diff --git a/states.py b/states.py new file mode 100644 index 0000000..c055965 --- /dev/null +++ b/states.py @@ -0,0 +1,382 @@ +import pyxel +import random +import math +import string +import util + +class State: + def __init__(self, **kwargs): + pass + + def update(self, app): + pass + + def draw(self, app): + pass + +VALID_GUESSES = { + pyxel.KEY_A: "A", pyxel.KEY_B: "B", pyxel.KEY_C: "C", + pyxel.KEY_D: "D", pyxel.KEY_E: "E", pyxel.KEY_F: "F", + pyxel.KEY_G: "G", pyxel.KEY_H: "H", pyxel.KEY_I: "I", + pyxel.KEY_J: "J", pyxel.KEY_K: "K", pyxel.KEY_L: "L", + pyxel.KEY_M: "M", pyxel.KEY_N: "N", pyxel.KEY_O: "O", + pyxel.KEY_P: "P", pyxel.KEY_Q: "Q", pyxel.KEY_R: "R", + pyxel.KEY_S: "S", pyxel.KEY_T: "T", pyxel.KEY_U: "U", + pyxel.KEY_V: "V", pyxel.KEY_W: "W", pyxel.KEY_X: "X", + pyxel.KEY_Y: "Y", pyxel.KEY_Z: "Z", +} + +class TitleScreen(State): + def __init__(self, **kwargs): + self.next_state_counter = 0 + self.sent_input = False + pyxel.stop() + pyxel.playm(0, loop=True) + + def update(self, app): + if self.next_state_counter != 0: + self.next_state_counter += 1 + if self.next_state_counter == 30: + app.next_round() + if not self.sent_input and util.if_continue_button(): + self.next_state_counter += 1 + pyxel.play(1, 0) + self.sent_input = True + + def draw(self, app): + pyxel.rect(0, 0, 64, 64, 11) + pyxel.blt(0, 0, 0, 0, 32, 64, 64, 11) + center_x = 32 + center_y = 75 + radius = 40 + time_adjust = 20 + colors = [5, 8, 10, 2] + for i in range(19): + portion = (360.0 / 18) / 3 + t0 = app.frame - self.next_state_counter + portion * i + t1 = t0 / time_adjust + t2 = (t0 + portion) / time_adjust + x1 = radius*math.cos(t1) + center_x + y1 = radius*math.sin(t1) + center_y + x2 = radius*math.cos(t2) + center_x + y2 = radius*math.sin(t2) + center_y + pyxel.tri(center_x, center_y, x1, y1, x2, y2, colors[i % len(colors)]) + + + pyxel.text(8, 45, "Press Start!", 7 if int(app.frame/8)%2 else 0) + + +class GuessingState(State): + def __init__(self, **kwargs): + self.guessed = False + self.guess = kwargs.get("next_guess") + self.guessed_at = kwargs["app"].frame + self.via_next = kwargs.get("via_next") + if self.guess: + kwargs["app"].guessed.add(self.guess) + self.guessed = True + + correct = False + for word in kwargs["app"].phrase: + for i, letter in enumerate(word): + if letter == self.guess: + correct = True + if correct: + pyxel.play(1, 0) + else: + pyxel.play(1, 1) + + def update(self, app): + if not self.guessed: + for key, value in VALID_GUESSES.items(): + if pyxel.btnp(key) and value not in app.guessed: + self.guessed = True + self.guess = value + app.guessed.add(value) + self.guessed_at = app.frame + + correct = False + for word in app.phrase: + for i, letter in enumerate(word): + if letter == self.guess: + correct = True + if correct: + pyxel.play(1, 0) + else: + pyxel.play(1, 1) + + # TODO sum up outcomes for score? + else: + if app.frame - self.guessed_at > 50: + # If next round + missing_letter = False + correct = False + for word in app.phrase: + for i, letter in enumerate(word): + if letter not in app.guessed: + missing_letter = True + + if not missing_letter: + app.next_round(winner="THEM" if self.via_next else "YOU") + elif self.via_next: + app.set_state(SpinOrb) + else: + # chance for enemy to guess right increases each round + if random.random() < (0.1 * (6 + app.round)): + for word in app.phrase: + for letter in word: + if letter not in app.guessed: + them_guess = letter + break + else: + them_guess = random.choice(list( + set(list(string.ascii_uppercase)) - app.guessed + )) + app.set_state(Dialog, dialog=[{ + "who": "them", + "text": f"I'd like to guess '{them_guess}'" + }], + next=GuessingState, + next_kwargs={"next_guess": them_guess} + ) + + def draw(self, app): + pyxel.rect(0, 0, 64, 64, 3) + pyxel.rect(3, 41, 58, 21, 1) + for i in range(16): + color = 10 if ((int(app.frame/10) + i) % 4) == 0 else 13 + pyxel.rect(2 + 4*i, 2, 1, 1, color) + + color = 10 if ((int(app.frame/10) + 16- i) % 4) == 0 else 13 + pyxel.rect(2 + 4*i, 33, 1, 1, color) + y_offset = 4 + for word in app.phrase: + x_offset = (64-len(word)*8)/2 + for i, letter in enumerate(word): + index = 26 # default blank + if letter in app.guessed: + index = ord(letter) - ord('A') + if app.round == 3: + y_index = app.glyph_map[letter] + elif app.round == 4: + y_index = 1 + else: + y_index = 0 + pyxel.blt(x_offset + i*8, y_offset, 0, index*8, y_index*8, 8, 8) + if self.guessed and letter == self.guess and app.frame - self.guessed_at < 20: + pyxel.rect(x_offset + i*8, y_offset, 8, 8, 10) + y_offset += 10 + pyxel.text(6, 35, app.category, 7) + pyxel.text(6, 42, "GUESSED:", 7) + pyxel.line(4, 48, 58, 48, 7) + for i, value in enumerate(VALID_GUESSES.values()): + pyxel.text(6+4*(i%13), 50 + 6*int(i/13), value, 7 if value in app.guessed else 13) + + +class SpinOrb(State): + def __init__(self, **kwargs): + self.wheel_accel = 50 + self.wheel_outcome = None + self.wheel_spun = False + self.wheel_time = 0 + self.colors = [5, 8, 10, 2, 9] + random.shuffle(self.colors) + self.outcome_time = 50 + + def update(self, app): + if not self.wheel_spun: + if util.if_continue_button(): + self.wheel_spun = True + else: + self.wheel_time += self.wheel_accel/30 + if int(self.wheel_time/5) % 3 == 0: + pyxel.play(1, 12) + if self.wheel_time >= 80 and self.wheel_accel > 0: + self.wheel_accel -= 0.3 + elif self.wheel_accel < 1 and not self.wheel_outcome: + self.wheel_accel = 0 + self.wheel_outcome = random.choice(app.outcomes) + # TODO random event, change state based on that + self.outcome_time = 50 + else: + self.outcome_time -= 1 + if self.outcome_time <= 0: + if self.wheel_outcome == "GRAND PRIZE": + # TODO grand prize ending + pass + app.set_state(GuessingState) + + def draw(self, app): + pyxel.rect(0, 0, 64, 64, 11) + pyxel.blt(0, 0, 0, 0, 64, 64, 64, 11) + center_x = 32 + center_y = 75 + radius = 64 + time_adjust = 20 + + for i in range(19): + portion = (360.0 / 18) / 3 + t0 = self.wheel_time + portion * i + 4 + t1 = t0 / time_adjust + t2 = (t0 + portion) / time_adjust + x1 = radius*math.cos(t1) + center_x + y1 = radius*math.sin(t1) + center_y + x2 = radius*math.cos(t2) + center_x + y2 = radius*math.sin(t2) + center_y + pyxel.tri( + center_x, center_y, x1, y1, x2, y2, + self.colors[i % len(self.colors)]) + + pyxel.tri(30, 10, 32, 10, 30+int(self.wheel_time/5) % 3, 14, 0) + + if self.wheel_outcome: + pyxel.text(20, 20, self.wheel_outcome, 7 if int(app.frame/8)%2 else 0) + + +class Dialog(State): + def __init__(self, **kwargs): + self.dialog = kwargs["dialog"] + self.dialog_index = 0 + self.text_index = -1 + self.next = kwargs.get("next") + self.next_kwargs = kwargs.get("next_kwargs", {}) + self.is_end = kwargs.get("is_end") + if self.is_end: + self.text_index = -20 + + def update(self, app): + if self.dialog_index < len(self.dialog): + # advance 1 char at a time + if self.text_index < len(self.dialog[self.dialog_index]["text"]): + if app.frame % 3 == 0: + self.text_index += 1 + + if self.dialog[self.dialog_index]["who"] == "you": + pyxel.play(1, 0) + pyxel.play(1, random.choice([17, 18])) + elif self.dialog[self.dialog_index]["who"] == "them": + pyxel.play(1, random.choice([15, 16])) + elif self.dialog[self.dialog_index]["who"] == "camera": + pyxel.play(1, random.choice([13, 14])) + + if util.if_continue_button(): + # skip to end of text + if self.text_index < len(self.dialog[self.dialog_index]["text"]): + self.text_index = len(self.dialog[self.dialog_index]["text"]) + else: # move onto next text + self.text_index = -1 + self.dialog_index += 1 + else: + if self.next: + self.next_kwargs["via_next"] = True + app.set_state(self.next, **self.next_kwargs) + else: + app.set_state(SpinOrb) + + def draw(self, app): + pyxel.rect(0, 0, 64, 64, 3) + + if not self.is_end: + pyxel.bltm(0, 0, 0, 0, 0, 64, 64) + else: + pyxel.bltm(0, 0, 0, 64, 0, 64, 64) + pod_blink = int(app.frame / 20) % 3 + if pod_blink: + pyxel.blt(40, 40, 0, 0, 176, 16, 16, 5) + pyxel.blt(8, 40, 0, 0, 176, 16, 16, 5) + + + # draw camera + blink = int(app.frame / 20) % 2 + y_offset = 96 if blink else 104 + direction = 1 if int(app.frame/100) % 2 else -1 + pyxel.blt(24, 8, 0, 48, y_offset, 16 * direction, 8, 5) + + # which form of "them" to use + them_index = 0 if app.round > 2 else 3 + them_dir = 1 if app.round > 2 else -1 + pyxel.blt(8, 24, 0, 16 + 16 * them_index, 96, 16*them_dir, 16, 5) + + pyxel.blt(40, 24, 0, 32, 96 + 16, 16, 16, 5) + + if self.text_index > 0 and self.dialog_index < len(self.dialog): + pyxel.rect(1, 41, 62, 21, 7) + pyxel.rectb(1, 41, 62, 21, 0) + if self.dialog[self.dialog_index]["who"] == "you": + pyxel.tri(44, 34, 42, 41, 38, 41, 7) + pyxel.trib(44, 34, 42, 41, 38, 41, 0) + pyxel.rect(39, 41, 3, 1, 7) + + if self.text_index < len(self.dialog[self.dialog_index]["text"]): + pyxel.blt(40, 24, 0, 32, 96 + 16 * (int(app.frame/5) % 5), 16, 16, 5) + elif self.dialog[self.dialog_index]["who"] == "them": + pyxel.tri(20, 36, 21, 41, 26, 41, 7) + pyxel.trib(20, 36, 21, 41, 26, 41, 0) + pyxel.rect(22, 41, 3, 1, 7) + if self.text_index < len(self.dialog[self.dialog_index]["text"]): + pyxel.blt(8, 24, 0, 16 + 16 * them_index, 96 + 16 * (int(app.frame/5) % 5), 16 * them_dir, 16, 5) + elif self.dialog[self.dialog_index]["who"] == "camera": + pyxel.tri(32, 20, 30, 41, 34, 41, 7) + pyxel.trib(32, 20, 30, 41, 34, 41, 0) + pyxel.rect(31, 41, 3, 1, 7) + current_text = self.dialog[self.dialog_index]["text"][:self.text_index] + line_start = 0 + last_space = 0 + lines = [] + for i, c in enumerate(current_text): + if c == ' ': + last_space = i + if i - line_start >= 15: + lines.append(current_text[line_start:last_space].strip()) + line_start = last_space + last_space = i + lines.append(current_text[line_start:].strip()) + for i, line in enumerate(lines): + pyxel.text(3, 43 + i*6, line, 0) + + if self.is_end: + for i in range(random.randrange(int((self.dialog_index**3)/3)+1)): + y = random.randrange(64) + for j in range(64): + pyxel.rect(j, y, 1, 1, random.choice([1, 7, 13])) + + +class Credits(State): + def __init__(self, **kwargs): + pyxel.playm(0, loop=True) + + def update(self, app): + if app.frame - app.state_change_at > 2000: + pyxel.quit() + + def draw(self, app): + if app.frame - app.state_change_at < 200: + for y in range(64): + for j in range(64): + pyxel.rect(j, y, 1, 1, random.choice([1, 7, 13])) + else: + pyxel.rect(0, 0, 64, 64, 11) + center_x = 32 + center_y = 32 + radius = 50 + time_adjust = 20 + colors = [5, 8, 10, 2] + for i in range(19): + portion = (360.0 / 18) / 3 + t0 = app.frame + portion * i + t1 = t0 / time_adjust + t2 = (t0 + portion) / time_adjust + x1 = radius*math.cos(t1) + center_x + y1 = radius*math.sin(t1) + center_y + x2 = radius*math.cos(t2) + center_x + y2 = radius*math.sin(t2) + center_y + pyxel.tri(center_x, center_y, x1, y1, x2, y2, colors[i % len(colors)]) + + pyxel.rect(16, 3, 35, 16, 0) + pyxel.text(20, 5, "ORB OF", 7) + pyxel.text(18, 12, "FORTUITY", 7) + + pyxel.rect(8, 41, 48, 16, 0) + pyxel.text(16, 43, "Made by", 7) + pyxel.text(10, 51, "Mark Powers", 7) + diff --git a/util.py b/util.py new file mode 100644 index 0000000..df5c866 --- /dev/null +++ b/util.py @@ -0,0 +1,9 @@ +import pyxel + +def if_continue_button(): + for k in [ + pyxel.KEY_KP_ENTER, pyxel.KEY_RETURN, pyxel.KEY_SPACE + ]: + if pyxel.btnp(k): + return True + return False -- cgit v1.2.3