"""ロボコード探検隊:ブラウザーと通常のPythonで共用するシミュレーター。""" import ast import copy import sys import json import traceback import io import contextlib class Robot: def __init__(self, mission, equipment='none', unlocked=()): self.level = copy.deepcopy(mission) self.x, self.y, self.d = mission['start'] self.items = [dict(x=o[0], y=o[1], color=o[2], weight=o[3] if len(o)>3 else 1, revealed=not mission['hidden']) for o in mission['items']] self.energy = [list(p) for p in mission['energy']] self.blocks = copy.deepcopy(mission.get("breakables", [])) self.tools = copy.deepcopy(mission.get("tools", [])) self.hammer = False self.charges = 0 self.smashed = 0 self.carry = None self.depotCounts = [0] * len(mission['depots']) self.delivered = False self.scans = self.steps = self.actions = self.used = 0 self.reading = '未検出' self.unlocked = set(unlocked) self.equipment = equipment if equipment in self.unlocked else 'none' self.capacity = mission['budget'] + (8 if self.equipment == 'battery' else 0) self.log = [] self.line = 0 self.variables = {} def snapshot(self): return copy.deepcopy({k:getattr(self,k) for k in ('x','y','d','items','energy','carry','depotCounts','delivered','scans','steps','actions','reading','used','capacity','blocks','tools','hammer','charges','smashed')}) def _record(self, action, cost=0): self.used += cost if self.used > self.capacity: raise RuntimeError('電力が足りません。道順か装備を見直そう。') self.actions += 1 if self.actions > 200: raise RuntimeError('操作が200回を超えました。繰り返しの終了条件を確認しよう。') self.log.append(dict(type=action,line=self.line,vars=copy.deepcopy(self.variables),state=self.snapshot())) def _blocked(self,x,y): l=self.level return not (0<=x<6 and 0<=y<6) or [x,y] in self.blocks or [x,y] in l['rocks'] or [x,y] in l.get('holes',[]) or (l['bridge']==[x,y] and not self.delivered) def _target(self,n=1): return self.x+[0,1,0,-1][self.d]*n,self.y+[-1,0,1,0][self.d]*n def move(self,n): if type(n) is not int or not -20<=n<=20: raise ValueError('move() の数は -20〜20 の整数にしよう。マイナスは後退です。') direction = -1 if n < 0 else 1 for _ in range(abs(n)): x,y=self._target(direction) if self._blocked(x,y): raise RuntimeError(('後ろ' if direction<0 else '前')+'には進めません。岩・穴・端・閉じた橋を確認しよう。') cost=2 if self.carry and self.carry.get('weight',1)>1 else 1 if self.used+cost>self.capacity:raise RuntimeError('電力が足りません。往復の距離か装備を見直そう。') self.x,self.y=x,y self.steps+=1 self._collect_tool() if [x,y] in self.energy:self.energy.remove([x,y]) self._record('back' if direction<0 else 'move',cost) def _collect_tool(self): for item in list(self.tools): if item['x'] == self.x and item['y'] == self.y: self.hammer = True if self.level.get('toolMode') == 'limited': self.charges += item.get('uses', 1) self.tools.remove(item) def smash(self): if not self.hammer: raise RuntimeError('ハンマーがありません。道具のマスへ進んで拾おう。') target=list(self._target()) if target not in self.blocks: raise RuntimeError('目の前に壊せるブロックがありません。オレンジのひび入りブロックと向きを確認しよう。') if self.level.get('toolMode') == 'limited' and self.charges <= 0: raise RuntimeError('ハンマーの残り回数が0です。補充を探すか、最初から道順を考え直そう。') if self.used + 1 > self.capacity: raise RuntimeError('電力が足りません。壊すには電力1が必要です。') self.blocks.remove(target) if self.level.get('toolMode') == 'limited': self.charges -= 1 self.smashed += 1 self._record('smash', 1) def left(self): self.d=(self.d+3)%4 self._record('left') def right(self): self.d=(self.d+1)%4 self._record('right') def jump(self): if self.equipment!='jump' or 'jump' not in self.unlocked: raise RuntimeError('ジャンプユニットを装備しよう。') if self.carry and self.carry.get('weight',1)>1:raise RuntimeError('重い荷物を持ったままジャンプできません。歩ける道を探そう。') x,y=self._target(2) if self._blocked(x,y):raise RuntimeError('着地できません。2マス先を確認しよう。') # A locked bridge cannot be bypassed to rescue without delivery. if self.level['bridge']==list(self._target()) and not self.delivered:raise RuntimeError('電源が入っていない橋は飛び越えられません。') if self.used+3>self.capacity:raise RuntimeError('ジャンプの電力3が足りません。道順か装備を見直そう。') self.x,self.y=x,y self.steps+=2 self._collect_tool() if [x,y] in self.energy:self.energy.remove([x,y]) self._record('jump',3) def pick(self): if 'arm' not in self.unlocked:raise RuntimeError('第1章を修了して標準アームを獲得しよう。') if self.carry:raise RuntimeError('荷物は1つまで。先に届けよう。') item=next((o for o in self.items if (o['x'],o['y'])==(self.x,self.y)),None) if not item:raise RuntimeError('同じマスに荷物がありません。荷物の上へ進もう。') if item.get('weight',1)>1 and self.equipment!='power':raise RuntimeError('重さ2・3の荷物には強化アームを装備しよう。') self.carry=item self.items.remove(item) self._record('pick') def place(self): if not self.carry:raise RuntimeError('荷物を持っていません。先につかもう。') idx=next((i for i,o in enumerate(self.level['depots']) if (o['x'],o['y'])==(self.x,self.y)),None) if idx is not None: depot=self.level['depots'][idx] if depot['color']!=self.carry['color']:raise RuntimeError('配送先と荷物の色が違います。箱と倉庫の文字を見比べよう。') if self.depotCounts[idx]>=depot['count']:raise RuntimeError('ここは必要な荷物を受け取り済みです。') self.depotCounts[idx]+=1 self.delivered=all(n>=d['count'] for n,d in zip(self.depotCounts,self.level['depots'])) else: if any((o['x'],o['y'])==(self.x,self.y) for o in self.items):raise RuntimeError('ここには別の荷物があります。') self.items.append(dict(self.carry,x=self.x,y=self.y,revealed=True)) self.carry=None self._record('place') def color(self): if 'sensor' not in self.unlocked:raise RuntimeError('第8章を修了してセンサーパックを獲得しよう。') item=next((o for o in self.items if (o['x'],o['y'])==(self.x,self.y)),None) if item:item['revealed']=True self.reading=item['color'] if item else 'none' self.scans+=1 self._record('color') return self.reading def ahead(self): if 'sensor' not in self.unlocked:raise RuntimeError('センサーパックが必要です。') result=not self._blocked(*self._target()) self.reading=str(result) self.scans+=1 self._record('ahead') return result def weight(self): if 'sensor' not in self.unlocked:raise RuntimeError('センサーパックが必要です。') item=self.carry or next((o for o in self.items if (o['x'],o['y'])==(self.x,self.y)),None) value=item.get('weight',1) if item else 0 self.reading=str(value) self.scans+=1 self._record('weight') return value def battery(self): if 'sensor' not in self.unlocked:raise RuntimeError('センサーパックが必要です。') value=self.capacity-self.used self.reading=str(value) self.scans+=1 self._record('battery') return value def carrying(self):return self.carry is not None def delivered_count(self):return sum(self.depotCounts) def won(self): return self.smashed >= self.level.get('minSmash', 0) and [self.x,self.y]==self.level['goal'] and not self.energy and (not self.level['depots'] or self.delivered) and (not self.level['requireScan'] or self.scans>0) and self.used<=self.capacity robot = None class LearningTrace(ast.NodeTransformer): """Observe evaluated syntax without changing learner-visible line numbers.""" def wrap(self, node, names): return ast.copy_location(ast.Call(func=ast.Name(id='__learn_value__', ctx=ast.Load()), args=[ast.Constant('|'.join(names)), node], keywords=[]), node) def mark(self, name, node): return ast.copy_location(ast.Expr(value=ast.Call(func=ast.Name(id='__learn_mark__', ctx=ast.Load()), args=[ast.Constant(name)], keywords=[])), node) def visit_If(self, node): self.generic_visit(node) # An unreachable `if False:` must not satisfy the learning goal. if isinstance(node.test, ast.Constant): node.body.insert(0, self.mark('If', node)) if node.orelse: node.orelse.insert(0, self.mark('If', node)) else: node.test = self.wrap(node.test, ['If']) return node def visit_For(self, node): self.generic_visit(node) node.body.insert(0, self.mark('For', node)) return node def visit_While(self, node): self.generic_visit(node) if isinstance(node.test, ast.Constant): node.body.insert(0, self.mark('While', node)) else: node.test = self.wrap(node.test, ['While']) return node def visit_FunctionDef(self, node): self.generic_visit(node) node.body.insert(0, self.mark('FunctionDef', node)) return node def visit_Assign(self, node): self.generic_visit(node) node.value = self.wrap(node.value, ['Assign']) return node def visit_AugAssign(self, node): self.generic_visit(node) node.value = self.wrap(node.value, ['Assign','BinOp',type(node.op).__name__]) return node def visit_Return(self, node): self.generic_visit(node) node.value = self.wrap(node.value or ast.copy_location(ast.Constant(None),node), ['Return']) return node def visit_BinOp(self, node): self.generic_visit(node) return self.wrap(node, ['BinOp',type(node.op).__name__]) def visit_Compare(self, node): self.generic_visit(node) return self.wrap(node, ['Compare']+[type(op).__name__ for op in node.ops]) def visit_BoolOp(self, node): self.generic_visit(node) return self.wrap(node, ['BoolOp',type(node.op).__name__]) def visit_UnaryOp(self, node): self.generic_visit(node) return self.wrap(node, [type(node.op).__name__]) def visit_List(self, node): self.generic_visit(node) return self.wrap(node, ['List']) if isinstance(node.ctx,ast.Load) else node def visit_Subscript(self, node): self.generic_visit(node) return self.wrap(node, ['Subscript']) if isinstance(node.ctx,ast.Load) else node LEARNING_NAMES = {'Assign':'変数への代入','BinOp':'計算','Add':'足し算 +','Sub':'引き算 -','Mult':'掛け算 *','FloorDiv':'整数の割り算 //','Mod':'余り %','Compare':'値の比較','Eq':'等しいかの比較 ==','NotEq':'異なるかの比較 !=','Lt':'小さいかの比較 <','Gt':'大きいかの比較 >','LtE':'以下の比較 <=','GtE':'以上の比較 >=','If':'条件分岐 if','For':'for の繰り返し','While':'while の条件判断','BoolOp':'条件の組み合わせ','And':'and','Or':'or','Not':'not','FunctionDef':'自分で作った関数の呼び出し','Return':'return で値を返す','List':'リスト','Subscript':'リストの要素の参照'} def run_program(source, mission, equipment='none', unlocked=()): global robot robot=Robot(mission,equipment,unlocked) steps=0 evaluated=set() called=set() def learn_value(names, value): evaluated.update(names.split('|')) return value def learn_mark(name): evaluated.add(name) def trace(frame,event,arg): nonlocal steps if event=='call' and frame.f_locals.get('self') is robot: called.add(frame.f_code.co_name) if frame.f_code.co_filename=='mission.py' and event=='line': steps+=1 if steps>5000:raise RuntimeError('処理が5000行を超えました。whileの終了条件を確認しよう。') robot.line=frame.f_lineno robot.variables={k:repr(v)[:100] for k,v in frame.f_locals.items() if not k.startswith('_') and isinstance(v,(int,float,str,bool,list,tuple,dict))} return trace output=io.StringIO() error=None try: tree=ast.parse(source,'mission.py') code=compile(ast.fix_missing_locations(LearningTrace().visit(tree)),'mission.py','exec') sys.settrace(trace) with contextlib.redirect_stdout(output): exec(code,{'__name__':'__main__','__learn_value__':learn_value,'__learn_mark__':learn_mark}) except BaseException as e: line=getattr(e,'lineno',None) or robot.line error=f'{line}行目:{type(e).__name__}: {e}' finally: sys.settrace(None) if error is None: required=mission.get('required',[]) missing=[LEARNING_NAMES.get(n,n) for n in required if n not in evaluated] missing += ['robot.'+n+'()' for n in mission.get('requiredCalls',[]) if n not in called] if missing:error='学習目標:まだ実行を確認できないもの:'+ '、'.join(missing)+'。画面の学習目標と練習を確認しよう。' if mission.get('expectedOutput') is not None and output.getvalue().strip()!=mission['expectedOutput']: error='出力を確認しよう。'+mission.get('outputHint','説明の計算・比較の結果をprintで表示しよう。') return dict(log=robot.log,won=robot.won() and error is None,error=error,output=output.getvalue()[:4000],final=robot.snapshot(),variables=robot.variables,learning=dict(syntax=sorted(evaluated),calls=sorted(called))) if __name__=='__main__': # Make `from robocode import robot` resolve to this module when used as a CLI. sys.modules['robocode']=sys.modules[__name__] from pathlib import Path source=Path(sys.argv[1]).read_text(encoding='utf8') mission=json.loads(Path(sys.argv[2]).read_text(encoding='utf8')) equipment=sys.argv[3] if len(sys.argv)>3 else 'none' result=run_program(source,mission,equipment,['arm','jump','sensor','battery','power']) print(result['output']) print(result['error'] or ('MISSION COMPLETE' if result['won'] else 'まだ目標に届いていません')) print('位置:',result['final']['x']+1,result['final']['y']+1,'電力:',result['final']['used'])