目录
项目名称
Sudoku(数独游戏)
项目概述
数独是源自18世纪瑞士的一种数学游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。设计开发一个简单的安卓版数独游戏。
项目要求
-
设计完整的APP结构,包括以下页面
- 引导页面
- 主页面
- 关卡选择页面
- 游戏页面
- 排行榜页面
- “关于”页面
-
游戏共24关,每六个一组,分为四个难度
-
在游戏页面,若玩家填入的数字不合法,用红色字体表示,合法则用蓝色字体表示,游戏初始化的数字用黑色字体。
-
玩家完成关卡后,显示祝贺信息及用时,并将完成日期,完成关卡,及用时写入数据库。
-
排行榜页面中,显示玩家所有完成关卡的游戏记录,并且关卡,用时均按升序进行二级排序。
设计开发
引导页面
该页面只起引导作用,即用户打开程序,显示载入图片,约 3s 后自动跳转至主页面。具体实现如下
- 新建一个空活动,GuideActivity,并设置为 launch_activity ,作为引导页面。
- 在活动的布局文件中不需要其他控件,仅设置背景为载入图片
- 设置该活动全屏显示,实现方法如下 (使用该方法需要 1 中的 GuideActivity 直接继承自 Activity 类)
//全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
注:后文中涉及全屏显示均使用以上代码
- 设置计时器以实现自动跳转
//定义一个用来打开MainActivity的Intent
Intent intent = new Intent(GuideActivity.this, MainActivity.class);
//设置计时器,等待3s后启动活动
Timer timer = new Timer();
TimerTask tast = new TimerTask() {
@Override
public void run() {
startActivity(intent);
}
};
timer.schedule(tast, 3000);
- 解决一些其他的问题
因为该页面只起引导作用,所以只需在打开应用时显示,退出应用时应不再显示。若不做处理,在主页面点击返回按钮,或点击退出时,将再次跳转至该活动,解决方法:- 在 GuideActivity 类中设置一个静态变量 state 并初始化为0;
- 重写该活动的 OnPause(),OnStop()方法,当这些方法被调用时将 state 的值改为1;
- 重写该活动的 OnCreat() 方法时,先判断,若 state == 1 则直接退出活动;
主页面
该页面包括游戏标题及四个按钮
- 开始游戏
- 排行榜
- 关于
- 退出游戏
实现如下
- 新建活动 MainActivity ,在其布局文件中添加一个 ImageView 来显示游戏的标题图片。
- 在布局文件中添加四个 Button 分别对应上述四个按钮。
- 为按钮添加监听事件。
例:
Button btn1= findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, LevelChooseActivity.class);
startActivity(intent);
}
});
- 重写 OnKeyDown() 方法为返回按钮添加“再按一次退出程序”功能
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK
&& event.getAction() == KeyEvent.ACTION_DOWN) {
if ((System.currentTimeMillis() - exitTime) > 2000) {
//若连续点击时间间隔大于2s则弹出提示
Toast.makeText(getApplicationContext(), "再按一次退出程序", Toast.LENGTH_SHORT).show();
//记录当前点击时间
exitTime = System.currentTimeMillis();
//若连续点击时间间隔小于2s则直接退出
} else {
finish();
}
return true;
}
return super.onKeyDown(keyCode, event);
}
“关于”页面
该页面显示游戏介绍,版本信息,作者联系方式等信息,只需要一个 TextView 即可。
关卡选择页面
该页面包含24个按钮,点击按钮便跳转至对应关卡。
-
新建活动 LevelChooseActivity ,并在布局文件中添加24个 Button 并设置页面背景色及按钮背景色。
-
在类中创建一个 Button 数组来存储24个按钮。并以此为其设置监听事件,使得点击按钮时打开游戏页面。
for (int i = 0; i < levels.length; i++) { final int t = i + 1;//表示关卡 levels[i].setOnClickListener(new View.OnClickListener() { public void onClick(View v) { Intent intent=new Intent(LevelChooseActivity.this,GameActivity.class); //将关卡信息传入GameActivity intent.putExtra("level",t); startActivity(intent); } });
-
关于按钮的尺寸及位置
考虑到不同手机屏幕的大小不同,应动态的设置按钮的大小位置,实现如下- 得到屏幕尺寸
//得到屏幕尺寸 WindowManager wm = (WindowManager) this .getSystemService(Context.WINDOW_SERVICE); width = wm.getDefaultDisplay().getWidth(); height = wm.getDefaultDisplay().getHeight(); //每行四个按钮 size = width / 4.0F;
- 计算按钮大小并动态设置
float white = size * 0.2F;//2*margin float color = size * 0.8F;//按钮边长 //获得导航栏高度 Resources resources = this.getResources(); int resourceId = resources.getIdentifier("navigation_bar_height","dimen", "android"); int h = resources.getDimensionPixelSize(resourceId); float white_y = (height-6*color-1.5F*h)/6.0F; //设置Button尺寸 TableRow.LayoutParams layoutParams = new TableRow.LayoutParams(); layoutParams.width=(int)color; layoutParams.height = (int)color; layoutParams.leftMargin=(int)(white*0.5F); layoutParams.rightMargin=(int)(white*0.5F); layoutParams.topMargin=(int)(white_y*0.5F); layoutParams.bottomMargin=(int)(white_y*0.5F); for(int i=0;i<24;i++){ levels[i].setLayoutParams(layoutParams); }
游戏页面
游戏页面为游戏的主体部分,自定义一个控件 SudoView 来绘制游戏界面,新建一个 Game 类实现游戏逻辑。
Game类
该类控制游戏逻辑,实现初始化游戏,设置计时器,判断玩家填数是否合法,判断游戏是否结束等方法。
package com.mahaoyuan.sudoku;
public class Game {
//储存数独的初始情况用0表示空位
private final String[] str = new String[24];
//表示当前关卡
private int level;
//对应数独的81个格子,根据玩家操作更新。
private int sudoku[] = new int[81];
//初始数组,不随玩家操作更新,即记录玩家不可更改的格子
private int initial[] = new int[81];
//表示某格子不能填哪些数
private int used[][][] = new int[9][9][];
//记录游戏开始时间
private long time;
public Game(int Level){
level = Level;
getString();
sudoku = StringtoArray(str[Level-1]);
initial=StringtoArray(str[Level-1]);
//得到当前系统时间
time=System.currentTimeMillis();
calculateAllUsedTiles();
}
//得到游戏开始时间
public long getTime(){return time;}
//得到当前关卡
public int getLevel(){return level;}
//得到x行y列的格子中的值
private int getTile(int x,int y){
return sudoku[9*y+x];
}
//初始化24个关卡
private void getString(){
str[0]="360000000004230800000004200070460003820000014500013020001900000007048300000000045";
str[1]="005020109018094003060010007690850030002100800030400051700080090500270310106040700";
str[2]="600003007509082010070400082130007800080009030005320071950001020060870104800200003";
str[3]="030901600050600830790004002040020050807010306010098020900500083073009060002103070";
str[4]="480300102070460300006200700210090800007804900004003071008002400001035090603008017";
str[5]="950400068600107004003060700040080071020905030570001090008010600100604007760002013";
str[6]="207600001100400070003008090008000609004900500905006200070300100050001004300002708";
str[7]="006004010020060040040080200600100302300706005204009006007050030090010050010900600";
str[8]="007000600006832700020000080060504090100000007090703010050000070004156200008000400";
str[9]="700108005020040010001000700600504002030070040400803006004000200070090080900307004";
str[10]="007314900000600001000059408080000049000805000190000060201960000500001000003572100";
str[11]="007406000030109002008000150020800060304000907060004030059000400100605080000703600";
str[12]="700802004080600200090040600000006002050008030400000000007020080009007060100503007";
str[13]="050800010000001700080004900500002001200130008800900007008700020005200000010009040";
str[14]="009600000600050070005100008030200400080090050006530010100007800050080006000005300";
str[15]="400102003008600050010005400050400800000000000001900020007500090030006100100304005";
str[16]="200007060006504009700002040900050000004008700000000006050800004100206500080700001";
str[17]="060200070100005020005046100070000000600300009000010040004120900030500004090007010";
str[18]="020000900060200007700400000005700009800500006600000300000006002100005080003000040";
str[19]="400000080060100300090800000002060900040000030008002700000006010001007050020000006";
str[20]="020000000500007001008006002009100040200000006030070900800500200600300005000000090";
str[21]="179000000465000000328000000000000000000000000000000000000000653000000794000000812";
str[22]="005800700003700090009000000001004030020900070080000100000000900040002600006001800";
str[23]="950800000104009200000340000005000090308070506020000700000058000007200603000007012";
}
//将x行y列的格子中的值转换为字符串
public String getTileString(int x,int y){
int v=getTile(x,y);
if(v==0)
return "";
else
return String.valueOf(v);
}
//将字符串数组转换为数字
protected int[] StringtoArray(String str){
int [] sudo=new int[81];
for(int i=0;i<81;i++)
sudo[i]=str.charAt(i)-'0';
return sudo;
}
//计算x行y列的格子中不能在填的数字
public int[] calculateUsedTile(int x,int y){
int c[]=new int[9];
//找出所在行已经填过的数字
for (int i=0;i<9;i++) {
if (i == y)
continue;
int t = getTile(x, i);
if (t != 0)
c[t - 1] = t;
}
//找出所在列已经填过的数字
for (int i=0;i<9;i++) {
if (i == x)
continue;
int t=getTile(i,y);
if(t!=0)
c[t-1]=t;
}
//找出所在矩形已经填过的数字
int startx=(x/3)*3;
int starty=(y/3)*3;
for(int i=startx;i<startx+3;i++)
for (int j=starty;j<starty+3;j++){
if(i==x&&j==y)
continue;
int t=getTile(i,j);
if(t!=0)
c[t-1]=t;
}
int nused=0;
for(int t:c)
if(t!=0)
nused++;
int cc[]=new int[nused];
nused=0;
for(int t:c)
if(t!=0)
cc[nused++]=t;
return cc;
}
//计算used数组
public void calculateAllUsedTiles(){
for(int i=0;i<9;i++)
for (int j=0;j<9;j++)
used[i][j]=calculateUsedTile(i,j);
}
//将x行y列的格子中的值改为value
protected void setTile(int x,int y,int value){
if(value==10)
value=0;
sudoku[y*9+x]=value;
}
//判断玩家是否可更改x行y列的格子的值
public boolean isEditable(int x,int y){
if(initial[x+9*y]==0)
return true;
else
return false;
}
//判断玩家所填数字是否合法
public boolean isValid(int x,int y){
calculateUsedTile(x,y);
for(int t:used[x][y])
//若所填数字在used数组中出现过则不合法
if(sudoku[9*y+x]==t)
return false;
return true;
}
//判断游戏是否结束
public boolean isFinished(){
calculateAllUsedTiles();
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
if(getTile(i,j)==0||!isValid(i,j))
//若有格子为空,或所填数字不合法,则游戏未结束
return false;
return true;
}
}
SudoView类
该类继承自 View 类,完成游戏界面绘制,触碰事件响应等。
package com.mahaoyuan.sudoku;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.View;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SudoView extends View {
public SudoView(Context context) {
super(context);
}
//记录每个格子的尺寸
private float width;
private float height;
//记录触摸位置
private int selectx;
private int selecty;
private Game game;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
this.width=w/9f;
this.height=h/9f;
super.onSizeChanged(w, h, oldw, oldh);
}
//重写OnDraw()方法,绘制游戏界面
@Override
protected void onDraw(Canvas canvas) {
//绘制背景颜色
Paint background=new Paint();
background.setARGB(175,196,224,225);
canvas.drawRect(0,0,getWidth(),getHeight(),background);
//格子线条画笔
Paint light=new Paint();
light.setARGB(100,7,152,199);
//加深线条画笔
Paint hilite=new Paint();
hilite.setARGB(100,109,173,226);
//大矩形的边界线条,更粗。
Paint dark=new Paint();
dark.setARGB(255,25,25,112);
//绘制线条
for(int i=0;i<=9;i++)
{
//细线
canvas.drawLine(0,i*height,getWidth(),i*height,light);
canvas.drawLine(0,i*height+1,getWidth(),i*height+1,light);
canvas.drawLine(0,i*height+2,getWidth(),i*height+2,hilite);
canvas.drawLine(i*width,0,i*width,getHeight(),light);
canvas.drawLine(i*width+1,0,i*width+1,getHeight(),light);
canvas.drawLine(i*width+2,0,i*width+2,getHeight(),hilite);
//粗线
if(i%3==0)
{
canvas.drawLine(i*width,0,i*width,getHeight(),dark);
canvas.drawLine(i*width+1,0,i*width+1,getHeight(),dark);
canvas.drawLine(i*width+2,0,i*width+2,getHeight(),hilite);
canvas.drawLine(0,i*height,getWidth(),i*height,dark);
canvas.drawLine(0,i*height+1,getWidth(),i*height+1,dark);
canvas.drawLine(0,i*height+2,getWidth(),i*height+2,hilite);
}
}
//单独绘制最下面的粗线
canvas.drawLine(0,10*height,getWidth(),10*height,dark);
canvas.drawLine(0,10*height+1,getWidth(),10*height+1,dark);
canvas.drawLine(0,10*height+2,getWidth(),10*height+2,hilite);
//数字画笔
Paint number=new Paint();
number.setStyle(Paint.Style.FILL);
number.setTextSize(height*0.75f);
number.setTextAlign(Paint.Align.CENTER);
//控制数字大小
Paint.FontMetrics fm=number.getFontMetrics();
float x=width/2;
float y=height/2-(fm.ascent+fm.descent)/2;
//绘制数字
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
{
//初始化的数字,不可编辑
if(!game.isEditable(i,j))
number.setColor(Color.BLACK);
//不合法的数字
else if(!game.isValid(i,j))
number.setColor(Color.RED);
//合法数字
else
number.setColor(Color.BLUE);
canvas.drawText(game.getTileString(i,j),i*width+x,j*height+y,number);
}
super.onDraw(canvas);
}
//重写 OnTouchEvent()
@Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction()!=MotionEvent.ACTION_DOWN)
return super.onTouchEvent(event);
//得到点击位置
selectx=(int)(event.getX()/width);
selecty=(int)(event.getY()/height);
//若点击位置小于零或处于不可编辑格子,不进行任何操作
if(selecty<0||!game.isEditable(selectx,selecty))
return false;
//显示数字显示页面
KeyDialog keyDialog=new KeyDialog(getContext());
keyDialog.show();
setListeners(keyDialog);
return true;
}
//设置监听事件
public void setListeners(final KeyDialog keyDialog){
for(int i=0;i<keyDialog.keys.length;i++) {
final int t = i + 1;
keyDialog.keys[i].setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
setSelectedTile(t);
keyDialog.dismiss();
//如果游戏结束,调用Finish()方法
if(game.isFinished())
Finish();
}
});
}
}
//填入数字,更新game
public void setSelectedTile(int tile){
game.setTile(selectx,selecty,tile);
game.calculateAllUsedTiles();
this.invalidate();
}
//设置游戏关卡
public void setGame(int level) {
game=new Game(level);
}
//结束游戏
private void Finish(){
//计算游戏时间
long t = (System.currentTimeMillis()-game.getTime())/1000;
//弹出结束对话框
FinishDialog finishDialog=new FinishDialog(getContext());
finishDialog.setTime(t);
finishDialog.show();
//将本局游戏信息写入数据库
MySQLiteOpenHelper mySQLiteOpenHelper = new MySQLiteOpenHelper(getContext());
SQLiteDatabase mydatebase = mySQLiteOpenHelper.getWritableDatabase();
ContentValues record = new ContentValues();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
Date date = new Date(System.currentTimeMillis());
record.put("date",simpleDateFormat.format(date));
record.put("level",game.getLevel());
record.put("time",t);
mydatebase.insert("Rank",null,record);
}
}
注:FinishDialog 和 Keypad类及布局需自行定义
排行榜页面
该页面显示玩家所有完成关卡的游戏记录,并对用时排序。,主体用 ListView 实现
- 新建 RankListActivity 活动,在布局页面中加入一个 TableLayout 其中包括三个 TextView 作为排行榜题头,下面加入一个线性布局,里面是 ListView
<TableLayout
android:id="@+id/tableLayout"
android:background="@color/colorRank"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:stretchColumns="*"
app:layout_constraintBottom_toTopOf="@+id/linearLayout">
<TableRow>
<TextView
android:id="@+id/date"
android:background="@color/colorRankRow"
android:text="完成日期"
android:textAlignment="center"
android:textSize="25sp" />
<TextView
android:id="@+id/level"
android:background="@color/colorRankRow"
android:text="关卡"
android:textAlignment="center"
android:textSize="25sp" />
<TextView
android:id="@+id/usetime"
android:background="@color/colorRankRow"
android:text="用时"
android:textAlignment="center"
android:textSize="25sp" />
</TableRow>
</TableLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
app:layout_constraintTop_toBottomOf="@+id/tableLayout">
<ListView
android:id="@+id/ranklist"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
</LinearLayout>
- 在 RankListActivity 中动态设置题头 TextView 尺寸
//得到屏幕尺寸
WindowManager wm = (WindowManager) this
.getSystemService(Context.WINDOW_SERVICE);
width = wm.getDefaultDisplay().getWidth();
TableRow.LayoutParams layoutParams_date = new TableRow.LayoutParams();
layoutParams_date.width=(int)(width*0.6F);
layoutParams_date.rightMargin=3;
layoutParams_date.leftMargin=3;
layoutParams_date.topMargin=3;
layoutParams_date.bottomMargin=3;
TextView tv_date = findViewById(R.id.date);
tv_date.setLayoutParams(layoutParams_date);
TableRow.LayoutParams layoutParams_level = new TableRow.LayoutParams();
layoutParams_level.width=(int)(width*0.2F);
layoutParams_level.rightMargin=3;
layoutParams_level.leftMargin=3;
TextView tv_level = findViewById(R.id.level);
tv_level.setLayoutParams(layoutParams_level);
TableRow.LayoutParams layoutParams_time = new TableRow.LayoutParams();
layoutParams_time.width=(int)(width*0.2F);
layoutParams_time.leftMargin=3;
layoutParams_time.rightMargin=3;
TextView tv_time = findViewById(R.id.usetime);
tv_time.setLayoutParams(layoutParams_time);
}
- 新建 myAdapter 类继承自 SimpleCursorAdapter 类,重写 getView() 方法,格式化设置 ListView 的 Item 中TextView 的大小,以适应不同尺寸的屏幕。
public class myAdapter extends SimpleCursorAdapter {
private float width;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position,convertView,parent);
View db_date = view.findViewById(R.id.db_date);
View db_level = view.findViewById(R.id.db_level);
View db_time = view.findViewById(R.id.db_time);
LinearLayout.LayoutParams linearParams0 = (LinearLayout.LayoutParams)db_date.getLayoutParams();
linearParams0.width = (int)(width*0.6F);
db_date.setLayoutParams(linearParams0);
LinearLayout.LayoutParams linearParams1 = (LinearLayout.LayoutParams)db_level.getLayoutParams();
linearParams1.width = (int)(width*0.2F);
db_level.setLayoutParams(linearParams1);
LinearLayout.LayoutParams linearParams2 = (LinearLayout.LayoutParams)db_time.getLayoutParams();
linearParams2.width = (int)(width*0.2F);
db_time.setLayoutParams(linearParams2);
return view;
}
- 从数据库中读取数据,并显示在 ListView 中
MySQLiteOpenHelper mySQLiteOpenHelper = new MySQLiteOpenHelper(this);
SQLiteDatabase db = mySQLiteOpenHelper.getReadableDatabase();
String sql = "SELECT date as _id, level, time FROM Rank ORDER BY level, time;";
Cursor cursor = db.rawQuery(sql,null);
myAdapter adapter =
new myAdapter(this,R.layout.item,cursor,
new String[]{"_id","level","time"},
new int[]{R.id.db_date,R.id.db_level,R.id.db_time});
adapter.setWidth(width);
ListView listview = findViewById(R.id.ranklist);
listview.setAdapter(adapter);
项目展示
项目总结
- 第一次 Android 开发实践,过程中遇到了不少问题,最终都依靠 Google 一一解决。
- 因为缺少经验,许多代码的实现都是只要实现功能就好,也许不是经典的,或者通用的做法。
- 部分代码的实现只是学到了为了实现对应功能,该怎么使用它,而没有深入研究。
- Game类和SudoView类的编写使得更加深刻的理解面向对象开发。
- 目前程序功能简单,单一,日后考虑加入网络游戏,实现网络排名,题库更新等题目