# MIT License # # Copyright (c) 2026 Pyxel 100 contributors # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # The bundled Noto-derived bitmap glyphs are separately licensed under SIL OFL. # See assets/NOTO-LICENSE.txt. """うそつき市場・教材3/5。有限の架空市場。現実の投資/価格予測ではありません。""" from dataclasses import dataclass, replace, asdict STAGE = 3 ITEMS = ("TEA", "JAM") BASE = ((1,3),(4,3),(3,3)) INITIAL_CASH = 10 GOAL = 12 BAG_LIMIT = 3 KINDS = ("fixed", "rush", "bold") ACTUAL_KINDS = ("bold", "fixed", "rush") @dataclass(frozen=True) class Memory: bought: bool = False passed: bool = False @dataclass(frozen=True) class Observation: event: str item: int @dataclass(frozen=True) class Quote: shop: int item: int price: int @dataclass(frozen=True) class Evidence: event: str item: int price: int = 0 @dataclass(frozen=True) class State: cash: int = INITIAL_CASH stock: tuple = (True,)*6 bag: tuple = () memories: tuple = (Memory(),)*3 history: tuple = ((),(),()) quote: Quote | None = None spent: int = 0 ended: bool = False outcome: str = "" reason: str = "ready" def initial(): return State() def price_policy(kind, base, memory): if kind == 'rush' and memory.passed: return max(1, base - 1) if kind == 'bold' and memory.bought: return base * 2 return base def observe(event, item): return Observation(event, item) def remember(memory, obs): return Memory(memory.bought or obs.event == 'buy', memory.passed or obs.event == 'pass') def add_evidence(s, shop, event, item, price=0): h = list(s.history) h[shop] = h[shop] + (Evidence(event, item, price),) return replace(s, history=tuple(h)) def bag_value(state): return len(state.bag)*3 def total(state): return state.cash+bag_value(state) def preview(state): if state.quote is None: return None return {"price":state.quote.price,"new_item_value":3,"existing_gain":0, "final_total":total(state)-state.quote.price+3} def validate(state): assert state.cash >= 0 and state.spent >= 0 assert state.cash+state.spent == INITIAL_CASH assert len(state.bag)<=BAG_LIMIT and len(set(state.bag))==len(state.bag) assert sum(state.stock)+len(state.bag)==6 assert all(not state.stock[shop*2+item] for shop,item in state.bag) def step(state, action): if state.ended: return state if not isinstance(action,tuple) or not action: return replace(state,reason="invalid") kind=action[0] if kind=="quote": if state.quote is not None: return replace(state,reason="already_quote") if len(action)!=3 or type(action[1]) is not int or type(action[2]) is not int or not 0<=action[1]<3 or action[2] not in (0,1): return replace(state,reason="invalid") shop,item=action[1:] if not state.stock[shop*2+item]: return replace(state,reason="sold") value=price_policy(ACTUAL_KINDS[shop],BASE[shop][item],state.memories[shop]) state=add_evidence(state,shop,"quote",item,value) return replace(state,quote=Quote(shop,item,value),reason="quote") if kind=="pass" and len(action)==1: if state.quote is None: return replace(state,reason="no_quote") q=state.quote memories=list(state.memories) memories[q.shop]=remember(memories[q.shop],observe("pass",q.item)) state=add_evidence(state,q.shop,"pass",q.item) return replace(state,quote=None,memories=tuple(memories),reason="passed") if kind=="buy" and len(action)==1: if state.quote is None: return replace(state,reason="no_quote") q=state.quote if not state.stock[q.shop*2+q.item]: return replace(state,reason="sold") if state.cash=BAG_LIMIT: return replace(state,reason="full") stock=list(state.stock) stock[q.shop*2+q.item]=False memories=list(state.memories) memories[q.shop]=remember(memories[q.shop],observe("buy",q.item)) state=add_evidence(state,q.shop,"buy",q.item,q.price) out=replace(state,cash=state.cash-q.price,spent=state.spent+q.price,stock=tuple(stock), bag=state.bag+((q.shop,q.item),),quote=None,reason="bought",memories=tuple(memories)) validate(out) return out if kind=="finish" and len(action)==1: result="won" if total(state)>=GOAL else "below" return replace(state,quote=None,ended=True,outcome=result,reason=result) return replace(state,reason="invalid") class App: def __init__(self): self.reset() def reset(self): self.state=initial() self.selected=0 self.confirm=False self.gated=False self.message="READY" def control(self,pressed,held): if "R" in pressed: self.reset() self.gated=True return if self.gated: if not pressed and not held: self.gated=False return if len(pressed | held)>1: self.gated=True self.message="RELEASE ALL KEYS" return if len(pressed)!=1: return key=next(iter(pressed)) self.gated=True if self.state.ended: return if self.confirm: if key=="X": self.confirm=False self.message="RETURN CANCELLED" elif key=="Z": self.state=step(self.state,("finish",)) self.confirm=False self.message=self.state.reason.upper() return if self.state.quote is not None: if key in ("Z","X"): self.state=step(self.state,("buy" if key=="Z" else "pass",)) self.message=self.state.reason.upper() else: self.message="QUOTE OPEN: Z BUY / X PASS" return if key=="F": self.confirm=True self.message="RETURN NOW? Z YES / X CANCEL" elif key in ("LEFT","RIGHT"): self.selected=(self.selected+(-1 if key=="LEFT" else 1))%6 self.message="SELECT ONLY" elif key in tuple(str(n) for n in range(1,7)): self.selected=int(key)-1 self.message="SELECT ONLY" elif key=="Z": self.state=step(self.state,("quote",self.selected//2,self.selected%2)) self.message=self.state.reason.upper() def draw(self,p): state=self.state p.cls(1) p.text(8,8,"FICTIONAL MARKET / STAGE "+str(STAGE),10) p.text(8,23,"CASH "+str(state.cash)+" BAG VALUE "+str(bag_value(state))+" TOTAL "+str(total(state))+" / GOAL "+str(GOAL),7) p.text(8,39,"SPENT "+str(state.spent)+" BAG "+str(len(state.bag))+" / "+str(BAG_LIMIT),11) for n in range(6): shop,item=divmod(n,2) x=8+shop*164;y=62+item*43 p.rect(x,y,156,37,2 if n==self.selected else 1) p.rectb(x,y,156,37,10 if n==self.selected else 5) p.text(x+6,y+6,str(n+1)+" "+"ABC"[shop]+" / "+ITEMS[item],7) p.text(x+6,y+20,"BASE "+str(BASE[shop][item])+" / "+("STOCK" if state.stock[n] else "SOLD"),6) bag=" ".join("ABC"[shop]+":"+ITEMS[item] for shop,item in state.bag) or "EMPTY" p.text(8,156,"BAG ORIGINS: "+bag,7) p.text(8,173,"PRACTICE: EVERY ITEM VALUE IS 3",11) q=state.quote if q: p.text(8,194,"LOCKED QUOTE: "+"ABC"[q.shop]+" "+ITEMS[q.item]+" / PRICE "+str(q.price),10) blocked="MONEY" if state.cash=BAG_LIMIT else "" if blocked: p.text(8,210,"BUY REFUSED: "+blocked+" / X: PASS",8) else: forecast=preview(state) p.text(8,210,"IF BOUGHT: NEW "+str(forecast['new_item_value'])+" / OLD GAIN "+str(forecast['existing_gain'])+" / TOTAL "+str(forecast['final_total']),11) else: p.text(8,194,"Z OPENS QUOTE. NEXT Z PAYS LOCKED PRICE.",10) p.text(8,237,"ONE-TIME MERCHANT MEMORY / NOT PERSONAL DATA",6) p.text(8,292,self.message,14) p.text(8,309,"RESULT: "+(state.outcome.upper() if state.ended else "SHOPPING"),10) p.text(8,330,"ARROWS / 1-6 SELECT Z ACT X PASS/CANCEL",7) p.text(8,346,"F RETURN CONFIRMATION R RESET ESC QUIT",7) p.text(8,362,"NO UNDO / NO RESELL / NO REAL MONEY",6) DEMO_KEYS = ('1', 'Z', 'Z', '2', 'Z') def main(headless=False,screenshot=None,demo=False): import pyxel as p p.init(508,382,title="Fictional market lesson "+str(STAGE),fps=30,display_scale=2,headless=headless) app=App() keys={"LEFT":p.KEY_LEFT,"RIGHT":p.KEY_RIGHT,"Z":p.KEY_Z,"X":p.KEY_X,"F":p.KEY_F,"R":p.KEY_R} keys.update({str(n):getattr(p,"KEY_"+str(n)) for n in range(1,7)}) limit=max(16,2*len(DEMO_KEYS)+6) if demo else 16 def update(): if p.btnp(p.KEY_ESCAPE) or headless and p.frame_count>=limit: if screenshot: app.draw(p) p.screen.save(screenshot,1) p.quit() if headless: if demo: index=p.frame_count//2 pressed={DEMO_KEYS[index]} if p.frame_count%2==0 and index