Click here to Skip to main content
15,892,253 members
Articles / Programming Languages / C#

A Spiral Tic-Tac-Toe AI Example Using WPF

Rate me:
Please Sign up or sign in to vote.
4.78/5 (17 votes)
28 Mar 2008CPOL11 min read 69.4K   1.6K   52  
Presents an implementation of a Spiral Tic-Tac-Toe AI using a vanilla Negamax search algorithm and WPF DrawingVisuals
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace SpiralTicTacToe
{
    #region EVENT ARGS

    public class MoveRequestRoutedEventArgs : RoutedEventArgs
    {
        private int _hub;
        private int _spoke;

        public MoveRequestRoutedEventArgs(RoutedEvent routedEvent, object source, int hub, int spoke)
            : base(routedEvent, source)
        {
            _hub = hub;
            _spoke = spoke;
        }

        public int Hub
        {
            get { return _hub; }
        }

        public int Spoke
        {
            get { return _spoke; }
        }
    }

    #endregion

    public class SpiralBoard : FrameworkElement 
    {
        private const int TOLERANCE = 12;

        private VisualCollection _visuals;

        private Point[,] _positions;

        private Color _backColor;
        private Color _boardColor;
        private Color _crossColor;
        private Color _circleColor;

        private Brush _backBrush;
        private Pen _boardPen;
        private Pen _crossPen;
        private Pen _circlePen;

        #region ROUTED EVENTS SETUP

        public static readonly RoutedEvent MoveRequestEvent;

        static SpiralBoard()
        {
            //register routed event
            SpiralBoard.MoveRequestEvent = EventManager.RegisterRoutedEvent(
                "MoveRequest", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SpiralBoard));
        }

        public event RoutedEventHandler MoveRequest
        {
            add { AddHandler(SpiralBoard.MoveRequestEvent, value); }
            remove { RemoveHandler(SpiralBoard.MoveRequestEvent, value); }
        }

        #endregion 

        #region CONSTRUCTION

        public SpiralBoard()
        {
            _visuals = new VisualCollection(this);

            _boardColor = Colors.Black;
            _crossColor = Colors.Black;
            _circleColor = Colors.Black;

            _boardPen = new Pen(new SolidColorBrush(_boardColor),1);
            _crossPen = new Pen(new SolidColorBrush(_boardColor), 2);
            _circlePen = new Pen(new SolidColorBrush(_boardColor), 2);

            this.ClipToBounds = true;

            this.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(Board_MouseLeftButtonUp);
        }

        #endregion

        #region OVERRIDES

        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            _visuals.Add(DrawBoardVisual()); //side-effect: initialize _positions
        }

        protected override int VisualChildrenCount
        {
            get { return _visuals.Count; }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index < 0 || index > _visuals.Count)
            {
                throw new ArgumentOutOfRangeException();
            }

            return _visuals[index];
        }

        #endregion

        #region EVENT HANDLERS

        private void Board_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            Point hit = e.GetPosition(this);
            hit.Offset(-this.Width / 2.0, -this.Width / 2.0);

            for (int hub = 0; hub < SpiralAI.HUBS; hub++)
            {
                for (int spoke = 0; spoke < SpiralAI.SPOKES; spoke++)
                {
                    if (Math.Abs(_positions[hub, spoke].X - hit.X) <= TOLERANCE && Math.Abs(_positions[hub, spoke].Y - hit.Y) <= TOLERANCE)
                    {
                        RaiseEvent(new MoveRequestRoutedEventArgs(SpiralBoard.MoveRequestEvent, this, hub, spoke));
                        break;
                    }
                }
            }
        }

        #endregion

        #region PUBLIC PROPERTIES

        [Category("Colors")]
        [TypeConverter(typeof(ColorConverter))]
        public Color BackColor
        {
            get { return _backColor; }
            set
            {
                _backColor = value;
                _backBrush = new SolidColorBrush(_backColor);
            }
        }

        [Category("Colors")]
        [TypeConverter(typeof(ColorConverter))]
        public Color BoardColor
        {
            get { return _boardColor; }
            set 
            { 
                _boardColor = value;
                _boardPen = new Pen(new SolidColorBrush(_boardColor), 1);
            }
        }

        [Category("Colors")]
        [TypeConverter(typeof(ColorConverter))]
        public Color CrossColor
        {
            get { return _crossColor; }
            set 
            { 
                _crossColor = value;
                _crossPen = new Pen(new SolidColorBrush(_crossColor), 2);
            }
        }

        [Category("Colors")]
        [TypeConverter(typeof(ColorConverter))]
        public Color CircleColor
        {
            get { return _circleColor; }
            set 
            { 
                _circleColor = value;
                _circlePen = new Pen(new SolidColorBrush(_circleColor), 2);
            }
        }

        #endregion

        #region PUBLIC METHODS

        public void Reset()
        {
            if (_visuals.Count > 1)
            {
                _visuals.RemoveRange(1, _visuals.Count - 1);
            }
        }

        public void ExecuteMove(int hub, int spoke, SpiralAIOccupier occupier)
        {
            //occupier width
            double fromWidth = this.Width * 0.25;
            double toWidth = this.Width * 0.025;
            if (occupier == SpiralAIOccupier.Cross)
                toWidth = this.Width * 0.04;

            DrawingVisual visual = new DrawingVisual();

            DrawingContext context = visual.RenderOpen();

            //translate origin            
            context.PushTransform(new TranslateTransform(this.Width / 2.0, this.Width / 2.0));

            //draw occupier
            if (occupier == SpiralAIOccupier.Cross)
            {
                GeometryGroup group = new GeometryGroup();

                LineGeometry line1 = new LineGeometry(
                    new Point(
                        _positions[hub, spoke].X - (fromWidth / 2.0),
                        _positions[hub, spoke].Y - (fromWidth / 2.0)),
                    new Point(
                        _positions[hub, spoke].X + (fromWidth / 2.0),
                        _positions[hub, spoke].Y + (fromWidth / 2.0)));

                LineGeometry line2 = new LineGeometry(
                    new Point(
                        _positions[hub, spoke].X - (fromWidth / 2.0),
                        _positions[hub, spoke].Y + (fromWidth / 2.0)),
                    new Point(
                        _positions[hub, spoke].X + (fromWidth / 2.0),
                        _positions[hub, spoke].Y - (fromWidth / 2.0)));

                group.Children.Add(line1);
                group.Children.Add(line2);

                GeometryDrawing drawing = new GeometryDrawing(null, _crossPen, group);
               
                context.DrawDrawing(drawing);

                //now animate it
                PointAnimation line1p1 = new PointAnimation();
                line1p1.To = new Point(
                        _positions[hub, spoke].X - (toWidth / 2.0),
                        _positions[hub, spoke].Y - (toWidth / 2.0));

                PointAnimation line1p2 = new PointAnimation();
                line1p2.To = new Point(
                        _positions[hub, spoke].X + (toWidth / 2.0),
                        _positions[hub, spoke].Y + (toWidth / 2.0));

                PointAnimation line2p1 = new PointAnimation();
                line2p1.To = new Point(
                        _positions[hub, spoke].X - (toWidth / 2.0),
                        _positions[hub, spoke].Y + (toWidth / 2.0));

                PointAnimation line2p2 = new PointAnimation();
                line2p2.To = new Point(
                        _positions[hub, spoke].X + (toWidth / 2.0),
                        _positions[hub, spoke].Y - (toWidth / 2.0));

                line1.BeginAnimation(LineGeometry.StartPointProperty, line1p1);
                line1.BeginAnimation(LineGeometry.EndPointProperty, line1p2);
                line2.BeginAnimation(LineGeometry.StartPointProperty, line2p1);
                line2.BeginAnimation(LineGeometry.EndPointProperty, line2p2);
            }
            else
            {
                EllipseGeometry ellipse = new EllipseGeometry(_positions[hub, spoke], fromWidth, fromWidth);

                GeometryDrawing drawing = new GeometryDrawing(null, _circlePen, ellipse);

                context.DrawDrawing(drawing);

                //now animate it
                DoubleAnimation radiusXAnimation = new DoubleAnimation();
                radiusXAnimation.To = toWidth;

                DoubleAnimation radiusYAnimation = new DoubleAnimation();
                radiusYAnimation.To = toWidth;

                ellipse.BeginAnimation(EllipseGeometry.RadiusXProperty, radiusXAnimation);
                ellipse.BeginAnimation(EllipseGeometry.RadiusYProperty, radiusYAnimation);            
            }

            context.Close();

            _visuals.Add(visual);
        }

        public void UndoMove()
        {
            if (_visuals.Count > 1)
            {
                _visuals.RemoveAt(_visuals.Count - 1);
            }
        }

        #endregion

        #region PRIVATE METHODS

        private DrawingVisual DrawBoardVisual()
        {
            DrawingVisual visual = new DrawingVisual();

            DrawingContext context = visual.RenderOpen();

            //draw bounding box with back color
            context.DrawRectangle(_backBrush, null, new Rect(0, 0, this.Width, this.Height));

            //translate origin            
            context.PushTransform(new TranslateTransform(this.Width / 2.0, this.Width / 2.0));

            double diameter, decrement;
            int hub, spoke, adjHub, adjSpoke;
            double radius, radians;

            //draw concentric circles
            diameter = this.Width - Convert.ToInt32(0.1 * Convert.ToDouble(this.Width));
            decrement = diameter / SpiralAI.HUBS;
            for (hub = 0; hub < SpiralAI.HUBS; hub++)
            {
                diameter = diameter - (hub * decrement);
                context.DrawEllipse(null, _boardPen, new Point(0, 0), diameter / 2.0, diameter / 2.0);
            }

            //draw grid lines
            for (radians = 0; radians < (2 * Math.PI); radians += (Math.PI / 6))
            {
                context.DrawLine(_boardPen, new Point(0, 0), new Point(Convert.ToInt32(Math.Cos(radians) * (this.Width / 2))
                    , Convert.ToInt32(Math.Sin(radians) * (this.Width / 2))));
            }		

            context.Close();

            //record possible occupier positions
            diameter = this.Width - Convert.ToInt32(0.1 * Convert.ToDouble(this.Width));
            _positions = new Point[SpiralAI.HUBS, SpiralAI.SPOKES];
            for (hub = 0; hub < SpiralAI.HUBS; hub++)
            {
                for (spoke = 0; spoke < SpiralAI.SPOKES; spoke++)
                {
                    //adjust row and column
                    adjHub = hub + 1;
                    adjSpoke = spoke + 3;

                    //calculate radius and angle
                    radius = Convert.ToDouble((diameter - ((SpiralAI.HUBS - adjHub) * decrement))) / 2;
                    radians = (Math.PI / 6) * adjSpoke;

                    //calculate x and y
                    _positions[hub, spoke].X = Convert.ToInt32(Math.Sin(radians) * radius);
                    _positions[hub, spoke].Y = Convert.ToInt32(Math.Cos(radians) * radius);
                }
            }

            return visual;
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions