Skip to content

Commit 86dfc8c

Browse files
authored
Add Gem Puzzle Environment and Renderer (#1000)
* Add Gem Puzzle Environment and Renderer * Add reset functionality and improve button management in GemPuzzleRenderer
1 parent 12e7d4b commit 86dfc8c

File tree

5 files changed

+1283
-1
lines changed

5 files changed

+1283
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ for (let i = 0; i < n; i++) {
200200
| maze | A maze on a fine grid plane. |
201201
| waterball | Moving amidst the drift of bait and poison. |
202202
| blackjack | Blackjack game. |
203+
| gem puzzle | 15 puzzle. |
203204
| draughts | Draughts game. |
204205
| reversi | Reversi game. |
205206
| gomoku | Gomoku game. |

js/platform/rl.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@ import RLRenderer from '../renderer/rl.js'
88
const LoadedRLEnvironmentClass = {}
99

1010
const AIEnv = {
11-
MD: ['grid', 'cartpole', 'mountaincar', 'acrobot', 'pendulum', 'maze', 'blackjack', 'waterball', 'breaker'],
11+
MD: [
12+
'grid',
13+
'cartpole',
14+
'mountaincar',
15+
'acrobot',
16+
'pendulum',
17+
'maze',
18+
'blackjack',
19+
'waterball',
20+
'breaker',
21+
'gem_puzzle',
22+
],
1223
GM: ['reversi', 'draughts', 'gomoku'],
1324
}
1425

js/renderer/rl/gem_puzzle.js

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import GemPuzzleRLEnvironment from '../../../lib/rl/gem_puzzle.js'
2+
3+
export default class GemPuzzleRenderer {
4+
constructor(renderer) {
5+
this.renderer = renderer
6+
this._init_menu()
7+
}
8+
9+
_init_menu() {
10+
const r = this.renderer.setting.rl.configElement
11+
r.replaceChildren()
12+
r.appendChild(document.createTextNode('Size '))
13+
const size = document.createElement('input')
14+
size.type = 'number'
15+
size.min = 2
16+
size.max = 10
17+
size.value = this.renderer.env._size[0]
18+
size.onchange = () => {
19+
this.renderer.env._size = [+size.value, +size.value]
20+
this.renderer.env._board._size = [+size.value, +size.value]
21+
this.renderer.platform.init()
22+
this.renderer.env.reset()
23+
this.renderer.setting.ml.refresh()
24+
}
25+
r.appendChild(size)
26+
}
27+
28+
init(r) {
29+
const width = 500
30+
const height = 500
31+
const base = r.appendChild(document.createElement('div'))
32+
base.style.position = 'relative'
33+
this._envrenderer = new Renderer(this.renderer.env, { width, height, g: base })
34+
this._envrenderer.init()
35+
36+
this._resetButton = document.createElement('button')
37+
this._resetButton.innerText = 'Reset'
38+
this._resetButton.onclick = async () => {
39+
this.renderer.env.reset()
40+
this._envrenderer.render()
41+
}
42+
r.appendChild(this._resetButton)
43+
44+
this._manualButton = document.createElement('button')
45+
this._manualButton.innerText = 'Manual'
46+
this._manualButton.onclick = async () => {
47+
this._game = new GemPuzzleGame(this.renderer.platform)
48+
this._autoButton.disabled = true
49+
this._manualButton.disabled = true
50+
this._resetButton.disabled = true
51+
this._cancelButton.style.display = 'inline'
52+
await this._game.start()
53+
this._autoButton.disabled = false
54+
this._manualButton.disabled = false
55+
this._resetButton.disabled = false
56+
this._cancelButton.style.display = 'none'
57+
this._game = null
58+
}
59+
r.appendChild(this._manualButton)
60+
61+
this._autoButton = document.createElement('button')
62+
this._autoButton.innerText = 'Auto'
63+
this._autoButton.onclick = async () => {
64+
this._game = new GemPuzzleGame(this.renderer.platform)
65+
this._autoButton.disabled = true
66+
this._manualButton.disabled = true
67+
this._resetButton.disabled = true
68+
this._cancelButton.style.display = 'inline'
69+
await this._game.start(true)
70+
this._autoButton.disabled = false
71+
this._manualButton.disabled = false
72+
this._resetButton.disabled = false
73+
this._cancelButton.style.display = 'none'
74+
this._game = null
75+
}
76+
r.appendChild(this._autoButton)
77+
78+
this._cancelButton = document.createElement('button')
79+
this._cancelButton.innerText = 'Cancel'
80+
this._cancelButton.onclick = async () => {
81+
this._game.cancel()
82+
this._cancelButton.style.display = 'none'
83+
}
84+
this._cancelButton.style.display = 'none'
85+
r.appendChild(this._cancelButton)
86+
}
87+
88+
render() {
89+
const displayButton = this.renderer.platform._manager._modelname ? 'none' : null
90+
this._resetButton.style.display = displayButton
91+
this._manualButton.style.display = displayButton
92+
this._autoButton.style.display = displayButton
93+
this._envrenderer.render()
94+
}
95+
}
96+
97+
class Renderer {
98+
constructor(env, config = {}) {
99+
this.env = env
100+
101+
this._size = [config.width || 200, config.height || 200]
102+
103+
this._points = []
104+
105+
this._q = null
106+
107+
this._render_blocks = []
108+
109+
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
110+
this.svg.setAttribute('width', this._size[0])
111+
this.svg.setAttribute('height', this._size[1])
112+
this.svg.setAttribute('viewbox', '0 0 200 200')
113+
if (config.g) {
114+
config.g.replaceChildren(this.svg)
115+
}
116+
}
117+
118+
init() {
119+
const height = this._size[0]
120+
const width = this._size[1]
121+
const dy = height / this.env._size[0]
122+
const dx = width / this.env._size[1]
123+
this._render_blocks = []
124+
for (let i = 0; i < this.env._size[0]; i++) {
125+
this._render_blocks[i] = []
126+
for (let j = 0; j < this.env._size[1]; j++) {
127+
const g = (this._render_blocks[i][j] = document.createElementNS('http://www.w3.org/2000/svg', 'g'))
128+
g.classList.add('grid')
129+
g.setAttribute('stroke-width', 1)
130+
g.setAttribute('stroke', 'black')
131+
g.setAttribute('stroke-opacity', 0.2)
132+
this.svg.appendChild(g)
133+
134+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
135+
rect.setAttribute('x', dx * j)
136+
rect.setAttribute('y', dx * i)
137+
rect.setAttribute('width', dx)
138+
rect.setAttribute('height', dy)
139+
rect.setAttribute('fill', 'white')
140+
g.appendChild(rect)
141+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
142+
text.classList.add('value')
143+
text.setAttribute('x', dx * (j + 0.5))
144+
text.setAttribute('y', dy * (i + 0.5))
145+
text.setAttribute('font-size', 14)
146+
text.setAttribute('user-select', 'none')
147+
text.setAttribute('dominant-baseline', 'middle')
148+
text.setAttribute('text-anchor', 'middle')
149+
g.appendChild(text)
150+
}
151+
}
152+
this.render()
153+
}
154+
155+
render() {
156+
const board = this.env._board
157+
158+
for (let i = 0; i < this.env._size[0]; i++) {
159+
for (let j = 0; j < this.env._size[1]; j++) {
160+
this._render_blocks[i][j].querySelector('text.value').replaceChildren(board.at([i, j]) ?? '')
161+
if (board.at([i, j]) === null) {
162+
this._render_blocks[i][j].querySelector('rect').setAttribute('fill', 'rgba(0, 0, 0, 0.5)')
163+
} else {
164+
this._render_blocks[i][j].querySelector('rect').setAttribute('fill', 'white')
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
class GemPuzzleGame {
172+
constructor(platform) {
173+
this._platform = platform
174+
this._env = platform.env
175+
this._cancel = false
176+
}
177+
178+
async start(auto = false) {
179+
this._platform.render()
180+
if (auto) {
181+
const path = this._env._board.solve()
182+
for (const m of path) {
183+
await new Promise(resolve => setTimeout(resolve, 50))
184+
this._env.step([m])
185+
this._platform.render()
186+
if (this._cancel) {
187+
break
188+
}
189+
}
190+
return
191+
}
192+
const { promise, resolve: cancelResolver } = Promise.withResolvers()
193+
this._cancelResolver = cancelResolver
194+
while (true) {
195+
const move = await Promise.race([
196+
promise,
197+
new Promise(resolve => {
198+
const keyDown = e => {
199+
if (e.code === 'ArrowUp') {
200+
resolve(GemPuzzleRLEnvironment.UP)
201+
} else if (e.code === 'ArrowDown') {
202+
resolve(GemPuzzleRLEnvironment.DOWN)
203+
} else if (e.code === 'ArrowLeft') {
204+
resolve(GemPuzzleRLEnvironment.LEFT)
205+
} else if (e.code === 'ArrowRight') {
206+
resolve(GemPuzzleRLEnvironment.RIGHT)
207+
}
208+
document.removeEventListener('keydown', keyDown)
209+
}
210+
document.addEventListener('keydown', keyDown)
211+
}),
212+
])
213+
if (move === null) {
214+
break
215+
}
216+
const { done } = this._env.step([move])
217+
this._platform.render()
218+
if (done) {
219+
break
220+
}
221+
await new Promise(resolve => setTimeout(resolve, 10))
222+
}
223+
}
224+
225+
cancel() {
226+
this._cancelResolver?.(null)
227+
this._cancel = true
228+
}
229+
}

0 commit comments

Comments
 (0)