帮酷LOGO
0 0 评论
文章标签:Solver  Puzzle  
Puzzles Solver

介绍

游戏解决方案是一个应用程序,我一段时间前上传到Android市场。 应用程序的想法来自一个旧的CodeProject文章。 在这篇文章中,我提出了一些已经知的游戏的Collection,以及计算机解决它们的机制。 谜题被实现为 JavaApplet,因此很容易将它们移植到Android平台上。 应用程序提供以下游戏:

  • 8 皇后:玩家需要在棋盘上放置皇后,这样就不会攻击其他玩家。
  • 在棋盘的所有方格中引导骑士并返回到起点。
  • 独奏: 跳过销子来移除它们。 除最后一个钉子外全部除去。

应用程序为所有游戏提供了变化。 它也能解决你的难题。 未使用预存储的解决方案。 应用程序通过应用一个简单的恶意搜索算法尝试计算解决方案。

在本文中,我想展示我所做的所有设计决定和我所遵循的模式,同时开发应用程序。 我希望文章读者会发现他们有用,也许会在自己的应用程序中使用它们。

用户界面图

下面的图表描述了应用程序的activity 框图。 当开始设计一个新应用程序时,尝试绘制一下应用程序是一个好主意。 这将帮助你尽早识别用户界面挑战。 它还将帮助将可视组件映射到Java类。

Wireframe diagram for Puzzle Solver

在本文中,框架框帮助显示一些重要的用户界面模式。 应用程序可以很快启动,所以不需要启动屏幕。 应用程序立即启动主屏幕,它只显示一行按钮。 这个屏幕提供了应用程序提供( 开始拼图,看分数,获取帮助) 功能的视觉提示,并允许用户使用一或者两个触摸屏访问它。 当然,你可以将第一个屏幕设置为比一行按钮更有趣,但是每个移动应用程序都很少使用。 第一个屏幕还提供了菜单,但这不是必需的。 菜单提供的功能与菜单相同。 只有关于框的内容才隐藏在它的中。 你可以在菜单中隐藏这些功能,以节省屏幕空间,而不会分散用户的注意力。

从主屏幕到谜题和分数( 每个谜题都有不同的得分屏幕)的转换是通过一个列表完成的。 按下按钮时,会出现一个带有可用谜题的列表。 另一种解决方案是使用单独的屏幕( 由一个新的Activity 实现) 来显示所有的选择。 如果有很多游戏选择,这将是非常理想的( 比如。 在线游戏,定时和非计时模式,水平选择)。

游戏引擎

游戏引擎基于一个文章我在CodeProject上写的文章,就像在 2004中所写的那样。 支持同一应用程序中多个难题的想法是创建一个 abstract 类来实现所有通用功能。 对于每个单独的难题,都有 abstract 类的具体实现。 这是 solve 方法的摘录,因为这是在 abstract 类( 为了演示目的作了轻微修改) 中实现的:

while(!searched_all && movesMade()!= movesTableSize) {
 if (!findNextMove()) {
 searched_all =!goBack();
 while(!searched_all &&
 movesMade() == 0) {
 searched_all =!goBack();
 }
 }
}

这里 while 循环是求解器算法的核心。 各个谜题实现了其中的方法,即 movesMade()goBack()findNextMove()。 这实际上是模板方法设计 Pattern 插件的一种形式。

abstractPuzzle 类还用于为单个 Puzzle 类的调用方提供接口。 只有在初始化谜题时才会出现具体的实现。 下面的类图描述了应用程序的主要类,即 PuzzleActivityPuzzleViewPuzzle

Puzzles Solver Class Diagram

PuzzleActivityonCreate 方法中,初始化一个 PuzzleView 实例。 同时创建 Puzzle 对象并将它的注入到 PuzzleView 中。

@Overridepublicvoid onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 Intent intent = getIntent();
 Bundle bundle = intent.getExtras();
 gameType = bundle.getInt("GameType", 0);
 switch(gameType) {
 case0:
 puzzle = new Q8Puzzle(this);
 break;
 case1:
 puzzle = new NumberSquarePuzzle(this);
 break;
 case2:
 puzzle = new SoloPuzzle(this);
 break;
 }
 timeCounter = new TimeCounter();
 puzzle.init();
 puzzleView = new PuzzleView(this);
 puzzleView.setPuzzle(puzzle);
 puzzleView.setTimeCounter(timeCounter);
 setContentView(puzzleView);
 timerHandler = new Handler();
 replayHandler = new Handler();
 scoresManager = ScoresManagerFactory.getScoresManager(getApplicationContext());
}

这是唯一的地方,Puzzle的具体实现出现了。 负责服务用户界面的PuzzleView 通过调用 Puzzle.drawPuzzle.onTouchEvent 方法来执行作业。 如果要添加一个新的拼图,那么唯一需要的就是一个新的拼图 case 语句。

在本文的第二版中,我增加了在Solo中创建定制板的支持。 这导致了一些特殊的处理添加:

case R.id.custom_boards:
 if (puzzle instanceof SoloPuzzle) {
 if (soloPuzzleRepository!= null && 
 soloPuzzleRepository.getCustomBoardsCount()> 0) {
 Intent editIntent = new Intent
 ("gr.sullenart.games.puzzles.SOLO_EDIT_BOARDS");
 startActivity(editIntent); 
 }
 else {
 SoloCustomBoardActivity.showDirections = true;
 Intent addIntent = new Intent
 ("gr.sullenart.games.puzzles.SOLO_ADD_BOARD");
 startActivity(addIntent);
 }
 }

但是,也可以扩展基类以支持自定义板。 毕竟,添加定制板是许多难题的共同特征。 这样,特殊的处理就会消失。

支持多个屏幕

Android在各种设备上运行,因此应用程序能够支持不同的屏幕大小和密度是很重要的。 官方文档插件提供了屏幕支持过程的全面指导。 这是不断发展的文档,每个新版本的SDK新屏幕大小和密度都在增加。

在这里介绍的实践中,我将介绍一种使用瓷砖绘制用户界面的技术。 瓦片是小矩形图像( 例如 80 x80或者 100 x100像素)。 用户界面是通过水平和垂直重复这些图像构建的。 这种技术利用了在Android中你可以轻松地动态调整图像大小的优点。 因这里,一组瓦片可以支持各种屏幕大小,同时也可以支持 portrait 和 landscape 方向。 然而,我应该注意,这种技术适合于游戏或者它的他应用程序的拼图,在用户界面上只有慢变化。 使用它快速更改用户界面可能不是有效的做法。 这里技术还提供了一种在应用程序中支持主题化的方法。

让我们从使用tile在布局中创建 background 开始。 这就像定义一个可以绘制的XML一样简单:

<?xmlversion="1.0"encoding="utf-8"?><bitmapxmlns:android="http://schemas.android.com/apk/res/android"android:src="AndroidPuzzlesSolver/@drawable/bg_tile"android:tileMode="repeat"android:dither="true"/>

然后在布局XML中:

<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="fill_parent"android:layout_height="fill_parent"android:orientation="vertical"android:background="@drawable/background">

当你从布局创建用户界面时,这就解决了这个问题。 但是,对于谜题和简单游戏,在视图的onDraw 方法上绘制一切都是很常见的。 下面的方法在 onDraw的开头调用时,在画布上绘制 background:

privatevoid drawBackgroundRepeat(Canvas canvas, Bitmap bgTile) {
 float left = 0, top = 0;
 float bgTileWidth = bgTile.getWidth();
 float bgTileHeight = bgTile.getWidth();
 while (left <screenWidth) {
 while (top <screenHeight) {
 canvas.drawBitmap(bgTile, left, top, null);
 top += bgTileHeight;
 }
 left += bgTileWidth;
 top = 0;
 }
}

为了在多个屏幕尺寸中使用图像来绘制用户界面,需要调整一些大小。 下面的图表描述了屏幕的组织。 虚线显示 background 瓦片。 因为这只是相同图像的重复,并且不需要在边缘显示完整的图像,所以不需要调整大小。 实线显示游戏牌必须放置在哪里才能形成游戏板。

Using tiles for creating the User Interface

作为例子,Solo屏幕是用下面的瓦片绘制的。 所有图像均使用 Gimp 生成。

TypeWood ThemeMarble主题
棋盘平铺
孔平铺
钉在孔上
选定的peg
选择的孔

首先让我们看看 ImageResizer 类。

publicclass ImageResizer {
 private Matrix matrix;
 publicvoid init(int oldWidth, int oldHeight, float newWidth, float newHeight) {
 float scaleWidth = newWidth/oldWidth;
 float scaleHeight = newHeight/oldHeight;
 matrix = new Matrix();
 matrix.postScale(scaleWidth, scaleHeight);
 }
 public Bitmap resize(Bitmap bitmap) {
 if (matrix == null) {
 return bitmap;
 }
 int width = bitmap.getWidth();
 int height = bitmap.getHeight();
 Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
 width, height, matrix, true);
 return resizedBitmap;
 }
}

这是一个实用工具类,提供了动态调整位图大小的方法。 需要通过提供原始位图和已经调整大小的位图的尺寸来初始化此类的实例。 这个类被应用在拼图视图的onSizeChanged 方法中。 当屏幕的大小改变时,调用 onSizeChanged 方法( 比如。 当发生方向更改或者屏幕第一次显示时,。 在这里方法中,从应用程序的资源加载平铺图像。 根据主题不同,加载了不同的图像。 加载的位图然后调整大小以适应现有屏幕并保存在内存中。 图像在画布上绘制,每个拼图的draw 方法。

publicvoid onSizeChanged(int w, int h) {
 super.onSizeChanged(w, h);
 int boardSize = (boardRows> boardColumns)? boardRows : boardColumns;
 if (w <h) {
 tileSize = (w - 10)/boardSize;
 }
 else {
 tileSize = (h - 10)/boardSize;
 }
 offsetX = (screenWidth - tileSize*boardColumns)/2;
 offsetY = (screenHeight - tileSize*boardRows)/2;
 imageResizer = new ImageResizer();
 if (theme.equals("marble")) {
 emptyImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.marble_tile);
 tileImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.wood_sphere);
 tileSelectedImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.golden_sphere);
 }
 else {
 emptyImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.wood_tile);
 tileImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.glass);
 tileSelectedImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.glass_selected);
 }
 freePosImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.hole);
 freePosAllowedMoveImage = BitmapFactory.decodeResource(context.getResources(),
 R.drawable.hole_move);
 imageResizer.init(emptyImage.getWidth(), emptyImage.getHeight(), 
 tileSize, tileSize);
 emptyImage = imageResizer.resize(emptyImage);
 freePosImage = imageResizer.resize(freePosImage);
 tileImage = imageResizer.resize(tileImage);
 tileSelectedImage = imageResizer.resize(tileSelectedImage);
 freePosAllowedMoveImage = imageResizer.resize(freePosAllowedMoveImage);
}

弹出窗口

在本文的第三个修订中,我用一个更好的弹出窗口替换了简单列表谜题选择器。 你可以在下图中比较这两个选择器的差异:

Puzzle Selectors

新的弹出窗口更具吸引力。 它也使拼图选择更快,因为它看起来更接近点,用户第一次触摸并且不灰色。 它也可以轻松的动画。 为了创建弹出窗口,从布局XML文件开始,就像对对话框或者视图所做的那样。 为了你的布局定义 background 很重要,因为默认情况下,Android PopupWindow 并没有提供一个。 在Java代码中,使 PopupWindow 显示为以下代码:

privatevoid showGameSelectPopup(View parentView) {
 LayoutInflater inflater = (LayoutInflater)
 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 View layout = inflater.inflate(R.layout.puzzle_select, null, false); 
 gameSelectPopupWindow = new PopupWindow(this);
 gameSelectPopupWindow.setTouchInterceptor(new OnTouchListener() {
 @Overridepublicboolean onTouch(View v, MotionEvent event) {
 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
 gameSelectPopupWindow.dismiss();
 gameSelectPopupWindow = null;
 return true;
 }
 return false;
 }
 });
 gameSelectPopupWindow.setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
 gameSelectPopupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
 gameSelectPopupWindow.setTouchable(true);
 gameSelectPopupWindow.setFocusable(true);
 gameSelectPopupWindow.setOutsideTouchable(true);
 gameSelectPopupWindow.setContentView(layout);
 // Set click listeners by calling layout.findViewById()// layout.findViewById(R.id.queens_select).setOnClickListener();int [] location = newint [] {0, 0};
 parentView.getLocationInWindow(location);
 int width = parentView.getWidth()/2;
 int x = location[0] - width/2;
 int y = location[1] + parentView.getHeight(); 
 gameSelectPopupWindow.setAnimationStyle(R.style.AnimationPopup);
 gameSelectPopupWindow.showAtLocation(layout, Gravity.NO_GRAVITY, x, y); 
}@Overridepublicvoid onStop() {
 if (gameSelectPopupWindow!= null) {
 gameSelectPopupWindow.dismiss();
 }
 super.onStop();
}

show 方法接受作为参数的视图,视图是启动操作的Button 或者其他项。 这允许将 PopupWindow relative 定位到用户首次单击的点。 首先,我们通过放大布局来为 PopupWindow 创建视图。 然后我们创建 PopupWindow 对象并适当配置它。 我们使用父视图的getLocationInWindow()getWidth()getHeight() 方法来正确定位 Popup。 finally,我们设置动画样式并显示 Popup 窗口。 如果用户在窗口外单击,将取消这里操作。 为了不泄漏窗口,我们需要检查 Popup 是否可以见,当 Activity 停止并关闭它时。 如果方向改变,而 Popup 可见,则可能发生这种情况。

另一个棘手的一点是动画风格。 必须在 values styles.xml 中的文件中定义该文件,并具有 below 窗体:

<resources><stylename="AnimationPopup"><itemname="@android:windowEnterAnimation">@anim/popup_show</item><itemname="@android:windowExitAnimation">@anim/popup_hide</item></style></resources>

保持分数

为了存储用户的分数,你需要一个持久化机制。 Android平台的持久性选项在文档中描述。 从这些选项中,最适合存储分数的是SQLite数据库。 下面的语句介绍了我使用的table的架构:

privatestaticfinalString DATABASE_CREATE =
 "create table Scores (_id integer primary key autoincrement," +
 "game text not null, category text not null, 
 player text not null, date text," +
 "score integer not null);";

为了从它的余的代码抽象数据库,我创建了一个 ScoresManager 类,它提供了访问分数的方法。

publicclass ScoresManager {
 publicboolean addScore(String game, String group, String player, int score);
 publicboolean isHighScore(String game, int score);
 public List<Score> getScores(String game);
 public List<Score> getScoresByGroup(String group);
}

我应该指出,Android有很多开源的分数库。 你可以找到经过良好测试的库,提供丰富的特性,包括与网页评分系统集成。 如果不要学习与 sql rtc数据库交互,最好使用这些数据库中的一个进行评分。

设置

另一个持久性需要是存储用户的选择。 由于每个谜题都有许多变化,如果应用程序记住了,用户最后一次播放的游戏。 你可以使用 SharedPreferences 轻松地在Android中实现这一点。

SharedPreferences 是在一个单独的XML文件中为每个谜题定义的。 Puzzleabstract 类定义了 configure 方法。 具体的类实现这里方法以读取用户的设置。

publicboolean configure(SharedPreferences preferences)

Android提供了 PreferenceActivity,你可以很容易地扩展它,以便创建简单的用户界面来编辑设置。

publicclass PuzzleOptionsActivity extends PreferenceActivity {
 @Overrideprotectedvoid onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 Intent intent = getIntent();
 Bundle bundle = intent.getExtras();
 int gameResources = bundle.getInt("GameResources", 0);
 addPreferencesFromResource(gameResources);
 }
}

当然,这并不是开发 SharedPreferences的唯一方法。 你可以创建一个漂亮的用户界面,使用Java代码读取和写入 SharedPreferences。 在解决方案中,我使用了一个 PreferencesScreen,而不是实际显示任何内容,以便存储用户的NAME。 当用户获得高分时,要求她输入 NAME的祝贺屏幕会预先填充以前使用过的NAME。

本地化

应用在英语,德语和希腊文中被本地化。 本地化元素是应用程序(。存储在 strings.xml 值,值和值el文件夹)。应用程序(。存储在可以绘制,可以绘制的de文件夹中)的logo 和应用程序页面的help,它们在资产文件夹( 以后更详细的细节) stored作为HTML页面存储。

帮助

提供应用程序的使用说明是非常重要的。 当然,应用程序应该很容易使用和界面直观,使用户可以开始使用应用程序。 这不是 Having 很好的帮助页面的原因。 有些用户可能不熟悉你的游戏或者应用程序的概念,需要一些指导。 此外,你可能会在帮助页面中提供一些提示和技巧,即使是高级用户也会发现帮助。

我相信使用 web view来显示HTML文件是在Android应用程序中实现帮助页面的理想解决方案。 需要的Java代码数量很少,然后你只需要在 assets 文件夹中使用一个或者多个HTML文件。 编写HTML帮助页面非常容易。 对文本进行样式化和添加图像很简单。 你可能还可以使用描述你的应用程序的现有页面。 为帮助页面使用HTML很容易,因为这样很多应用程序仍然喜欢使用它的他帮助系统。

如果你想要支持多种语言,那么你将需要不同版本的HTML页面。 将所有页面放置在 assets 文件夹中。 然后在 strings.xml 中,为每种语言定义页面的NAME:

<stringname="help_file_name">index.html</string>

WebView 中加载该页时,从资源中读取 NAME:

webview.loadUrl("file:///android_asset/" +
 getResources().getString(R.string.help_file_name));

低调广告

这对于移动应用非常普遍,尤它的是对于免费给出的这些广告,显示某种广告。 这些广告可以为开发者带来收入,同时还允许她自由地分发应用程序。 有许多可以供选择的广告框架。 对于 PuzzleSolver,我选择了 AdMob。 无论你选择什么框架,你都需要确保广告不会分散用户的注意力。 在游戏中,广告应该只显示在辅助屏幕( 游戏选择,得分,帮助) 中,而不能显示在游戏屏幕中。 广告不应该分散用户玩游戏的注意力。

为了能够在多个 Activity 中显示广告,而不重复相同的代码,我创建了一个名为 AdsManager的实用工具类。 AdsManager 实现 AdListener 并提供 addAdsView 方法,它设置发布者 id。测试设备',并将请求添加到视图中。

publicclass AdsManager implements AdListener {
 privateString publisherId = "your publisher id here";
 publicvoid addAdsView(Activity activity, LinearLayout layout) {
 AdView adView;
 int screenLayout = activity.getResources().getConfiguration().screenLayout;
 if ((screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)> = 3) {
 adView = new AdView(activity, AdSize.IAB_LEADERBOARD, publisherId);
 }
 else {
 adView = new AdView(activity, AdSize.BANNER, publisherId);
 }
 layout.addView(adView);
 AdRequest request = new AdRequest();
 request.addTestDevice(AdRequest.TEST_EMULATOR);
 request.addTestDevice("Your test device id here - Find the id in Log Cat");
 adView.loadAd(request);
 } 

注意,对于不同的屏幕大小,有一个 differentation。 对于小型和普通屏幕,( 值 1和 2 ) 显示横幅。 对于大型和超大屏幕( 值 3和 4 ),将显示一个排行榜。

这里方法在显示广告的每个 Activity的onCreate 方法中调用:

LinearLayout layout = (LinearLayout)findViewById(R.id.banner_layout);
(new AdsManager()).addAdsView(this, layout);

历史记录

  • ::
    • 增加了对三角形板的支持和单独的对角线移动。
    • 板上按钮( 撤消,重新启动,解决)。
    • 当没有剩余的移动时,会出现一条消息。
  • July 2012: 固定 Bug 新 icon。 使用最新版本的Android工具。 jar 包含在libs文件夹中。
  • April 2012: 新得分屏幕。 更好地支持大屏幕,包括AdMob的更改。 一个新的独奏主题。在骑士游览中显示连线的选项。 消除了大部分Lint警告。
  • January 2012: 为拼图选择添加了新的弹出窗口
  • January: 在Solo中增加了创建定制板的支持。 Bug 固定
  • 文章的第一个版本


文章标签:Solver  Puzzle  

Copyright © 2011 HelpLib All rights reserved.    知识分享协议 京ICP备05059198号-3  |  如果智培  |  酷兔英语