基于scala-swing构建的推箱子游戏

game

推箱子游戏属于一种解谜问题。开发思路比较简单,并且有比较正规定义。

首先定义地图

地图格式可以参考这里

建立地图文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    #####
# #
#$ #
### $##
# $ $ #
### # ## # ######
# # ## ##### ..#
# $ $ ..#
##### ### #@## ..#
# #########
#######

############
#.. # ###
#.. # $ $ #
#.. #$#### #
#.. @ ## #
#.. # # $ ##
###### ##$ $ #
# $ $ $ $ #
# # #
############
...

更多内容可以参考源码实现。

引入依赖

1
val scalaSwing = "org.scala-lang.modules" %% "scala-swing" % scalaSwingV

构建元素

包括墙壁、玩家、箱子等。

1
2
3
4
5
6
7
8
9
trait WorldElement

case class Box() extends WorldElement
case class BoxOnGoalSquare() extends WorldElement
case class Player() extends WorldElement
case class PlayerOnGoalSquare() extends WorldElement
case class Floor() extends WorldElement
case class GoalSquare() extends WorldElement
case class Wall() extends WorldElement

构建世界

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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

import scala.collection.mutable.ListBuffer
import scala.io.Source
import scala.swing.Publisher
import scala.swing.event.Event
import scala.swing.event.Key._

case class PlayerMove() extends Event
case class PlayerHint() extends Event

object World extends Publisher {
type Level = Array[Array[WorldElement]]
val levels: ListBuffer[Level] = ListBuffer[Level]()

val MAX_WIDTH = 19
val MAX_HEIGHT = 16

var currentLevel: Level = _

var manX = 0
var manY = 0

var toGoX = 0
var toGoY = 0

var nb_move = 0

def loadLevel(num: Int) {
currentLevel = copyMap(levels(num))
nb_move = 0
publish(PlayerMove())
}

def onKeyPress(keyCode: Value) {
keyCode match {
case Left => move(-1, 0)
case Right => move(1, 0)
case Up => move(0, 1)
case Down => move(0, -1)
case _ =>
}
}

def fromString(stringWorld: String): Level = {
val level = Array.ofDim[WorldElement](MAX_WIDTH, MAX_HEIGHT)
val listStringMap = stringWorld.toList
var wEl: WorldElement = null
var x, y = 0
for (i <- listStringMap.indices) {
listStringMap(i) match {
case '#' => wEl = Wall()
case '.' => wEl = GoalSquare()
case '$' => wEl = Box()
case '@' => wEl = Player()
case '+' => wEl = PlayerOnGoalSquare()
case '*' => wEl = BoxOnGoalSquare()
case ' ' => wEl = Floor()
case '\n' =>
y += 1
x = 0
case _ =>
}
if (wEl != null) {
level(x)(y) = wEl
x += 1
wEl = null
}
}
level
}

def fromFile(filePath: String) {
var lines: ListBuffer[String] = ListBuffer()
val stream = Source.fromResource(filePath)
for (line <- stream.getLines()) {
line match {
case "" =>
levels += fromString(lines.mkString("\n"))
lines = ListBuffer()
case _ => lines += line
}
}
}

private def locateMan() {
for {
i <- currentLevel.indices
j <- currentLevel(i).indices
} {
if (currentLevel(i)(j).isInstanceOf[Player]) {
manX = i
manY = j
return
}
}
}

private def move(x: Int, y: Int) {
locateMan()
toGoX = x
toGoY = y
nb_move += 1
currentLevel(manX + toGoX)(manY - toGoY) match {
case _: Box => moveBox()
case _: Floor => movePlayerToFloor()
case _: GoalSquare => movePlayerToGoalSquare()
case _: BoxOnGoalSquare => moveBox()
case _ => nb_move -= 1
}
publish(PlayerMove())
}

private def moveBox() {
val (x, y) = nextCase(manX, manY)
val (bhCrateX, bhCrateY) = nextCase(x, y)
currentLevel(bhCrateX)(bhCrateY) match {
case _: Floor =>
currentLevel(bhCrateX)(bhCrateY) = Box()
moveManToSpaceOrStorage(x, y)
case _: GoalSquare =>
currentLevel(bhCrateX)(bhCrateY) = BoxOnGoalSquare()
moveManToSpaceOrStorage(x, y)
case _ =>
}
}

private def movePlayerToFloor() {
letSpaceOrStorage()
manX = manX + toGoX
manY = manY - toGoY
currentLevel(manX)(manY) = Player()
}

private def movePlayerToGoalSquare() {
letSpaceOrStorage()
manX = manX + toGoX
manY = manY - toGoY
currentLevel(manX)(manY) = PlayerOnGoalSquare()

}

private def letSpaceOrStorage() {
currentLevel(manX)(manY) match {
case _: Player =>
currentLevel(manX)(manY) = element.Floor()
case _: PlayerOnGoalSquare =>
currentLevel(manX)(manY) = GoalSquare()
}
}

private def moveManToSpaceOrStorage(x: Int, y: Int) {
currentLevel(x)(y) match {
case _: Box =>
movePlayerToFloor()
case _: BoxOnGoalSquare =>
movePlayerToGoalSquare()
}
}

private def nextCase(x: Int, y: Int) = {
(x + toGoX, y - toGoY)
}

private def copyMap(originMap: Level): Level = {
val copyMap = Array.ofDim[WorldElement](MAX_WIDTH, MAX_HEIGHT)
for {
x <- originMap.indices
y <- originMap(x).indices
} copyMap(x)(y) = originMap(x)(y)
copyMap
}
}

设计UI

比较简单,加载到地图元素后,匹配到元素直接画即可。我这里使用了Graphics2D直接画,如果有合适的图片元素,可以直接转BufferedImage。然后调用g.drawImage相对会更好看一些。

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

import java.awt.{Color, Graphics2D, RenderingHints}

import cn.galudisu.game.World
import cn.galudisu.game.element._

import scala.swing.Panel
import scala.swing.Swing._
import scala.swing.event._

case class LevelFinish() extends Event

class UIWorld() extends Panel {
var currentLevelNum: Int = _

background = Color.white
preferredSize = (500, 400)
focusable = true
listenTo(keys)
reactions += {
case KeyTyped(_, 't', _, _) =>
publish(LevelFinish())
case KeyTyped(_, 'r', _, _) =>
restart()
case KeyPressed(_, key, _, _) =>
World.onKeyPress(key)
repaint()
case _: FocusLost => repaint()
}
var success = true

def loadWorld(levelNum: Int) {
currentLevelNum = levelNum
World.loadLevel(levelNum)
repaint()
requestFocus()
}

def nextLevel() {
loadWorld(currentLevelNum + 1)
}

def restart() {
World.loadLevel(currentLevelNum)
repaint()
}

override def paintComponent(g: Graphics2D) {
g.clearRect(0, 0, size.width, size.height)
val level = World.currentLevel
for (x <- level.indices) {
for (y <- level(x).indices if level(x)(y) != null) {
level(x)(y) match {
case Box() =>
g.setColor(new Color(165, 130, 90))
g.fillRect((x + 1) * 20, (y + 1) * 20, 20, 20)
g.fill3DRect((x + 1) * 20, (y + 1) * 20, 20 - 1, 20 - 1, true)
g.setColor(new Color(165, 165, 165))
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.drawLine((x + 1) * 20 + 4, (y + 1) * 20 + 4, (x + 1) * 20 + 20 - 4, (y + 1) * 20 + 20 - 4)
g.drawLine((x + 1) * 20 + 4, (y + 1) * 20 + 20 - 4, (x + 1) * 20 + 20 - 4, (y + 1) * 20 + 4)
case BoxOnGoalSquare() =>
g.setColor(new Color(255, 20, 20))
g.fillRect((x + 1) * 20, (y + 1) * 20, 20, 20)
g.fill3DRect((x + 1) * 20, (y + 1) * 20, 20 - 1, 20 - 1, true)
case Player() =>
g.setColor(Color.WHITE)
g.draw3DRect((x + 1) * 20, (y + 1) * 20, 20 - 1, 20 - 1, true)
g.setColor(Color.GREEN)
g.fill3DRect((x + 1) * 20 + 1, (y + 1) * 20 + 1, 20 - 2, 20 - 2, true)
case PlayerOnGoalSquare() =>
g.setColor(new Color(120, 160, 160))
g.draw3DRect((x + 1) * 20, (y + 1) * 20, 20 - 1, 20 - 1, true)
g.setColor(new Color(20, 250, 20))
g.fill3DRect((x + 1) * 20 + 1, (y + 1) * 20 + 1, 20 - 2, 20 - 2, true)
case GoalSquare() =>
g.setColor(new Color(90, 160, 90))
g.drawOval((x + 1) * 20 + 5, (y + 1) * 20 + 5, 10, 10)
g.fillOval((x + 1) * 20 + 5, (y + 1) * 20 + 5, 10, 10)
case Wall() =>
g.setColor(new Color(150, 150, 150))
g.fillRect((x + 1) * 20, (y + 1) * 20, 20, 20)
g.fill3DRect((x + 1) * 20, (y + 1) * 20, 20 - 1, 20 - 1, true)
case Floor() =>
g.setColor(Color.WHITE)
}
level(x)(y) match {
case Box() =>
success = false
case _ =>
}
}
}
if (success) {
publish(LevelFinish())
} else {
success = true
}
}
}

编写主程序入口

主程序需要加载地图信息,设计按钮和处理事件等。

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

import cn.galudisu.game.World.Level
import cn.galudisu.game.ui.{LevelFinish, UIWorld}
import javax.swing.UIManager

import scala.swing.BorderPanel.Position
import scala.swing.ListView._
import scala.swing._
import scala.swing.event._

object Sokoban extends SimpleSwingApplication {
activeNimbus()

World.fromFile("worlds.txt")

val uiWorld = new UIWorld

val uiChooseLevel: FlowPanel = new FlowPanel {
val comboBox: ComboBox[Level] = new ComboBox(World.levels) {
renderer = Renderer(el => World.levels.indexOf(el))
focusable = false
}
val loadLevelButton: Button = new Button {
text = "Load level"
focusable = false
}

val restartButton: Button = new Button {
text = "Restart"
focusable = false
}

contents += new Label("Change level")
contents += comboBox
contents += loadLevelButton
contents += new Separator()
contents += restartButton

listenTo(loadLevelButton, restartButton)

reactions += {
case ButtonClicked(`loadLevelButton`) =>
val levelNum = World.levels.indexOf(comboBox.selection.item)
uiWorld.loadWorld(levelNum)
case ButtonClicked(`restartButton`) =>
uiWorld.restart()
}
}

val nbMoveLabel: Label = new Label() {
listenTo(World)
reactions += {
case PlayerMove() =>
text = "NB Move: " + World.nb_move
}
}

val uiStats: FlowPanel = new FlowPanel {
nbMoveLabel.text = "NB Move:"
contents += nbMoveLabel
}

val borderPanel: BorderPanel = new BorderPanel {
add(uiChooseLevel, Position.North)
add(uiWorld, Position.Center)
add(uiStats, Position.South)

uiWorld.loadWorld(0)

listenTo(uiWorld)

reactions += {
case LevelFinish() =>
val result = Dialog.showOptions(
this,
message = "Success, level finish with " + World.nb_move + " move.",
title = "Level Finished!",
messageType = Dialog.Message.Question,
optionType = Dialog.Options.YesNo,
entries = Seq("Next level", "Ok"),
initial = 1
)
result match {
case Dialog.Result.Yes =>
uiWorld.nextLevel()
case _ =>
}
}
}

def top: MainFrame = new MainFrame {
title = "Sokoban Game"
contents = borderPanel
centerOnScreen()
}

def activeNimbus() {
for (info <- UIManager.getInstalledLookAndFeels) {
if ("Nimbus".equals(info.getName)) {
UIManager.setLookAndFeel(info.getClassName)
return
}
}
}
}

如果觉得游戏功能单一,可以试试添加一个“提示”按钮,协助游戏玩家解题。可以参考这里

Copris1 是一门约束编程(Constraint programming)语言。


  1. [http://bach.istc.kobe-u.ac.jp/copris)