【面试题】类似国际象棋的棋子移动实现

都将近年关了,居然还有人找我帮做第一轮的笔试题,与其它第一轮在线做题相比,这边比较新颖的是:居然是给一个Solution,要求按例子实现代码。可能这是外企比较流行的做法吧,题目大致是实现一个8*8的棋盘,实现3个棋子在上面随机移动,当然限制要求也有一些,具体的要求文档如下:

You have been provided with a third-party library "ChessLib" which calculates the legal moves a knight can make given
a position on an 8 by 8 board. The library has been used to create a program which moves a knight randomly around a
board, given an initial starting position and a total number of moves to make.

Problem:
========

Extend this program to set up an 8 by 8 square game board containing several different pieces in predefined positions.
For each move of the game, the program will choose a piece at random, and move it to a randomly selected valid
position.

You are not allowed to change any of the ChessLib code.
 
Extend the program as required. 
Use Object Oriented Design and Modeling appropriately for extensibility.

Please supply all the code for your solution in the file Answer.cs in the SampleProgram project.
Please supply all the tests for your solution in the file TestAnswer.cs in the SampleProgram.Test project.


Game Rules:
-----------
 
* Only one piece can occupy any position on the board at a given time.
* All pieces can “jump” any occupied position.

Note: Although the game bears a striking resemblance to Chess, this is entirely coincidental. Do not assume other
chess rules apply.

 
Game Pieces to support:
-----------------------

* Knight – Moves as implemented by ChessLib
* Bishop - Moves diagonally, any distance within board boundaries
* Queen – Moves diagonally, horizontally or vertically, any distance within board boundaries

说实在的,这题目我刚看时,都没看懂啥意思,问求助者是否理解,对方只是回复猎头给的题目,大约花了一个小时的时间,结合Solution自带的Demo,大致琢磨出来这个题目到底是想做啥:实现一个类似国际象棋的游戏,Demo中呢已经提供了Knight的实现,笔试者不允许修改ChessLib的代码,然后实现BishopQueen,并约定同一个时间一个位置上只能有一个棋子,然后就是所以棋子都可以跳过被占的位置,最后就是不要套用其它国际象棋规则!

下面是国际象棋的棋盘图。

【面试题】类似国际象棋的棋子移动实现_第1张图片

先列举下Demo中ChessLib的代码,代码包含PositionKnightMove,功能与类名一致:一个代表棋盘位置信息,一个代表Knight如何获取后续可以跳到的Position

    public struct Position
    {
        public readonly int X;
        public readonly int Y;

        public Position(int x, int y)
        {
            X = x;
            Y = y;
        }

        public override bool Equals(object obj)
        {
            if (obj is Position)
            {
                var val = (Position) obj;
                return val.X==X && val.Y==Y;
            }
            return false;
        }

        public override int GetHashCode()
        {
            return X^Y;
        }

        public override string ToString()
        {
            return String.Format("{0},{1}", X, Y);
        }
    }
    
    public class KnightMove
    {
        public static readonly int[,] Moves = new[,] { { 1, 2 }, { 1, -2 }, { -1, 2 }, { -1, -2 }, { 2, 1 }, { -2, 1 }, { 2, -1 }, { -2, -1 } };

        public IEnumerable<Position> ValidMovesFor(Position pos)
        {
            for(var i=0;i<=Moves.GetUpperBound(0);i++)
            {
                var newX = pos.X + Moves[i,0];
                var newY = pos.Y + Moves[i,1];
                if (newX > 8 || newX < 1 || newY > 8 || newY < 1)
                    continue;
                yield return new Position(newX, newY);
            }
        }
    }

然后是Demo的其它代码

    public static class Program
    {
        public static void Main()
        {
            var game = new Game();
            //var game = new ComplexGame();

            game.Setup();
            game.Play(15);

            Console.WriteLine("Press any key ...");
            Console.ReadKey();
        }
    }
  
    public class Game
    {
        private readonly Random _rnd = new Random();

        private Position _startPosition;

        public void Play(int moves)
        {
            var knight = new KnightMove();
            var pos = _startPosition;
            Console.WriteLine("0: My position is {0}", pos);

            for(var move = 1; move <= moves; move++)
            {
                var possibleMoves = knight.ValidMovesFor(pos).ToArray();
                pos = possibleMoves[_rnd.Next(possibleMoves.Length)];
                Console.WriteLine("{1}: My position is {0}", pos, move);
            }
        }

        public void Setup()
        {
            _startPosition = new Position(3, 3);
        }
    }

Demo运行起来效果后如下
【面试题】类似国际象棋的棋子移动实现_第2张图片

下面开始讲思路,既然题目要求了要考虑可扩展性,那就不可能是简单的一套代码撸下去。既然题目要求要实现三个棋子KnightBishopQueue,那首先就是要有一个棋子的接口设计,既然是棋子,题目也只是要求移动部分,所以设计上该接口只需要关注移动相关的设定,接口定义IChessPiece如下:

    public interface IChessPiece
    {
        /// 
        /// 棋子当前位置
        /// 
        Position Current { get; }
        /// 
        /// 获取当前位置下所有允许移动的位置
        /// 
        /// 
        IReadOnlyCollection<Position> GetAllPositions();
        /// 
        /// 移动棋子,注意目标位置必须为允许移动的位置
        /// 
        /// 
        void Move(Position targetPosition);
    }

一开始设计上是三个棋子是互不相干的实现,但做着做着突然发现,棋子的一些共有功能都是一样的,唯一不同的地方,只是获取可以移动的位置有差异,于是BasicChessPiece抽象实现被提取了出来,因为Move时肯定也要判断目标位置是否是符合移动规则的位置,所以额外设置了NextPositions,用于缓存当前位置下后续可以移动的位置点

    public abstract class BasicChessPiece : IChessPiece
    {
        public BasicChessPiece(Position startPosition)
        {
            this.ValidPosition(startPosition);
            this.Current = startPosition;
        }
        public Position Current { get; protected set; }
        /// 
        /// 允许移动的后续位置集合
        /// 
        protected IDictionary<string, Position> NextPositions { get; set; }
        public virtual IReadOnlyCollection<Position> GetAllPositions()
        {
            this.InitNextPositions();
            return NextPositions.Values.ToArray();//not allowed to be changed
        }
        /// 
        /// 获取所有后续位置集合
        /// 
        /// 
        protected abstract IEnumerable<Position> GetPositions();
        public virtual void Move(Position targetPosition)
        {
            this.ValidPosition(targetPosition);
            this.InitNextPositions();
            if (this.NextPositions.ContainsKey(targetPosition.GetUnionKey()))
            {
                this.NextPositions = null;//棋子已经移动,清空位置
                this.Current = targetPosition;
            }
            else
            {
                throw new ArgumentException("error position");
            }
        }

        /// 
        /// 判定位置是否符合棋盘规则
        /// 
        /// 
        /// 
        /// 
        protected bool IsValidPosition(int x, int y)
        {
            return x >= 1 && x <= 8 && y >= 1 && y <= 8;
        }

        private void ValidPosition(Position pos)
        {
            if (!this.IsValidPosition(pos.X, pos.Y))
            {
                throw new ArgumentException("invalid position");
            }
        }

        private void InitNextPositions()
        {
            if (NextPositions == null) //约定通过null来判断是否已经生成了允许移动的位置
            {
                var positions = this.GetPositions();
                if (positions == null || !positions.Any())
                {
                    this.NextPositions = new Dictionary<string, Position>();//如果没有可移动的位置,则设置空字典
                }
                else
                {
                    this.NextPositions = positions.ToDictionary(k => k.GetUnionKey(), v => v);
                }
            }
        }
    }

接下来就是三个棋子的实现,因为Knight已经有了ChessLib实现,所以Knight直接是对KnightMove做了一层适配,而BishopQueue可移动的位置比骑士多了很多(Bishop可以有8*4=32个位置,Queue可以有8*8=64个位置),所以不考虑KnightMove的类似实现,而是直接通过棋子的当前位置,向规则允许的方向进行循环计算,另外Queue其实只比Bishop多了垂直和水平四个移动方向,所以Queue直接继承自Bishop

    public class Knight : BasicChessPiece
    {
        KnightMove knight;
        public Knight(Position startPosition)
            : base(startPosition)
        {
            knight = new KnightMove();
        }

        protected override IEnumerable<Position> GetPositions()
        {
            return knight.ValidMovesFor(this.Current);
        }
    }
    public class Bishop : BasicChessPiece
    {
        public Bishop(Position startPosition)
            : base(startPosition)
        {
        }
        protected override IEnumerable<Position> GetPositions()
        {
            for (var h = 0; h < 4; h++)
            {
                //四个45度角方向允许移动的位置
                var byX = h % 2 == 0 ? 1 : -1;
                var byY = h / 2 == 0 ? 1 : -1;
                for (var i = 1; i < 8; i++)
                {
                    var newX = this.Current.X + byX * i;
                    var newY = this.Current.Y + byY * i;
                    if (!this.IsValidPosition(newX, newY))
                    {
                        break;
                    }
                    yield return new Position(newX, newY);
                }
            }
        }
    }
    public class Queen : Bishop
    {
        public Queen(Position startPosition)
            : base(startPosition)
        {
        }
        protected override IEnumerable<Position> GetPositions()
        {
            var positions = base.GetPositions().ToList();
            for (var h = 0; h < 4; h++)
            {
                //垂直和水平四个方向允许移动的位置
                var byX = 0;
                var byY = 0;
                if (h < 2)
                {
                    byX = h % 2 == 0 ? 1 : -1;
                }
                else
                {
                    byY = h % 2 == 0 ? 1 : -1;
                }
                for (var i = 1; i < 8; i++)
                {
                    var newX = this.Current.X + byX * i;
                    var newY = this.Current.Y + byY * i;
                    if (!this.IsValidPosition(newX, newY))
                    {
                        break;
                    }
                    positions.Add(new Position(newX, newY));
                }
            }
            return positions;
        }
    }

棋子定义完了,接下来就是对ComplexGame的填充,首先是用到了两个扩展方法,设置internal是限制其作用范围

    internal static class RandomHelper
    {
        public static int Next(this int maxNumber)
        {
            if (maxNumber <= 0)
            {
                throw new ArgumentException("'maxNumber' must great than zero");
            }
            return Math.Abs(Guid.NewGuid().GetHashCode() % maxNumber);
        }
    }

    internal static class PositionExtensions
    {
        /// 
        /// 获取Position对应的唯一标志
        /// 
        /// 
        /// 
        public static string GetUnionKey(this Position pos)
        {
            return $"{pos.X}_{pos.Y}";
        }
    }

接下来是ComplexGame,代码与例子Game类似,判断移动的代码都写在了此处,其实棋盘控制部分的代码可以在此进一步提取,即KnightBishopQueen只是最基础的棋子实现,可以设定一个IChess来进行限定布局、阻塞、吃掉棋子等行为,只是这里没这么做而已

    public class ComplexGame
    {
        IList<IChessPiece> pieces;
        IChessPiece prevPiece;
        HashSet<string> occupiedPositions;
        public void Setup()
        {
            // TODO: Set up the state of the game here
            var knight = new Knight(new Position(1, 2));
            var bishop = new Bishop(new Position(1, 3));
            var queen = new Queen(new Position(1, 4));
            occupiedPositions = new HashSet<string>
            {
                knight.Current.GetUnionKey(),
                bishop.Current.GetUnionKey(),
                queen.Current.GetUnionKey(),
            };
            pieces = new List<IChessPiece>()
            {
                knight,
                bishop,
                queen
            };
        }

        public void Play(int moves)
        {
            // TODO: Play the game moves here
            for (var move = 1; move <= moves; move++)
            {
                var piece = this.GetRandomChessPiece();
                var pieceName = piece.GetType().Name;
                Console.WriteLine("0: {0} current position is {1}", pieceName, piece.Current);
                var possibleMoves = piece.GetAllPositions();
                if (possibleMoves != null && possibleMoves.Count > 0)
                {
                    possibleMoves = this.GetValidPositions(possibleMoves);
                }
                if (possibleMoves == null || possibleMoves.Count == 0)
                {
                    Console.WriteLine("No position for {0} to move", pieceName);
                    //move--; //注释表示不移动也算过了一步,如果不允许这样要注释此行
                    continue;
                }
                var newPosition = this.GetNextPositon(possibleMoves);
                this.MoveTo(piece, newPosition);
                this.SetPrevChessPiece(piece);
                Console.WriteLine("{1}: {2} move to position {0}", newPosition, move, pieceName);
            }
        }

        protected virtual IChessPiece GetRandomChessPiece()
        {
            var piece = pieces
                .Where(_ => _ != prevPiece)//不允许同样的棋子连续移动,如果不限制可以注释此行
                .OrderBy(_ => Guid.NewGuid()).First();
            return piece;
        }
        protected virtual IReadOnlyCollection<Position> GetValidPositions(IEnumerable<Position> positions)
        {
            //因为题目要求是允许跳过,而不是障碍,所以只要简单的移除掉已被占用的位置即可
            return positions.Where(_ => !occupiedPositions.Contains(_.GetUnionKey()))/*.OrderBy(_ => Guid.NewGuid())*/.ToList();
            //此处因为数组数量较小,如果不考虑性能,可以直接OrderBy简单方式随机排序
        }
        protected virtual Position GetNextPositon(IEnumerable<Position> positions)
        {
            var source = positions as List<Position>;
            if (source == null)
            {
                source = positions.ToList();
            }
            var index = RandomHelper.Next(source.Count);
            return source[index];

        }
        private void MoveTo(IChessPiece piece, Position position)
        {
            var prevPositionKey = piece.Current.GetUnionKey();
            occupiedPositions.Remove(prevPositionKey);
            piece.Move(position);
            occupiedPositions.Add(position.GetUnionKey());
        }
        private void SetPrevChessPiece(IChessPiece piece)
        { 
            this.prevPiece = piece;
        }
    }

最终的运行效果
【面试题】类似国际象棋的棋子移动实现_第3张图片
最后是测试用例,完全仿照Demo的测试用例

    using ChessLib;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using System.Collections.Generic;

    [TestClass]
    public class TestAnswerFixture
    {
        // TODO: add additional tests for your answer
        [TestMethod]
        public void TestKnightFromInsideBoard()
        {
            var pos = new Position(3, 3);
            var knight = new Knight(pos);

            var moves = knight.GetAllPositions();

            Assert.IsNotNull(moves);
            Assert.AreEqual(8, moves.Count);

            foreach (var move in moves)
            {
                switch (Math.Abs(move.X - pos.X))
                {
                    case 1:
                        Assert.AreEqual(2, Math.Abs(move.Y - pos.Y));
                        break;
                    case 2:
                        Assert.AreEqual(1, Math.Abs(move.Y - pos.Y));
                        break;
                    default:
                        Assert.Fail();
                        break;
                }
            }
        }

        [TestMethod]
        public void TestKnightFromCorner()
        {
            var pos = new Position(1, 1);
            var knight = new Knight(pos);

            var moves = new HashSet<Position>(knight.GetAllPositions());

            Assert.IsNotNull(moves);
            Assert.AreEqual(2, moves.Count);

            var possibles = new[] { new Position(2, 3), new Position(3, 2) };

            foreach (var possible in possibles)
            {
                Assert.IsTrue(moves.Contains(possible));
            }
        }

        [TestMethod]
        public void TestBishopFromInsideBoard()
        {
            var pos = new Position(3, 3);
            var bishop = new Bishop(pos);

            var moves = bishop.GetAllPositions();

            Assert.IsNotNull(moves);
            Assert.AreEqual(11, moves.Count);

            foreach (var move in moves)
            {
                Assert.AreEqual(Math.Abs(move.X - pos.X), Math.Abs(move.Y - pos.Y));
            }
        }

        [TestMethod]
        public void TestBishopFromCorner()
        {
            var pos = new Position(1, 1);
            var bishop = new Bishop(pos);

            var moves = new HashSet<Position>(bishop.GetAllPositions());

            Assert.IsNotNull(moves);
            Assert.AreEqual(7, moves.Count);

            var possibles = new[] { new Position(3, 3), new Position(7, 7) };

            foreach (var possible in possibles)
            {
                Assert.IsTrue(moves.Contains(possible));
            }
        }

        [TestMethod]
        public void TestQueenFromInsideBoard()
        {
            var pos = new Position(3, 3);
            var queen = new Queen(pos);

            var moves = queen.GetAllPositions();

            Assert.IsNotNull(moves);
            Assert.AreEqual(25, moves.Count);

            foreach (var move in moves)
            {
                if (pos.X == move.X)
                {
                    Assert.AreNotEqual(pos.Y, move.Y);
                }
                else if (pos.Y == move.Y)
                {
                    Assert.AreNotEqual(pos.X, move.X);
                }
                else
                {
                    Assert.AreEqual(Math.Abs(move.X - pos.X), Math.Abs(move.Y - pos.Y));
                }
            }
        }

        [TestMethod]
        public void TestQueenFromCorner()
        {
            var pos = new Position(1, 1);
            var queen = new Queen(pos);

            var moves = new HashSet<Position>(queen.GetAllPositions());

            Assert.IsNotNull(moves);
            Assert.AreEqual(21, moves.Count);

            var possibles = new[] { new Position(3, 3), new Position(7, 7), new Position(7, 1), new Position(1, 8) };

            foreach (var possible in possibles)
            {
                Assert.IsTrue(moves.Contains(possible));
            }
        }
    }

测试用例运行效果
【面试题】类似国际象棋的棋子移动实现_第4张图片
2022-03-05补充
新题目增加了Pawn,其它不变

* Pawn – Moves horizontally or vertically, one square everytime, one or two squares on first move only

Key words:
-----------------------
1. don't change the chesslib
2. OO design
3. extendable
4. unitest

严格遵守开闭原则,增加Pawn实现

    public class Pawn : BasicChessPiece
    {
        private bool isFirst = true;
        public Pawn(Position startPosition)
            : base(startPosition)
        {
        }
        protected override IEnumerable<Position> GetPositions()
        {
            var maxMove = isFirst ? 2 : 1;//第一次允许移动2格范围
            for (var h = 0; h < 4; h++)
            {
                var byX = 0;
                var byY = 0;
                if (h < 2)
                {
                    byX = h % 2 == 0 ? 1 : -1;
                }
                else
                {
                    byY = h % 2 == 0 ? 1 : -1;
                }
                for (var i = 1; i <= maxMove; i++)
                {
                    var newX = this.Current.X + byX * i;
                    var newY = this.Current.Y + byY * i;
                    if (!this.IsValidPosition(newX, newY))
                    {
                        break;
                    }
                    yield return new Position(newX, newY);
                }
            }
        }
        public override void Move(Position targetPosition)
        {
            base.Move(targetPosition);
            this.isFirst = false;
        }
    }

增加单元测试,因为Pawn移动时区分是否第一次移动,所以单元测试内也进行了对应的体现

        [TestMethod]
        public void TestPawnFromInsideBoard()
        {
            var pos = new Position(3, 3);
            var pawn = new Pawn(pos);

            //move first
            InnerTest(8);

            //move second
            pawn.Move(new Position(3, 1));
            InnerTest(3);

            void InnerTest(int moveCount)
            {
                var moves = pawn.GetAllPositions();
                Assert.IsNotNull(moves);
                Assert.AreEqual(moveCount, moves.Count);
                foreach (var move in moves)
                {
                    if (pos.X == move.X)
                    {
                        Assert.AreNotEqual(pos.Y, move.Y);
                    }
                    else
                    {
                        Assert.AreNotEqual(pos.X, move.X);
                    }
                }
            }
        }

        [TestMethod]
        public void TestPawnFromCorner()
        {
            var pos = new Position(1, 1);
            var pawn = new Pawn(pos);

            var positions = new[] { new Position(1, 2), new Position(1, 3), new Position(2, 1), new Position(3, 1) };
            //move first
            InnerTest(positions);

            //move second
            pawn.Move(positions[1]);
            positions = new[] { new Position(1, 2), new Position(1, 4), new Position(2, 3) };
            InnerTest(positions);

            void InnerTest(Position[] possibles)
            {
                var moves = new HashSet<Position>(pawn.GetAllPositions());
                Assert.IsNotNull(moves);
                Assert.AreEqual(possibles.Length, moves.Count);
                foreach (var possible in possibles)
                {
                    Assert.IsTrue(moves.Contains(possible));
                }
            }
        }

你可能感兴趣的:(C#,c#,面试)