扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
上图是数据文件 konka.bxb 的结构图。该数据文件大小为 297 字节,包含三个关卡,各个关卡的大小分别为:“8x7”、“8x7”和“9x5”。内容如下:
1. 文件头(32字节,图中的青色部分)。首先是保留的四个字节。然后是一个字节的数据文件版本号(目前为“2”)。接着是三个字节的标志(内容为“BOX”)。接着是十六字节的组名(编码为“GB2312”,本数据文件中为“康佳”)。接着是总关数(Int32,四个字节,因为本组共有三个关卡,所以内容为“3”)。最后是第一关起始地址位置(Int32,四个字节,本数据文件中的内容为“0x11D”)。
2. 以关数据头(32字节,图中的绿色部分)开始的各关数据。首先是一个字节的关数据头开始标志(“@”)。然后是一个字节的标志(最低位:0:未通关,1:已通关)。接着是通关总步数(Int32,四个字节)。接着是通关推箱子步数(Int32,四个字节)。接着是十四个字节的保留字段。接着是本关的宽度(Int32,四个字节)。接着是本关的高度(Int32,四个字节)。最后是“本关宽度x本关高度”个字节的关数据,也就是说地图中每个单元格占一个字节,取值范围是“0”到“7”,分别表示:0:地,1:槽,2:墙,3:砖,4:箱子放在地上,5:箱子放在槽上,6:工人站在地上,7:工人站在槽上。注意,每一个关卡必须刚好有一个工人。
3. 数据文件的最后是各关起始地址列表(“总关数x4”个字节,图中的黄色部分)。每关的起始地址均为四个字节(Int32),所以共有“总关数x4”个字节。
密封类 DataFile 的源代码中有详细的注释,很容易看懂。
1. InitMap 方法用来初始化地图。地图的大小是“(关的高度+2) x (关的宽度+2)”,这是为了在地图四周砌上围墙,以免搜索算法越出地图边界。
2. DeleteLevel 方法用来删除指定的关。注意,删除关时并不删除关数据,只是将该关的起始地址从各关地址列表中删除,然后将文件缩短四个字节(因为各关地址列表在数据文件的最后)。这样数据文件中就可能包含不需要的冗余数据。通过“菜单 -> 数据 -> 转换”,先“导出”,然后“导入”,可以消除冗余数据。
以下是引用片段: 1 using System; 2 using System.IO; 3 using System.Drawing; 4 using System.Collections.Generic; 5 using System.Windows.Forms; 6 7 namespace Skyiv.Ben.PushBox.Common 8 { 9 // data/.bxb 文件格式 10 // 保留 ver(2) BOX 组名- 总关数 第1关起始地址位置 11 // 0--3 4----- 5-7 8--23 24--27 28-------------31 12 // 13 // @ Flag 总步数 推箱子步数 保留- wide- high- data 14 // 0 1--- 2----5 6--------9 10-23 24-27 28-31 32.. 15 // Flag: 最低位: 0:未通关 1:已通关 16 // 17 // 第1关起始地址 第2关起始地址 . 最后一关起始地址 18 // 0-----------3 4-----------7 . (文件最后四字节) 19 // 20 // steps/.bxs 文件格式见 Step.cs 21 // 其中为关数(1起始),最少四位,不足前补零 22 // 23 // text/.bxa 文件格式 24 // 0 - land SPACE 25 // 1 + slot . 26 // 2 # wall # 27 // 3 % brick N/A 28 // 4 x box on land $ 29 // 5 X box on slot * 30 // 6 ( man on land @ 31 // 7 ) man on slot + .XSB 文件格式 32 // 第一行如果以!开头的话, 则为组名(不能超过16个字符) 33 // 以:开头的行为通关步骤, 格式同(.bxs)文件 34 // 以'开头的行为注释, 完全忽略 35 // 各关之间必须以空行分隔 36 37 /// 38 /// 管理数据文件: *.bxb *.bxa *.bxs 39 /// 40 sealed class DataFile : IDisposable 41 { 42 const byte DataVersion = 2; // 数据文件(.bxb)的版本 43 const byte LevelFlag = (byte)'@'; // 数据文件(.bxb)的关标志 44 const char RemChar = '\''; // 文本文件(.bxa)的注释 45 const char StepsChar = ':'; // 文本文件(.bxa)的通关步骤 46 47 FileStream fs; // 数据文件 48 BinaryReader br; // 数据文件读取器 49 BinaryWriter bw; // 数据文件写入器 50 string groupName; // 当前组名称 51 int[] addrs; // 各关起始地址列表,最后一项为第1关起始地址位置 52 byte[,] map; // 当前关地图 53 int maxLevel; // 总关数 54 Size levelSize; // 当前关尺寸(以单元格为单位) 55 Point worker; // 当前工人位置(以单元格为单位) 56 int mans; // 工人数 57 int boxs; // 箱子数 58 int slots; // 槽数 59 int tasks; // 总任务数 60 int boths; // 已完成任务数 61 bool isFinished; // 是否曾经通关 62 int movedSteps; // 通关的总步数 63 int pushedSteps; // 通关的推箱子步数 64 string fileName { get { return Path.GetFileNameWithoutExtension(fs.Name); } } // 数据文件主名 65 66 public string GroupName { get { return groupName; } } 67 public int MaxLevel { get { return maxLevel; } } 68 public byte[,] Map { get { return map; } } 69 public Size LevelSize { get { return levelSize; } } 70 public bool IsFinished { get { return isFinished; } } 71 public int MovedSteps { get { return movedSteps; } } 72 public int PushedSteps { get { return pushedSteps; } } 73 public Point Worker { get { return worker; } } 74 public bool HasWorker { get { return mans != 0; } } 75 public int Boxs { get { return boxs; } } 76 public int Slots { get { return slots; } } 77 public int Tasks { get { return tasks; } } 78 public int Boths { get { return boths; } set { boths = value; } } 79 80 /// 81 /// 装入组数据 82 /// 83 /// 组文件名 84 public void LoadGroup(string name) 85 { 86 Dispose(); 87 fs = new FileStream(Path.Combine(Pub.DataDirectory, name + Pub.DataExtName), FileMode.Open); 88 br = new BinaryReader(fs, Pub.Encode); 89 bw = new BinaryWriter(fs, Pub.Encode); 90 br.ReadInt32(); // 保留 91 if (br.ReadByte() != DataVersion) throw new Exception("数据文件版本错"); 92 byte[] bs = br.ReadBytes(3); // 数据文件标志:BOX 93 for (int i = 0; i < bs.Length; i++) if (bs[i] != "BOX"[i]) throw new Exception("数据文件标志错"); 94 bs = br.ReadBytes(16); // 组名 95 for (int i = 0; i < bs.Length; i++) if (bs[i] == 0) bs[i] = 32; 96 groupName = Pub.Encode.GetString(bs, 0, bs.Length).Trim(); 97 if (groupName.Length == 0) groupName = fileName; // 如果数据文件中组名为空,则用数据文件主名代替 98 maxLevel = br.ReadInt32(); // 总关数 99 int addrPos = br.ReadInt32(); // 第1关起始地址位置 100 br.BaseStream.Seek(addrPos, SeekOrigin.Begin); 101 addrs = new int[maxLevel + 1]; // 各关起始地址列表,最后一项为第1关起始地址位置 102 for (int i = 0; i < maxLevel; i++) addrs[i] = br.ReadInt32(); 103 addrs[maxLevel] = addrPos; // 第1关起始地址位置 104 if (addrPos + 4 * maxLevel != br.BaseStream.Length) throw new Exception("数据文件地址表必须位于数据最后"); 105 } 106 107 /// 108 /// 装入关数据 109 /// 110 /// 关数 111 public void LoadLevel(int level) 112 { 113 LoadLevelHead(level); 114 InitMap(); 115 for (int i = 1; i <= levelSize.Height; i++) 116 { 117 for (int j = 1; j <= levelSize.Width; j++) 118 { 119 map[i, j] = br.ReadByte(); 120 UpdateCounts(j, i, true); 121 } 122 } 123 if (mans != 1) throw new Exception("读取关数据失败:必须刚好有一个工人"); 124 tasks = Math.Min(boxs, slots); 125 } 126 127 /// 128 /// 新建一关 129 /// 130 /// 是否复制当前关 131 /// 新建关的尺寸 132 public void NewLevel(bool isCopy, Size size) 133 { 134 Size levelSizeOem = levelSize; 135 byte[,] mapOem = isCopy ? (byte[,])map.Clone() : null; 136 levelSize = size; 137 InitMap(); 138 for (int i = 1; i <= levelSize.Height; i++) 139 { 140 for (int j = 1; j <= levelSize.Width; j++) 141 { 142 map[i, j] = (isCopy && i <= levelSizeOem.Height && j <= levelSizeOem.Width) ? mapOem[i, j] : Block.Land; 143 UpdateCounts(j, i, true); 144 } 145 } 146 if (mans != 1 && mans != 0) throw new Exception("不能超过一个工人"); 147 tasks = Math.Min(boxs, slots); 148 } 149 150 /// 151 /// 初始化地图 152 /// 153 private void InitMap() 154 { 155 map = new byte[levelSize.Height + 2, levelSize.Width + 2]; 156 for (int i = 0; i <= levelSize.Height + 1; i++) map[i, 0] = map[i, levelSize.Width + 1] = Block.Wall; 157 for (int j = 0; j <= levelSize.Width + 1; j++) map[0, j] = map[levelSize.Height + 1, j] = Block.Wall; 158 mans = boxs = slots = boths = 0; 159 } 160 161 /// 162 /// 根据地图项目更新统计 163 /// 164 /// 当前位置横坐标 165 /// 当前位置纵坐标 166 /// 加或减 167 public void UpdateCounts(int x, int y, bool isAdd) 168 { 169 int sign = isAdd ? 1 : -1; 170 if (Block.IsBox(map[y, x])) boxs += sign; 171 if (Block.IsSlot(map[y, x])) slots += sign; 172 if (Block.Box1 == map[y, x]) boths += sign; 173 if (Block.IsMan(map[y, x])) 174 { 175 mans += sign; 176 worker = isAdd ? new Point(x, y) : Point.Empty; 177 } 178 } 179 180 /// 181 /// 装入关数据头 182 /// 183 /// 关数 184 void LoadLevelHead(int level) 185 { 186 if (level > maxLevel - 1) throw new Exception(string.Format("当前关数({0})不能大于总关数({1})", level + 1, maxLevel)); 187 br.BaseStream.Seek(addrs[level], SeekOrigin.Begin); 188 if (br.ReadByte() != LevelFlag) throw new Exception("关数据标志错"); 189 isFinished = (br.ReadByte() & 1) == 1; // 是否曾经通关 190 movedSteps = br.ReadInt32(); // 通关的总步数 191 pushedSteps = br.ReadInt32(); // 通关的推箱子步数 192 br.ReadBytes(14); // 保留 193 levelSize.Width = br.ReadInt32(); 194 levelSize.Height = br.ReadInt32(); 195 } 196 197 /// 198 /// 更新当前关数据 199 /// 200 /// 关数 201 /// 通关步骤 202 /// 推箱子步数 203 public void SaveLevel(int level, Step[] steps, int pushs) 204 { 205 SaveLevelHead(level, steps.Length, pushs); 206 SaveLevelSteps(level, Pub.ToString(steps)); 207 LoadLevelHead(level); 208 } 209 210 /// 211 /// 更新当前关头数据 212 /// 213 /// 关数 214 /// 通关步数 215 /// 推箱子步数 216 void SaveLevelHead(int level, int moves, int pushs) 217 { 218 if (level > maxLevel - 1) throw new Exception("关数太大"); 219 bw.BaseStream.Seek(addrs[level] + 1, SeekOrigin.Begin); 220 bw.Write((byte)1); // 是否曾经通关 221 bw.Write(moves); // 通关的总步数 222 bw.Write(pushs); // 通关的推箱子步数 223 } 224 225 /// 226 /// 保存通关步骤 227 /// 228 /// 关数 229 /// 通关步骤 230 void SaveLevelSteps(int level, string steps) 231 { 232 if (!Directory.Exists(Pub.StepsDirectory)) Directory.CreateDirectory(Pub.StepsDirectory); 233 Fcl.WriteAllText(GetStepsFileName(fileName, level), steps); 234 } 235 236 /// 237 /// 给出通关步骤 238 /// 239 /// 关数 240 /// 通关步骤 241 public string GetSteps(int level) 242 { 243 return GetSteps(fileName, level); 244 } 245 246 string GetSteps(string name, int level) 247 { 248 return Fcl.ReadAllText(GetStepsFileName(name, level)); 249 } 250 251 string GetStepsFileName(string name, int level) 252 { 253 return Path.Combine(Pub.StepsDirectory, name + (level + 1).ToString("D4") + Pub.StepsExtName); 254 } 255 256 /// 257 /// 删除通关步骤文件 258 /// 259 /// 关数 260 private void DeleteStepsFile(int level) 261 { 262 // 虽然 File.Delete(): 删除指定的文件。如果指定的文件不存在,则不引发异常。 263 // 但是: 如果指定的路径无效,还是会引发 DirectoryNotFoundException 异常。 264 // 所以需要先用 File.Exists() 判断一下文件是否存在 265 string name = GetStepsFileName(fileName, level); 266 if (File.Exists(name)) File.Delete(name); 267 } 268 269 /// 270 /// 保存 271 /// 272 /// 是否新建 273 /// 要保存的关数 274 public void SaveDesign(bool isNew, int level) 275 { 276 if (isNew && level != maxLevel) throw new Exception("新建的关必须在最后一关之后"); 277 bw.BaseStream.Seek(addrs[level], SeekOrigin.Begin); 278 WriteLevel(level, string.Empty); // 如果不是新建,则关尺寸不能比原来的大 279 if (isNew) 280 { 281 Fcl.Resize(ref addrs, addrs.Length + 1); 282 addrs[++maxLevel] = (int)bw.BaseStream.Position; 283 WriteAddrs(); 284 } 285 DeleteStepsFile(level); // 删除通关步骤文件 286 } 287 288 /// 289 /// 删除最后一关 290 /// 291 /// 关数(必须是最后一关) 292 public void DeleteLastLevel(int level) 293 { 294 if (level != maxLevel - 1) throw new Exception("要删除的关必须是最后一关"); 295 DeleteLevel(level); 296 DeleteStepsFile(level); // 删除通关步骤文件,如果被删除的关不是最后一关,以后各关的通关步骤文件就不对了 297 } 298 299 /// 300 /// 删除指定的关 301 /// 302 /// 关数 303 void DeleteLevel(int level) 304 { 305 for (int i = level + 1; i <= maxLevel; i++) addrs[i - 1] = addrs[i]; // 之后的关起始地址前移 306 --maxLevel; // 更新总关数 307 WriteAddrs(); 308 } 309 310 /// 311 /// 更新各关起始地址列表及总关数和第1关起始地址位置 312 /// 313 private void WriteAddrs() 314 { 315 bw.Seek(addrs[maxLevel], SeekOrigin.Begin); 316 for (int i = 0; i < maxLevel; i++) bw.Write(addrs[i]); // 各关起始地址 317 bw.BaseStream.SetLength(bw.BaseStream.Position); // 关起始地址列表位于数据文件最后, 用于删除关的情况 318 bw.Seek(24, SeekOrigin.Begin); 319 bw.Write(maxLevel); // 总关数 320 bw.Write(addrs[maxLevel]); // 第1关起始地址位置 321 } 322 323 /// 324 /// 更新组名 325 /// 326 void WriteGroupName() 327 { 328 byte[] bs = new byte[16]; 329 byte[] bn = Pub.Encode.GetBytes(groupName); 330 for (int i = 0; i < bs.Length && i < bn.Length; i++) bs[i] = bn[i]; 331 for (int i = bn.Length; i < bs.Length; i++) bs[i] = 32; 332 bw.Seek(8, SeekOrigin.Begin); 333 bw.Write(bs); // 组名 334 } 335 336 /// 337 /// 写关数据和通关步骤 338 /// 注意:调用本函数前必须定位到数据文件的正确位置 339 /// 340 /// 关数 341 /// 通关步骤 342 /// 本关的统计信息 343 string WriteLevel(int level, string steps) 344 { 345 bw.Write(LevelFlag); // 关标志 346 bw.Write((byte)(string.IsNullOrEmpty(steps) ? 0 : 1)); // 标志:是否已通关 347 bw.Write(steps.Length); // 总步数 348 bw.Write(GetPushSteps(steps)); // 推箱子步数 349 bw.Write(new byte[14]); // 保留 350 bw.Write(levelSize.Width); // 当前关宽度 351 bw.Write(levelSize.Height); // 当前关高度 352 mans = slots = boxs = 0; 353 int lands = 0, walls = 0, bricks = 0; 354 for (int i = 1; i <= levelSize.Height; i++) 355 { 356 for (int j = 1; j <= levelSize.Width; j++) 357 { 358 bw.Write(map[i, j]); 359 switch (map[i, j]) 360 { 361 case Block.Land: lands++; break; 362 case Block.Slot: slots++; break; 363 case Block.Wall: walls++; break; 364 case Block.Brick: bricks++; break; 365 case Block.Box0: lands++; boxs++; break; 366 case Block.Box1: slots++; boxs++; break; 367 case Block.Man0: lands++; mans++; break; 368 case Block.Man1: slots++; mans++; break; 369 } 370 } 371 } 372 if (mans != 1) ErrorExit(true, level + 1, "必须刚好有一个工人"); 373 if (!string.IsNullOrEmpty(steps)) SaveLevelSteps(level, steps); 374 return string.Format("{1}: {2} {3} {4} {5} {6} {7} {8}{0}", 375 Fcl.NewLine, level + 1, Pub.ToString(levelSize), walls, bricks, lands, slots, boxs, steps.Length); 376 } 377 378 /// 379 /// 根据通关步骤给出推箱子步数 380 /// 381 /// 通关步骤 382 /// 推箱子步数 383 int GetPushSteps(string steps) 384 { 385 int n = 0; 386 foreach (char c in steps) if (((Step)c).IsBox) n++; 387 return n; 388 } 389 390 /// 391 /// 数据导入 392 /// 393 /// 数据文件主名 394 /// 最大关尺寸 395 /// 显示相关信息的文本框 396 public void Import(string name, int maxLevelSize, TextBox tbxMsg) 397 { 398 try 399 { 400 tbxMsg.Text = string.Format("{1} => {2}{0}", Fcl.NewLine, name + Pub.TextExtName, name + Pub.DataExtName); 401 if (!Directory.Exists(Pub.DataDirectory)) Directory.CreateDirectory(Pub.DataDirectory); 402 using (StreamReader sr = new StreamReader(Path.Combine(Pub.TextDirectory, name + Pub.TextExtName), Pub.Encode)) 403 { 404 Dispose(); 405 fs = new FileStream(Path.Combine(Pub.DataDirectory, name + Pub.DataExtName), FileMode.Create, FileAccess.Write); 406 bw = new BinaryWriter(fs, Pub.Encode); 407 byte[] buf = new byte[32]; 408 buf[4] = DataVersion; 409 buf[5] = (byte)'B'; 410 buf[6] = (byte)'O'; 411 buf[7] = (byte)'X'; 412 bw.Write(buf); 413 map = new byte[maxLevelSize + 2, maxLevelSize + 2]; 414 List addrList = new List(); // 各关起始地址列表,最后一项为第1关起始地址位置 415 addrList.Add((int)bw.BaseStream.Position); // 第1关起始地址 416 groupName = name; // 组名 417 int level = 0; 418 levelSize = Size.Empty; 419 string steps = ""; // 通关步骤 420 bool isFirst = true; 421 for (int line = 1; ; line++) 422 { 423 string s = sr.ReadLine(); 424 if (s != null) s = s.Trim(); 425 if (line == 1 && s != null && s.Length > 0 && s[0] == '!') 426 { 427 groupName = s.Substring(1).Trim(); 428 tbxMsg.Text += "组名: [" + groupName + "]" + Fcl.NewLine; 429 continue; 430 } 431 if (isFirst) 432 { 433 isFirst = false; 434 tbxMsg.Text += "#: 宽x高 墙 砖 地 槽 箱 通关步数" + Fcl.NewLine; 435 } 436 if ((s == null || s.Length == 0) && levelSize != Size.Empty) 437 { 438 tbxMsg.Text += WriteLevel(level, steps); 439 addrList.Add((int)bw.BaseStream.Position); // 下一关起始地址 440 level++; 441 levelSize = Size.Empty; 442 steps = ""; 443 } 444 if (s == null) break; 445 if (s.Length == 0 || s[0] == RemChar) continue; 446 if (s[0] == StepsChar) 447 { 448 steps = s.Substring(1).Trim(); // 通关步骤 449 continue; 450 } 451 levelSize.Height++; 452 if (levelSize.Height == 1) levelSize.Width = s.Length; 453 else if (levelSize.Width != s.Length) ErrorExit(false, line, "宽度不齐"); 454 if (levelSize.Width > maxLevelSize) ErrorExit(false, line, GetMessage("宽度太大", true)); 455 if (levelSize.Height > maxLevelSize) ErrorExit(false, line, GetMessage("高度太大", true)); 456 for (int i = 0; i < levelSize.Width; i++) 457 if (!Block.IsBlock(map[levelSize.Height, i + 1] = Block.GetByte(s[i]))) 458 ErrorExit(false, line, "非法字符:[" + s[i] + "]"); 459 } 460 addrs = addrList.ToArray(); 461 maxLevel = level; 462 WriteAddrs(); 463 WriteGroupName(); 464 } 465 } 466 catch (OutOfMemoryException ex) 467 { 468 throw new Exception(GetMessage("内存不足", false), ex); 469 } 470 finally 471 { 472 Dispose(); 473 } 474 tbxMsg.Text += "导入完成"; 475 } 476 477 string GetMessage(string msg1, bool isIncrease) 478 { 479 return msg1 + ",请在“菜单 -> 选项”对话框中" + (isIncrease ? "增加" : "减少") +"“最大关尺寸”"; 480 } 481 482 /// 483 /// 数据导出 484 /// 485 /// 数据文件主名 486 /// 显示相关信息的文本框 487 public void Export(string name, TextBox tbxMsg) 488 { 489 try 490 { 491 tbxMsg.Text = string.Format("{1} => {2}{0}", Fcl.NewLine, name + Pub.DataExtName, name + Pub.TextExtName); 492 LoadGroup(name); 493 if (!Directory.Exists(Pub.TextDirectory)) Directory.CreateDirectory(Pub.TextDirectory); 494 using (StreamWriter sw = new StreamWriter( 495 Path.Combine(Pub.TextDirectory, name + Pub.TextExtName), false, Pub.Encode)) 496 { 497 sw.WriteLine("! {0}", groupName); 498 tbxMsg.Text += "组名: [" + groupName + "]" + Fcl.NewLine; 499 tbxMsg.Text += "#: 宽x高 总任务数 通关步数" + Fcl.NewLine; 500 for (int level = 0; level < maxLevel; level++) 501 { 502 LoadLevel(level); 503 sw.WriteLine("{0}[{1}]", RemChar, level + 1); // 注释:第几关 504 for (int y = 0; y < levelSize.Height; y++) 505 { 506 for (int x = 0; x < levelSize.Width; x++) sw.Write(Block.GetChar(map[y + 1, x + 1])); 507 sw.WriteLine(); 508 } 509 string steps = GetSteps(name, level); // 通关步骤 510 if (!string.IsNullOrEmpty(steps)) sw.WriteLine(StepsChar + steps); 511 sw.WriteLine(); 512 tbxMsg.Text += string.Format("{1}: {2} {3} {4}{0}", 513 Fcl.NewLine, level + 1, Pub.ToString(levelSize), tasks, steps.Length); 514 } 515 } 516 } 517 finally 518 { 519 Dispose(); 520 } 521 tbxMsg.Text += "导出完成"; 522 } 523 524 void ErrorExit(bool isLevel, int idx, string msg) 525 { 526 throw new Exception(string.Format("错误:第{0}{1}:{2}", idx, isLevel ? "关" : "行", msg)); 527 } 528 529 public void Dispose() 530 { 531 if (br != null) br.Close(); 532 if (bw != null) bw.Close(); 533 if (fs != null) fs.Close(); 534 br = null; 535 bw = null; 536 fs = null; 537 } 538 } 539 } 540 |
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者