-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmcturtle.py
More file actions
152 lines (124 loc) · 5.37 KB
/
mcturtle.py
File metadata and controls
152 lines (124 loc) · 5.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import fastapi
import asyncio
import uuid
import json
from typing import Any, Literal, TypedDict
InteractableDirection = Literal["front", "up", "down"]
INSPECTABLE_DIRECTIONS: list[InteractableDirection] = ["front", "up", "down"]
MoveableDirection = Literal["forward", "back", "up", "down"]
InvSlot = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
class Item(TypedDict):
name: str
count: int
class MCTurtle:
def __init__(self, id: int, ws: fastapi.WebSocket):
self.id = id
self.ws = ws
self.pending_requests: dict[str, asyncio.Future] = {}
self._send_lock = asyncio.Semaphore(1)
async def send_one_way(self, data: dict):
await self.ws.send_json(data)
async def msg(self, message: str):
await self.send_one_way({
"method": "msg",
"content": message
})
async def send(self, data: dict, timeout: float = 30.0) -> dict | None:
async with self._send_lock:
nonce = str(uuid.uuid4())
data["nonce"] = nonce
future = asyncio.Future()
self.pending_requests[nonce] = future
try:
await self.ws.send_json(data)
response = await asyncio.wait_for(future, timeout=timeout)
return response
except asyncio.TimeoutError:
return None
finally:
self.pending_requests.pop(nonce, None)
async def eval(self, code: str) -> Any:
response = await self.send({
"method": "eval",
"content": code
})
if response:
return response.get('content')
else:
print(f"Turtle #{self.id} eval timeout")
return None
async def on_message(self, data: dict):
nonce = data.get("nonce")
if nonce and nonce in self.pending_requests:
future = self.pending_requests[nonce]
if not future.done():
future.set_result(data)
return
if data.get("method") == "msg":
print(f"Turtle #{self.id}: {data.get('content')}")
async def loop(self):
try:
while True:
msg = await self.ws.receive_json()
asyncio.create_task(self.on_message(msg))
except Exception as e:
print(f"Turtle #{self.id} disconnected: {e}")
finally:
print(f"Turtle #{self.id} loop ended")
"""
Turtle inspection API
"""
async def get_blocks(self) -> dict[InteractableDirection, Item | None]:
raw = await self.eval("return inspect()")
return {d: raw.get(d) for d in INSPECTABLE_DIRECTIONS}
async def inspect(self, direction: InteractableDirection | None = None) -> Item | None:
blocks = await self.get_blocks()
return blocks.get(direction or "front")
inspect_up = lambda self: self.inspect("up")
inspect_down = lambda self: self.inspect("down")
"""
Turtle movement API
"""
async def move(self, direction: MoveableDirection | None = None, distance: int | None = None):
direction = direction or "forward"
distance = distance or 1
code = f"local s=0 for i=1,{distance} do if turtle.{direction}() then s=s+1 else break end end return s"
successes = await self.eval(code)
if successes < distance:
block = await self.inspect(direction.replace("forward", "front")) if direction != "back" else None # type: ignore[literal-required]
if block:
raise Exception(f"Block in the way: {block}")
raise Exception(f"Only able to move {successes}/{distance} blocks")
forward = lambda self, distance=None: self.move("forward", distance)
back = lambda self, distance=None: self.move("back", distance)
up = lambda self, distance=None: self.move("up", distance)
down = lambda self, distance=None: self.move("down", distance)
"""
Turtle dig API
"""
async def dig(self, direction: InteractableDirection | None = None):
direction = direction or "front"
code = "turtle.dig{}()".format(direction.title() if direction != "front" else "")
return await self.eval(code)
dig_up = lambda self: self.dig("up")
dig_down = lambda self: self.dig("down")
"""
Turtle inventory API
"""
async def get_inventory(self) -> dict[InvSlot, str | None]:
inv = await self.eval("return inventory()")
print('INVENTORY:', json.dumps(inv, indent=2))
return {i: inv.get(str(i), None) for i in range(1, 17)} # type: ignore[literal-required]
async def get_slot(self, slot: InvSlot) -> str | None:
inv = await self.get_inventory()
return inv[slot]
async def select(self, slot: InvSlot):
return await self.eval(f"turtle.select({slot})")
async def select_item(self, target: str):
target = target.lower()
inv = await self.get_inventory()
for slot, item in inv.items():
name = item['name'].lower() if item else None # type: ignore[literal-required]
if name and (target == name or name.replace('minecraft:', '') == target):
return await self.select(slot)
raise Exception(f"Item {target} not found in inventory")