385 lines
16 KiB
Python
385 lines
16 KiB
Python
# =============================================================================
|
|
# Fishing Mini-Project - Main Game File (OUTLINE ONLY)
|
|
# =============================================================================
|
|
# This file is a STRUCTURAL OUTLINE: function signatures + descriptive comments.
|
|
# The actual implementation will be written by the project author (human).
|
|
#
|
|
# Engine: cmu_graphics | Pattern: MVC (Model-View-Controller)
|
|
# Data: creatures.tsv (tab-separated database at repo root)
|
|
# Save: save.json (persistent progress in the script directory)
|
|
# =============================================================================
|
|
|
|
from cmu_graphics import *
|
|
import json
|
|
import random
|
|
import math
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 1: DATA LOADING / FILE I/O
|
|
# -----------------------------------------------------------------------------
|
|
# Helpers that read from and write to disk. Kept separate from game logic so
|
|
# the data source (TSV path, save path) can change without touching gameplay.
|
|
# =============================================================================
|
|
|
|
def loadCreatures(path="creatures.tsv"):
|
|
# Read creatures.tsv (tab-separated) and parse it into a usable structure.
|
|
# Suggested return: a list of dicts, one per creature, with keys matching
|
|
# the header row: name, description, price, sprite-file, difficulty,
|
|
# length, weight. Convert numeric columns (price, difficulty, length,
|
|
# weight) from strings to int/float here; leave missing fields as sensible
|
|
# defaults so partially-filled rows (like the current TSV) don't crash.
|
|
creatures = []
|
|
with open(path) as f:
|
|
lines = f.readlines()
|
|
for line in lines[1:]: #ignore header
|
|
data = line.split("\t")
|
|
creatures.append({
|
|
"name": data[0],
|
|
"description": data[1],
|
|
"price": float(data[2]),
|
|
"sprite-file": data[3],
|
|
"difficulty": int(data[4]),
|
|
"length": float(data[5]), #inches
|
|
"weight": float(data[6]) #pounds
|
|
})
|
|
|
|
return creatures
|
|
|
|
|
|
def loadSave(path="save.json"):
|
|
# Load save.json and return the player's saved state (money, owned baits,
|
|
# owned upgrades, current bait, caught log, etc.). If the file is missing
|
|
# or unreadable, return a fresh default-state dict so a first-time player
|
|
# starts cleanly. This is the only place that reads the save file.
|
|
defaultState = {
|
|
"cash": 0,
|
|
"owned_baits": ["Worms"],
|
|
"owned_upgrades": [],
|
|
"current_bait": "Worms",
|
|
"catch_log": []
|
|
}
|
|
try:
|
|
with open(path, "r") as f:
|
|
return json.load(f)
|
|
except (FileNotFoundError, IOError):
|
|
return defaultState
|
|
|
|
|
|
def writeSave(saveState, path="save.json"):
|
|
# Serialize the current player state to save.json. Call this at key moments
|
|
# (after a sale, after a purchase, on quit) so progress persists between
|
|
# sessions. Keep the written structure identical to what loadSave expects.
|
|
with open(path, "w") as f: #overwrite existing
|
|
f.write(json.dumps(saveState))
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 2: MODEL - STATE SETUP & GAME DATA
|
|
# -----------------------------------------------------------------------------
|
|
# Everything that initializes and holds game state on the cmu_graphics `app`.
|
|
# No drawing and no event handling here - just state.
|
|
# =============================================================================
|
|
|
|
AVAILABLE_UPGRADES = [ # (name, bonus_type, bonus_modifier, price)
|
|
#power mods - lowers speed of cursor (additive)
|
|
("Smart Reel", "power", 2, 100),
|
|
("Clever Reel", "power", 7, 350),
|
|
("Intelligent Reel", "power", 10, 1500),
|
|
("Genius Reel", "power", 20, 20000),
|
|
#timing mods - lengthens bars in skill check (multiplicative)
|
|
("Medium Bobber", "timing", 1.1, 100),
|
|
("Large Bobber", "timing", 1.2, 300),
|
|
("Massive Bobber", "timing", 1.5, 1600),
|
|
("Enourmous Bobber", "timing", 2, 25000),
|
|
#count mods - lowers number of bars in skill check (subtractive (capped at 1))
|
|
("Shiny Rod", "count", 1, 450),
|
|
("Spectral Rod", "count", 1, 1200),
|
|
("Glowing Rod", "count", 2, 5000),
|
|
("Galaxy Rod", "count", 3, 40000)
|
|
]
|
|
|
|
AVAILABLE_BAITS = [ # (name, max_difficulty)
|
|
("Worms", 30),
|
|
("Prawns", 50),
|
|
("Tuna", 70),
|
|
("Bass", 100)
|
|
|
|
]
|
|
|
|
def onAppStart(app):
|
|
# cmu_graphics entry point. Initialize ALL app.* state here:
|
|
# - window/layout constants (width, height, dock/water/grass regions)
|
|
# - app.creatures (from loadCreatures)
|
|
# - app.save (from loadSave) and derived player values
|
|
# - fishing state (idle / waiting-for-bite / skill-check / result)
|
|
# - skill-check state placeholders (cursor angle, bars, speed, result)
|
|
# - app.stepsPerSecond for the game loop cadence
|
|
# Delegates to the smaller setup helpers below to keep this readable.
|
|
|
|
#DRAWING CONSTANTS
|
|
app.width = 600
|
|
app.height = 400
|
|
app.background = 'lightSkyBlue'
|
|
app.waterColor = 'midnightBlue'
|
|
app.grassColor = 'seaGreen'
|
|
app.waterHeight = app.height // 3
|
|
app.waterBottom = app.height - app.waterHeight
|
|
app.grassHeight = 20
|
|
app.grassBottom = app.height - app.waterHeight - app.grassHeight
|
|
|
|
#GAME STATE/SETUP
|
|
app.stepsPerSecond = 30
|
|
app.creatures = loadCreatures()
|
|
app.save = loadSave()
|
|
app.scene = "idle" # idle, waiting-for-bite, skill-check, result, shopping, menu
|
|
|
|
#SKILL CHECK
|
|
app.skillCheckAngle = 0
|
|
app.skillCheckSpeed = 1
|
|
app.skillCheckBars = [] # tuples of (position, length)
|
|
app.skillCheckFailed = False
|
|
|
|
#FISHING
|
|
app.fishingTimer = 0
|
|
app.hookedFish = None
|
|
|
|
|
|
|
|
def resetFishingState(app):
|
|
# Return the fishing cycle to its starting "idle" point so a new cast can
|
|
# begin. Clears any active skill-check data, the pending random bite timer,
|
|
# and the currently-hooked creature. Called at startup and after each catch
|
|
# is resolved/dismissed.
|
|
app.skillCheckAngle = 0 #measured in degrees
|
|
app.skillCheckSpeed = 1
|
|
app.skillCheckBars = []
|
|
app.skillCheckFailed = False
|
|
|
|
app.hookedFish = None
|
|
app.fishingTimer = 0
|
|
pass
|
|
|
|
|
|
def pickRandomCreature(app):
|
|
# Choose which creature the player hooks on this cast. Selection is filtered
|
|
# by the current bait (bait gates which difficulty tiers are available) and
|
|
# weighted toward easier creatures, while still giving rarer/harder ones a
|
|
# real chance (per the spec's "skewed toward easier but still fun" goal).
|
|
# Returns the chosen creature record (one entry from app.creatures).
|
|
max_difficulty = [bait[1] for bait in AVAILABLE_BAITS if bait[0] == app.save["current_bait"]][0]
|
|
|
|
#here we take the root of the difficulty to represent the weight of that fish, this makes higher difficulty fish easier to catch once unlocked
|
|
k = 0.5
|
|
#invert the difficulty and add one so it avoids zero
|
|
possibilities = [(creature["name"], (max_difficulty - creature["difficulty"] + 1) ** k) for creature in app.creatures if creature["difficulty"] <= max_difficulty]
|
|
total = sum([c[1] for c in possibilities])
|
|
|
|
#now pick randomly
|
|
roll = random.random() * (total - 0.0001) # a value between 0 and total (minus a small amount so we ensure the while loop triggers)
|
|
|
|
while roll > 0:
|
|
possibleCatch = possibilities.pop(0)
|
|
roll -= possibleCatch[1]
|
|
|
|
catch = [c for c in app.creatures if c["name"] == possibleCatch[0]][0]
|
|
return catch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def startSkillCheck(app, creature):
|
|
# Configure a new skill check based on the hooked creature's difficulty.
|
|
# Difficulty maps to: cursor rotation speed, number of target bars, and
|
|
# bar length/width. Also apply any owned upgrades that modify these values
|
|
# (e.g. slower cursor, wider bars, extra forgiveness). Stores the resulting
|
|
# parameters on app for the controller/view to use.
|
|
app.scene = "skill-check"
|
|
difficulty = creature["difficulty"]
|
|
|
|
fishingPower = 1 + sum([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "power")])
|
|
|
|
app.skillCheckSpeed = difficulty / fishingPower
|
|
numBars = difficulty // 10 - sum([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "count")])
|
|
numBars = max(numBars, 1)
|
|
|
|
lastPos = 0
|
|
lastLen = 0
|
|
while len(app.skillCheckBars) < numBars:
|
|
#50 is default length, adjust it proportionally to difficulty, then add upgrade length multipliers. also set to int instead of float
|
|
newLen = random.randrange(int((1 - (difficulty / 100)) * 50 * math.prod([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "timing")])))
|
|
#take the end of the last bar, and add a random amount between 5 degrees
|
|
#and the remaining amount to the end divided by the number of bars left
|
|
lastEndPos = (lastPos + lastLen)
|
|
newPos = lastEndPos + random.randrange(5, (360 - lastEndPos) // (numBars - len(app.skillCheckBars)))
|
|
#cap it to ensure we dont go over
|
|
newPos = min(newPos, 360-newLen)
|
|
#TODO: double check that multiple bars do not overlap
|
|
app.skillCheckBars.append((newPos, newLen))
|
|
lastPos = newPos
|
|
lastLen = newLen
|
|
|
|
#speed is set, bars are set, scene is good, should be fine?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 3: CONTROLLER - GAME LOOP & INPUT HANDLING
|
|
# -----------------------------------------------------------------------------
|
|
# cmu_graphics event hooks. These advance state and route input; they should
|
|
# call Model helpers rather than contain heavy logic themselves.
|
|
# =============================================================================
|
|
|
|
def onStep(app):
|
|
# Runs every time step (the game loop / "controller tick").
|
|
# Responsibilities depend on the current fishing phase:
|
|
# - waiting-for-bite: count down the random timer; when it elapses, pick a
|
|
# creature and transition into the skill check.
|
|
# - skill-check: advance the cursor's angle around the circle each step.
|
|
# - otherwise (idle, viewing a catch, in shop): do nothing time-based.
|
|
pass
|
|
|
|
|
|
def onMousePress(app, mouseX, mouseY):
|
|
# Primary input router. Behavior depends on app.scene / fishing phase:
|
|
# - dock idle: clicking starts a cast (begin waiting-for-bite).
|
|
# - skill-check active: register the click as a timing attempt and
|
|
# evaluate it via checkSkillHit.
|
|
# - catch popup showing: dismiss the popup and reset fishing.
|
|
# - shop / UI sprites clicked: open the shop or trigger that UI action.
|
|
# Keep this as dispatch only - defer the real work to helper functions.
|
|
pass
|
|
|
|
|
|
def onKeyPress(app, key):
|
|
# Optional keyboard controls. Candidates: a key to cast/reel as an
|
|
# alternative to clicking, a key to open/close the shop, or a debug/reset
|
|
# key during development. Remove if you decide the game is mouse-only.
|
|
pass
|
|
|
|
|
|
def checkSkillHit(app):
|
|
# Evaluate a skill-check click: is the cursor's current angle inside one of
|
|
# the active target bars? Decide hit vs miss. Define the multi-bar flow here
|
|
# (e.g. must the player clear every bar, or is one good hit enough?) and
|
|
# advance or fail the skill check accordingly. On full success, mark the
|
|
# creature as caught and move to the catch-display phase; on failure, end
|
|
# the cast without a catch.
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 4: SHOP / ECONOMY LOGIC
|
|
# -----------------------------------------------------------------------------
|
|
# Buying/selling and the rogue-like upgrade system. Pure state changes that the
|
|
# controller invokes; no drawing here.
|
|
# =============================================================================
|
|
|
|
def sellCreature(app, creature):
|
|
# Add the creature's market value to the player's money. Value scales with
|
|
# difficulty (harder creatures pay more, per the spec). Persist the updated
|
|
# money via writeSave. Decide here whether selling is automatic on catch or
|
|
# a deliberate action in the shop/market.
|
|
pass
|
|
|
|
|
|
def buyUpgrade(app, upgradeId):
|
|
# Attempt to purchase an upgrade. Check the player can afford it and doesn't
|
|
# already own it; if valid, deduct money, record ownership in app.save, and
|
|
# persist. Upgrades modify the skill check (cursor speed, bar size/count,
|
|
# extra attempts, etc.) and are read back in startSkillCheck.
|
|
pass
|
|
|
|
|
|
def buyBaitLicense(app, baitId):
|
|
# Purchase a one-time license for a bait tier. Once owned, the player can
|
|
# freely switch to that bait at no further cost (switching handled by
|
|
# setActiveBait). Bait determines which creatures are reachable in
|
|
# pickRandomCreature. Deduct money, record ownership, persist.
|
|
pass
|
|
|
|
|
|
def setActiveBait(app, baitId):
|
|
# Switch the currently-equipped bait to any bait the player already owns a
|
|
# license for. Free action. Updates app state so the next cast's creature
|
|
# pool reflects the new bait. Reject the switch if the bait isn't owned.
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 5: VIEW - DRAWING
|
|
# -----------------------------------------------------------------------------
|
|
# redrawAll + per-scene draw helpers. These ONLY read state and draw; they must
|
|
# never mutate app state. Composed top-down so redrawAll stays legible.
|
|
# =============================================================================
|
|
|
|
def redrawAll(app):
|
|
# Master draw routine. Draw the persistent world first (background, grass,
|
|
# water, dock, player), then layer the active scene/phase on top:
|
|
# - the shop UI, or
|
|
# - the active skill-check overlay, or
|
|
# - the catch-display popup.
|
|
# Delegates each piece to the helpers below.
|
|
pass
|
|
|
|
|
|
def drawBackground(app):
|
|
# Draw the static scene layout from the spec: grass behind, the dock, and
|
|
# the lake occupying the bottom ~1/3 of the screen (blue water, optional
|
|
# wave patterns). This is the backdrop present in every scene.
|
|
pass
|
|
|
|
|
|
def drawPlayerAndRod(app):
|
|
# Draw the character sprite seated at the end of the dock with the fishing
|
|
# rod. When a cast is active, draw the line and the bobber dropping onto the
|
|
# water surface. Reflects the current fishing phase visually.
|
|
pass
|
|
|
|
|
|
def drawShopEntities(app):
|
|
# Draw the clickable shop/UI sprites standing on the grass (per the spec).
|
|
# Positions here must line up with the hit-testing done in onMousePress so
|
|
# clicks land on the right entity.
|
|
pass
|
|
|
|
|
|
def drawSkillCheck(app):
|
|
# Draw the active skill-check overlay: the circle, the target bars placed
|
|
# around it (count/length from difficulty), and the rotating cursor at its
|
|
# current angle. Optionally show which bars are already cleared. Read-only.
|
|
pass
|
|
|
|
|
|
def drawCatchDisplay(app):
|
|
# Draw the "you caught it!" popup: the creature's sprite coming out of the
|
|
# water, plus its name, description, and stats (length, weight, value).
|
|
# Stays up until dismissed by a click (handled in onMousePress). Read-only.
|
|
pass
|
|
|
|
|
|
def drawHud(app):
|
|
# Draw the persistent heads-up display: current money, equipped bait, and
|
|
# any brief status/instruction text ("click to cast", etc.). Overlaid on
|
|
# every scene so the player always sees their core stats.
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# SECTION 6: PROGRAM ENTRY
|
|
# -----------------------------------------------------------------------------
|
|
def main():
|
|
# Start the cmu_graphics app (runApp with the chosen window dimensions).
|
|
pass
|
|
|
|
|
|
main()
|